diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index f347232..609dd74 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -12,31 +12,55 @@ permissions: concurrency: group: pr-preview-${{ github.event.pull_request.number }} +env: + BUILD_PATH: "." + PREVIEW_HOST: "sentrie-pr-${{ github.event.pull_request.number }}.surge.sh" + PREVIEW_URL: "https://sentrie-pr-${{ github.event.pull_request.number }}.surge.sh" + jobs: deploy-preview: name: Deploy Preview runs-on: ubuntu-latest steps: - - name: Checkout code + - name: Checkout uses: actions/checkout@v4 - - name: Setup Node.js + - name: Detect package manager + id: detect-package-manager + run: | + if [ -f "${{ github.workspace }}/yarn.lock" ]; then + echo "manager=yarn" >> $GITHUB_OUTPUT + echo "command=install" >> $GITHUB_OUTPUT + echo "runner=yarn" >> $GITHUB_OUTPUT + echo "lockfile=yarn.lock" >> $GITHUB_OUTPUT + exit 0 + elif [ -f "${{ github.workspace }}/package.json" ]; then + echo "manager=npm" >> $GITHUB_OUTPUT + echo "command=ci" >> $GITHUB_OUTPUT + echo "runner=npx --no-install" >> $GITHUB_OUTPUT + echo "lockfile=package-lock.json" >> $GITHUB_OUTPUT + exit 0 + else + echo "Unable to determine package manager" + exit 1 + fi + + - name: Setup Node uses: actions/setup-node@v4 with: node-version: "20" - cache: "npm" + cache: ${{ steps.detect-package-manager.outputs.manager }} + cache-dependency-path: ${{ env.BUILD_PATH }}/${{ steps.detect-package-manager.outputs.lockfile }} - name: Install dependencies - run: | - npm cache clean --force - rm -rf node_modules package-lock.json - npm install + run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} + working-directory: ${{ env.BUILD_PATH }} - - name: Build Astro site - env: - ASTRO_PREVIEW: "true" - ASTRO_SITE_URL: "https://sentrie-pr-${{ github.event.pull_request.number }}.surge.sh" - run: npm run build + - name: Build with Astro + run: | + ${{ steps.detect-package-manager.outputs.runner }} astro build \ + --site "${{ env.PREVIEW_URL }}" + working-directory: ${{ env.BUILD_PATH }} - name: Install Surge run: npm install --global surge @@ -46,10 +70,9 @@ jobs: SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} run: | cd dist - PREVIEW_DOMAIN="sentrie-pr-${{ github.event.pull_request.number }}.surge.sh" for i in {1..10}; do if surge --project . \ - --domain "$PREVIEW_DOMAIN" \ + --domain "${{ env.PREVIEW_HOST }}" \ -y; then echo "Surge deploy succeeded!" break @@ -61,16 +84,14 @@ jobs: exit 1 fi done - - name: Verify deployment timeout-minutes: 2 run: | - PREVIEW_URL="https://sentrie-pr-${{ github.event.pull_request.number }}.surge.sh" - echo "Verifying deployment at $PREVIEW_URL..." + echo "Verifying deployment at ${{ env.PREVIEW_URL }}..." for i in {1..10}; do - if curl -fsSL -o /dev/null "$PREVIEW_URL"; then - echo "Deployment verified successfully!" + if curl -fsSL -o /dev/null "${{ env.PREVIEW_URL }}"; then + echo "Deployment verified successfully at ${{ env.PREVIEW_URL }}!" exit 0 fi echo "Attempt $i/10 failed, retrying in 2 seconds..." @@ -97,8 +118,8 @@ jobs: ## 🚀 Preview Deployment - A preview of this PR has been deployed to: + A preview of this PR has been deployed to ${{ env.PREVIEW_URL }}: - 🔗 **[View Preview](https://sentrie-pr-${{ github.event.pull_request.number }}.surge.sh)** + 🔗 **[View Preview](${{ env.PREVIEW_URL }}) at ${{ env.PREVIEW_URL }}** This preview will be automatically removed when the PR is merged or closed. diff --git a/astro.config.mjs b/astro.config.mjs index 0773196..2932f1f 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -27,21 +27,11 @@ export default defineConfig({ label: "Getting Started", items: [ { - label: "What is Sentrie?", - slug: "getting-started/what-is-sentrie", - }, - { - label: "What is Policy as Code?", - slug: "getting-started/what-is-policy-as-code", - }, - { - label: "Why Sentrie?", - slug: "getting-started/why-sentrie", - }, - { - label: "Installation", - slug: "getting-started/installation", + label: "Introduction & Core Philosophy", + slug: "getting-started/introduction", }, + { label: "Quick Start", slug: "getting-started/quick-start" }, + { label: "Installation", slug: "getting-started/installation" }, { label: "Writing your first Policy", slug: "getting-started/writing-your-first-policy", @@ -50,34 +40,37 @@ export default defineConfig({ label: "Running your Policy", slug: "getting-started/running-your-policy", }, - { - label: "Policy Enforcement", - slug: "getting-started/enforcement", - }, + { label: "Enforcement", slug: "getting-started/enforcement" }, ], }, { - label: "Structure of a Policy Pack", + label: "Guides", items: [ + { label: "Testing Policies", slug: "guides/testing-policies" }, { - label: "Overview", - slug: "structure-of-a-policy-pack/overview", + label: "Policy Design Best Practices", + slug: "guides/policy-design-best-practices", }, { - label: "Pack File", - slug: "structure-of-a-policy-pack/packfile", + label: "Deployment and Security", + slug: "guides/deployment-and-security", }, + ], + }, + { + label: "Language Concepts", + items: [ { - label: "Program File", - slug: "structure-of-a-policy-pack/program-file", + label: "Type System & Shapes Overview", + slug: "language-concepts/type-system-shapes", }, { - label: "TypeScript File", - slug: "structure-of-a-policy-pack/typescript-file", + label: "Policy Composition", + slug: "language-concepts/policy-composition", }, { - label: "Example Pack", - slug: "structure-of-a-policy-pack/example-pack", + label: "Pattern Matching & Conditionals", + slug: "language-concepts/pattern-matching-conditionals", }, ], }, @@ -101,6 +94,10 @@ export default defineConfig({ label: "Overview", slug: "reference", }, + { + label: "Identifiers", + slug: "reference/identifiers", + }, { label: "Types and Values", slug: "reference/types-and-values", @@ -153,6 +150,10 @@ export default defineConfig({ label: "Boolean Operations", slug: "reference/boolean-operations", }, + { + label: "Membership Operations", + slug: "reference/membership-operations", + }, { label: "Using Functions", slug: "reference/functions", @@ -173,6 +174,10 @@ export default defineConfig({ label: "Precedence", slug: "reference/precedence", }, + { + label: "Errors", + slug: "reference/errors", + }, { label: "Security and Permissions", slug: "reference/security-and-permissions", @@ -182,10 +187,7 @@ export default defineConfig({ { label: "TypeScript Modules", items: [ - { - label: "Overview", - slug: "reference/typescript_modules", - }, + { label: "Overview", slug: "reference/typescript_modules" }, { label: "JavaScript Globals", slug: "reference/typescript_modules/sentrie/js", @@ -210,14 +212,8 @@ export default defineConfig({ label: "JSON", slug: "reference/typescript_modules/sentrie/json", }, - { - label: "JWT", - slug: "reference/typescript_modules/sentrie/jwt", - }, - { - label: "Net", - slug: "reference/typescript_modules/sentrie/net", - }, + { label: "JWT", slug: "reference/typescript_modules/sentrie/jwt" }, + { label: "Net", slug: "reference/typescript_modules/sentrie/net" }, { label: "Regex", slug: "reference/typescript_modules/sentrie/regex", @@ -230,10 +226,7 @@ export default defineConfig({ label: "Time", slug: "reference/typescript_modules/sentrie/time", }, - { - label: "URL", - slug: "reference/typescript_modules/sentrie/url", - }, + { label: "URL", slug: "reference/typescript_modules/sentrie/url" }, { label: "UUID", slug: "reference/typescript_modules/sentrie/uuid", @@ -241,27 +234,37 @@ export default defineConfig({ ], }, { - label: "CLI Reference", + label: "Extensibility", items: [ { - label: "Overview", - slug: "cli-reference", - }, - { - label: "init", - slug: "cli-reference/init", - }, - { - label: "exec", - slug: "cli-reference/exec", - }, - { - label: "serve", - slug: "cli-reference/serve", + label: "Writing Custom TypeScript Modules", + slug: "extensibility/writing-custom-typescript-modules", }, + ], + }, + { + label: "CLI Reference", + items: [ + { label: "Overview", slug: "cli-reference" }, + { label: "sentrie exec", slug: "cli-reference/exec" }, + { label: "sentrie init", slug: "cli-reference/init" }, + { label: "sentrie serve", slug: "cli-reference/serve" }, + { label: "sentrie validate", slug: "cli-reference/validate" }, + ], + }, + { + label: "Troubleshooting", + items: [ + { label: "Overview", slug: "troubleshooting" }, + { label: "Common Errors", slug: "troubleshooting/common-errors" }, + ], + }, + { + label: "Deployment & Operations", + items: [ { - label: "validate", - slug: "cli-reference/validate", + label: "Running as a Service", + slug: "deployment-operations/running-as-service", }, ], }, diff --git a/docs-template.md b/docs-template.md new file mode 100644 index 0000000..28eb907 --- /dev/null +++ b/docs-template.md @@ -0,0 +1,50 @@ +# [Feature Name] + +[A short, conversational paragraph explaining what this is and the exact problem it solves. Write as if you are explaining it to a colleague. Focus on the "why"—e.g., "When you need to guarantee the exact structure of an incoming payload before evaluating it, use Shapes."] + +Here is the basic syntax: + +```typescript +// The formal definition or type signature +``` + +## Configuration & Arguments + +[A brief, friendly sentence setting up the table, e.g., "You can customize the behavior using the following parameters:"] + +| Argument | Type | Required | What it does | +| :---------- | :----- | :------- | :-------------------------------------------- | +| `paramName` | `Type` | Yes/No | A plain-English explanation of this argument. | + +**Returns:** `ReturnType` — [Explain what the developer gets back in plain English, e.g., "A validated object, or it aborts the evaluation if constraints fail."] + +--- + +## Examples in Action + +### [Scenario 1: e.g., Validating a standard user profile] + +[One to two sentences explaining the real-world context of this example. Why would a developer actually write this?] + +```typescript +// Clean, copy-pasteable code using realistic variable names +// like userAge or billingAddress instead of foo and bar. +``` + +### [Scenario 2: e.g., Handling complex nested objects] + +[Context for the advanced use case. What edge case or complexity does this solve?] + +```typescript +// Advanced example code showing composition or error handling. +``` + +--- + +## Good to Know + +Before you implement this, keep a few boundaries in mind to ensure predictable execution: + +- **[Constraint]:** [e.g., "Sentrie evaluates these strictly. If an incoming payload includes keys not defined in your Shape, the evaluation will fail."] +- **[Performance tip]:** [e.g., "If you are validating deeply nested JSON, keep your recursion depth under X for optimal performance."] +- **[Edge case]:** [e.g., "Null values are only accepted if you explicitly use the `.nullable()` modifier."] diff --git a/src/content/docs/cli-reference/exec.md b/src/content/docs/cli-reference/exec.md index 316f177..1c0d37e 100644 --- a/src/content/docs/cli-reference/exec.md +++ b/src/content/docs/cli-reference/exec.md @@ -1,246 +1,72 @@ --- -title: "exec Command" -description: "Execute a policy or rule with Sentrie." +title: "sentrie exec" +description: "Execute a policy or rule with facts; output decisions to stdout." --- -# exec Command +When you want to run a policy from the command line—to test a rule, debug a decision, or script a one-off check—you use `sentrie exec`. It loads a policy pack, runs the rule(s) you specify with the facts you supply, and prints the decision output. Perfect for local development and CI. -The `exec` command executes a policy or rule from a policy pack. This is the primary way to test and run your policies locally. - -## Syntax - -```bash -sentrie exec [OPTIONS] -``` - -## Description - -The `exec` command loads a policy pack, executes a specific rule or all exported rules in a policy, and displays the results. You can provide facts (input data) via command-line flags or from a JSON file. - -## Arguments - -### `FQN` (required) - -The Fully Qualified Name (FQN) that identifies the namespace, policy, and optionally the rule to execute. - -**Format:** `namespace/policy/rule` or `namespace/policy` - -- **`namespace/policy`** - Execute all exported rules in the policy -- **`namespace/policy/rule`** - Execute only the specific rule - -**Examples:** -- `user_management/user_access` - Execute all exported rules in the `user_access` policy -- `user_management/user_access/allow_user` - Execute only the `allow_user` rule -- `com/example/auth/access_control/check_permission` - Execute a specific rule in a nested namespace - -## Options - -### `--pack-location` - -Specifies the directory containing the policy pack to load. - -```bash -sentrie exec user_management/user_access --pack-location ./my-policy-pack -``` - -**Default:** `./` (current directory) - -**Examples:** -- `--pack-location ./policies` - Load policies from `./policies` directory -- `--pack-location /path/to/policy-pack` - Load policies from absolute path - -### `--output` - -Specifies the output format for the results. - -```bash -sentrie exec user_management/user_access --output json -``` - -**Default:** `table` - -**Valid values:** -- `table` - Human-readable table format (default) -- `json` - JSON format for programmatic consumption - -**Table Format Example:** -``` -Namespace: user_management -Policy: user_access - -Rules: - ✓ allow_admin: ✓ True - ✓ allow_user: ✓ True - -Values: - ✓ allow_admin: true - ✓ allow_user: true - -Attachments: - ✓ allow_user: - reason: User has admin role -``` - -**JSON Format Example:** -```json -[ - { - "namespace": "user_management", - "policyName": "user_access", - "ruleName": "allow_admin", - "decision": { - "state": "TRUE", - "value": true - }, - "attachments": {} - }, - { - "namespace": "user_management", - "policyName": "user_access", - "ruleName": "allow_user", - "decision": { - "state": "TRUE", - "value": true - }, - "attachments": { - "reason": "User has admin role" - } - } -] -``` - -### `--fact-file` - -Specifies a JSON file containing facts to use for policy execution. +Here is the basic syntax: ```bash -sentrie exec user_management/user_access --fact-file ./facts.json -``` - -**Default:** (empty - no file) - -**File Format:** -The file must contain valid JSON with a top-level object: - -```json -{ - "user": { - "role": "admin", - "status": "active" - }, - "context": { - "environment": "production" - } -} +sentrie exec [ --pack-location ] [ --facts ] [ --fact-file ] [ --output (table|json) ] ``` -**Note:** Facts from `--fact-file` are loaded first, then facts from `--facts` flag override any conflicting keys. +**TARGET** is required: `namespace/policy` or `namespace/policy/rule`. The rest are optional: pack directory, inline facts, fact file, and output format. -### `--facts` +## Configuration & Arguments -Provides facts directly as a JSON string. +You can customize the run using the following options: -```bash -sentrie exec user_management/user_access --facts '{"user":{"role":"admin","status":"active"}}' -``` +| Argument | Type | Required | What it does | +| :------- | :--- | :------- | :----------- | +| TARGET | string | Yes | `namespace/policy` (all exported rules) or `namespace/policy/rule` (single rule). Slashes separate namespace, policy, and optional rule. | +| `--pack-location` | path | No | Directory containing the policy pack. Default: `./`. | +| `--facts` | JSON string | No | Inline facts. Keys match policy fact names or aliases. Overrides same keys from `--fact-file`. | +| `--fact-file` | path | No | Path to JSON file; top-level object gives facts. Loaded first; `--facts` overrides. | +| `--output` | enum | No | `table` or `json`. Default: `table`. | -**Default:** `{}` (empty object) +**Returns:** Exit 0 on success; non-zero on error. Output goes to stdout: table (human-readable) or JSON array of decision objects (namespace, policyName, ruleName, decision.state, decision.value, attachments). -**Fact Merging:** -If both `--fact-file` and `--facts` are provided, the facts from `--facts` will override any conflicting keys from the file. This allows you to use a base fact file and override specific values on the command line. +--- -**Example:** -```bash -# facts.json contains: {"user": {"role": "user", "status": "active"}} -# Command line overrides the role -sentrie exec user_management/user_access \ - --fact-file ./facts.json \ - --facts '{"user":{"role":"admin"}}' -# Result: user.role = "admin", user.status = "active" -``` +## Examples in Action -## Examples +### Running a single rule or all rules with inline facts -### Execute a specific rule with inline facts +You are testing one rule or the whole policy and your facts are small enough to pass on the command line. ```bash -sentrie exec user_management/user_access/allow_user \ - --facts '{"user":{"role":"admin","status":"active"}}' +sentrie exec com/example/user_management/user_access/allow_user --facts '{"user":{"role":"user","status":"active"}}' ``` -### Execute all exported rules in a policy - ```bash -sentrie exec user_management/user_access \ - --facts '{"user":{"role":"admin","status":"active"}}' +sentrie exec com/example/user_management/user_access --facts '{"user":{"role":"admin","status":"active"}}' ``` -### Execute with facts from a file - -```bash -sentrie exec user_management/user_access \ - --fact-file ./user-facts.json -``` +### Overriding a fact file from the command line and getting JSON -### Execute with facts from file and override specific values +You have a base fact file but want to override a few keys (e.g. role) for a quick test and pipe the result to another tool. ```bash -sentrie exec user_management/user_access \ +sentrie exec com/example/user_management/user_access \ --fact-file ./base-facts.json \ - --facts '{"user":{"role":"admin"}}' -``` - -### Execute and output as JSON - -```bash -sentrie exec user_management/user_access \ --facts '{"user":{"role":"admin"}}' \ --output json ``` -### Execute from a different pack location - -```bash -sentrie exec com/example/auth/access_control/check_permission \ - --pack-location ./policy-pack \ - --fact-file ./user-facts.json \ - --output json -``` +### Running from a different pack directory and piping JSON -### Pipe JSON output to another tool +Your policy pack lives in another directory and you want JSON output for scripting. ```bash -sentrie exec user_management/user_access \ - --facts '{"user":{"role":"admin"}}' \ - --output json | jq '.[0].decision.value' +sentrie exec com/example/auth/access/allow --pack-location ./policy-pack --facts '{"user":{"role":"admin"}}' --output json ``` -## Decision States - -The command output includes decision states: - -- **`TRUE`** (✓ True) - The rule evaluated to true -- **`FALSE`** (⨯ False) - The rule evaluated to false -- **`UNKNOWN`** (• Unknown) - The rule evaluated to unknown (e.g., when `when` condition is false and no default is provided) - -## Error Handling - -If the command encounters errors: - -- **Invalid FQN**: Returns an error if the namespace, policy, or rule is not found -- **Invalid facts**: Returns an error if facts don't match expected types or shapes -- **Policy errors**: Returns an error if policy evaluation fails -- **File errors**: Returns an error if `--fact-file` cannot be read or parsed - -## Output Destination +--- -All output is written to **stdout**, making it easy to: -- Pipe results to other commands -- Redirect to files -- Process programmatically (with JSON output) +## Good to Know -## See Also +Before you rely on this in scripts or CI, keep a few boundaries in mind: -- [Executing Policies](/running-sentrie/executing-policies) - Detailed guide on executing policies -- [CLI Reference](/cli-reference) - Complete CLI documentation -- [Policy Language Reference](/reference) - Learn about writing policies +- **Constraint:** Required facts must be present (in `--facts` and/or `--fact-file`). Optional facts may be omitted if they have defaults. Keys must match fact names or aliases; types must satisfy declared shapes. Facts from `--fact-file` are loaded first; `--facts` overrides conflicting keys. Table output lists namespace, policy, rules, values, attachments; JSON is an array of decision objects. Decision states are `TRUE`, `FALSE`, or `UNKNOWN`. +- **Edge case:** Invalid or missing TARGET (unknown namespace/policy/rule) → error; rule must be exported. Missing required fact → evaluation error. Invalid JSON in `--facts` or `--fact-file` → error. `--fact-file` path must exist and be readable. Pack directory must contain loadable `*.sentrie`. For the full HTTP API, see [Running as a Service](/deployment-operations/running-as-service). diff --git a/src/content/docs/cli-reference/index.md b/src/content/docs/cli-reference/index.md index 6e30bd6..501f14e 100644 --- a/src/content/docs/cli-reference/index.md +++ b/src/content/docs/cli-reference/index.md @@ -1,28 +1,38 @@ --- -title: "CLI Reference" -description: "Complete reference for the Sentrie command-line interface." +title: "Sentrie CLI" +description: "Command-line reference: sub-commands, global options, and exit codes." --- -# CLI Reference +The Sentrie CLI runs policy evaluations, initializes packs, serves the HTTP API, and validates policy packs. All commands support the global options below. -This document provides comprehensive reference for the Sentrie command-line interface. +## Sub-Commands -## Overview - -Sentrie provides a command-line interface for running policy servers and managing policy packs. The CLI is built on top of the `cling` framework and provides a consistent, user-friendly experience. +| Command | Description | +| :--- | :--- | +| [sentrie exec](/cli-reference/exec) | Execute a policy or rule with facts. | +| [sentrie init](/cli-reference/init) | Create a new policy pack in the current directory. | +| [sentrie serve](/cli-reference/serve) | Start the HTTP server for policy evaluation. | +| [sentrie validate](/cli-reference/validate) | Validate pack structure, syntax, and types. | ## Global Options -All Sentrie commands support these global options: +All commands support these options: + +| Option | Description | Default | +| :--- | :--- | :--- | +| `--help`, `-h` | Show help for the command. | — | +| `--version`, `-v` | Show Sentrie version. | — | +| `--debug` | Enable debug logging. | `false` | +| `--log-level` | Log level: DEBUG, INFO, WARN, ERROR. | `INFO` | + +## Exit Codes -| Option | Description | Default | -| ----------------- | ---------------------------------------- | ------- | -| `--help`, `-h` | Show help information | - | -| `--version`, `-v` | Show version information | - | -| `--debug` | Enable debug logging | `false` | -| `--log-level` | Set log level (DEBUG, INFO, WARN, ERROR) | `INFO` | +| Value | Description | +| :--- | :--- | +| `0` | Success. | +| Non-zero | Failure (e.g. parse error, missing fact, invalid target, port in use). | -## Commands +For `exec` and `validate`, exit 0 when evaluation or validation succeeds; non-zero on error. For `serve`, the process runs until SIGINT/SIGTERM; non-zero on startup failure (e.g. port in use, pack load error). ### `exec` diff --git a/src/content/docs/cli-reference/init.md b/src/content/docs/cli-reference/init.md index b8871e4..8400baa 100644 --- a/src/content/docs/cli-reference/init.md +++ b/src/content/docs/cli-reference/init.md @@ -1,105 +1,62 @@ --- -title: "init" -description: "Initialize a new policy pack." +title: "sentrie init" +description: "Create a new policy pack with sentrie.pack.toml in the current or given directory." --- -The `init` command initializes a new policy pack in the provided directory with the provided name. It creates a `sentrie.pack.toml` file with the correct structure and validates the pack name. +When you are starting a new policy pack and want a valid `sentrie.pack.toml` without editing one by hand, you use `sentrie init`. It creates the pack file with the name you give and the right schema so the rest of the tooling can load the pack. -## Syntax +Here is the basic syntax: ```bash -sentrie init {NAME} [OPTIONS] +sentrie init [ --directory ] ``` -## Arguments +**NAME** is required (e.g. `my-policy-pack`, `com.example.pack`). **--directory** is optional; the directory must be empty. Default is the current directory. -### `NAME` (required) +## Configuration & Arguments -The name of the policy pack. The name must be a valid identifier: +You can customize where the pack is created using the following options: -- Must start with a letter (a-z, A-Z) -- Can contain letters, numbers, underscores (`_`), hyphens (`-`), and dots (`.`) -- Dots can be used for hierarchical names (e.g., `com.example.pack`) -- Each segment after a dot must also start with a letter +| Argument | Type | Required | What it does | +| :------- | :--- | :------- | :----------- | +| NAME | string | Yes | Pack name. Must start with a letter; letters, numbers, underscores, hyphens, dots allowed; each segment after a dot must start with a letter. Invalid: leading digit, leading hyphen, double dot. | +| `--directory` | path | No | Directory for the pack. Must be empty. Default: `./`. | -**Valid examples:** +**Returns:** Exit 0 on success. Creates `sentrie.pack.toml` with `[schema] version = 1`, `[pack] name = ""`, `version = "0.0.1"`. No other files are created. -- `my-policy-pack` -- `my_policy_pack` -- `myPolicyPack` -- `com.example.pack` -- `org.mycompany.policies` - -**Invalid examples:** - -- `123pack` (starts with a number) -- `-mypack` (starts with a hyphen) -- `my..pack` (double dot) -- `my.123pack` (segment after dot starts with a number) - -## Options - -### `--directory` - -Specifies the directory to initialize the policy pack in. - -```bash -sentrie init my-policy-pack --directory ./my-policy-pack -``` - -:::warning[Important] -The directory **MUST be empty**. If the directory contains any files, the command will fail with an error. -::: - -**Default**: `./` (current directory) - -## What Gets Created - -The command creates a `sentrie.pack.toml` file with the following structure: - -```toml -[schema] -version = 1 - -[pack] -name = "my-policy-pack" -version = "0.0.1" -``` +--- -The pack file is validated against the Sentrie pack schema to ensure it's correctly formatted. +## Examples in Action -## Examples +### Creating a pack in the current directory -Create a policy pack in the current directory: +You are in an empty folder and want to turn it into a pack with a given name. ```bash sentrie init my-policy-pack ``` -Create a policy pack in a new directory: +### Creating a pack in a new directory + +You want the pack to live in a new subdirectory (e.g. to keep policies separate from app code). ```bash sentrie init my-policy-pack --directory ./my-policy-pack ``` -Create a policy pack with a hierarchical name: +### Using a hierarchical pack name + +You are naming the pack with a dotted identifier (e.g. for org or product). ```bash sentrie init com.example.iam --directory ./iam-pack ``` -## Error Messages - -If the pack name is invalid, you'll see an error: +--- -```bash -$ sentrie init 123pack -Error: name needs to be a valid identity. It must start with a letter and can only contain letters, numbers, underscores and `dot`. -``` +## Good to Know -If the directory is not empty: +Before you run this, keep a few boundaries in mind: -```bash -$ sentrie init my-pack -Error: directory is not empty - please choose a different directory -``` +- **Constraint:** The directory must exist and be empty. If it contains any files, the command fails. Only `sentrie.pack.toml` is written; structure matches the Sentrie pack schema. Pack name is validated; invalid name produces an error and no file is written. +- **Edge case:** Invalid name (e.g. `123pack`, `-mypack`, `my..pack`, `my.123pack`) → error; no pack file created. Non-empty directory → error (e.g. "directory is not empty - please choose a different directory"). `--directory` must point to an existing directory; `init` does not create the directory. diff --git a/src/content/docs/cli-reference/serve.md b/src/content/docs/cli-reference/serve.md index f9e5fb5..f9b317f 100644 --- a/src/content/docs/cli-reference/serve.md +++ b/src/content/docs/cli-reference/serve.md @@ -1,23 +1,21 @@ --- -title: "serve Command" -description: "Start the Sentrie HTTP server to evaluate policies." +title: "sentrie serve" +description: "Start the HTTP server for policy evaluation; load pack from a directory." --- -# serve Command +When you need to evaluate policies over HTTP (e.g. from a backend or API gateway), you run `sentrie serve`. It loads a policy pack from a directory and exposes a REST API so callers can POST facts and get decisions. Use it for local development or as the process behind a reverse proxy in production. -The `serve` command starts the Sentrie HTTP server to evaluate policies. - -## Syntax +Here is the basic syntax: ```bash -sentrie serve [OPTIONS] +sentrie serve [--pack-location ] [--http-port ] [--http-listen ...] ``` -## Description +All options are optional: pack directory defaults to the current directory, port to `7529`, and listen address to localhost (`--http-listen local`). -The `serve` command starts an HTTP server that provides a REST API for evaluating Sentrie policies. The server loads policy files from a specified directory, creates an index of available policies and rules, and starts listening for HTTP requests. +## Configuration & Arguments -## Options +You can customize where the pack is loaded from and how the server listens using the following options: | Option | Type | Default | Description | | ----------------- | -------- | ----------- | --------------------------------- | @@ -25,12 +23,20 @@ The `serve` command starts an HTTP server that provides a REST API for evaluatin | `--pack-location` | string | `./` | Directory containing policy files | | `--http-listen` | []string | `["local"]` | Address(es) to listen on | +**Returns:** Process runs until SIGINT/SIGTERM (graceful shutdown). Exit non-zero on startup failure (e.g. port in use, pack load or parse error). HTTP API base: `http://:`; see [Running as a Service](/deployment-operations/running-as-service) for request/response format. + ### --http-port -Specifies the port number for the HTTP server to listen on. +--- + +## Examples in Action + +### Starting the server with defaults + +You want to run the server on localhost with the current directory as the pack and default port. ```bash -sentrie serve --http-port 8080 +sentrie serve ``` **Default**: `7529` (PLCY on a phone keypad) @@ -105,39 +111,41 @@ sentrie serve --http-port 8080 sentrie serve --pack-location ./my-policies ``` -### Production Configuration +### Using a custom port and pack path + +You are developing with policies in a separate folder and want the server on a different port. ```bash +sentrie serve --http-port 8080 --pack-location ./my-policies + # Production setup with environment variables export SENTRIE_LOG_LEVEL=WARN export SENTRIE_PORT=8080 sentrie serve --pack-location /etc/sentrie/policies --http-listen 0.0.0.0 ``` -### Development Setup +### Binding to all interfaces for production-style access + +You are running behind a reverse proxy or need the server reachable from other machines; you point it at a fixed pack path and listen on all interfaces. ```bash -# Development setup with debug logging -sentrie serve --debug --log-level DEBUG --pack-location ./policies +sentrie serve --pack-location /etc/sentrie/policies --http-listen 0.0.0.0 --http-port 8080 ``` -### Multiple Listen Addresses +### Listening on multiple addresses with debug logging + +You want the server on specific IPs and more verbose logs for troubleshooting. ```bash -# Listen on multiple addresses -sentrie serve --http-listen 127.0.0.1 --http-listen 192.168.1.100 --http-port 8080 +# Listen on multiple addresses with debug logging +sentrie serve --http-listen 127.0.0.1 --http-listen 192.168.1.100 --http-port 8080 --debug --log-level DEBUG ``` -## Server Behavior +--- -### Startup Process +## Good to Know -1. **Load Pack**: Load policy pack from specified directory -2. **Parse Policies**: Parse all `.sentrie` files -3. **Validate Policies**: Check syntax and semantics -4. **Create Index**: Build index of policies and rules -5. **Start Server**: Begin listening for HTTP requests -6. **Log Status**: Log startup information and any errors +Before you run this in production, keep a few boundaries in mind: ### Pack Loading @@ -412,3 +420,8 @@ sentrie serve \ --pack-location ./policies \ --http-port 3000 ``` + +### Runtime behavior + +- **Constraint:** Startup loads the pack from `--pack-location`, parses `*.sentrie`, validates, builds the index, then listens. Policy or pack errors prevent startup. SIGINT (Ctrl+C) and SIGTERM trigger graceful shutdown; SIGKILL does not. Environment variables `SENTRIE_DEBUG`, `SENTRIE_LOG_LEVEL`, `SENTRIE_PORT` can override when not set via flags. All output goes to stdout/stderr; no file logging by default. +- **Edge case:** Port already in use → startup fails; use another `--http-port`. Invalid or missing pack directory or pack load/parse failure → startup fails. Listening on `0.0.0.0` exposes the server on all interfaces; secure with firewall, reverse proxy, and/or auth. HTTPS and auth are not provided by `serve`; use a reverse proxy. Full HTTP API schema and deployment details: [Running as a Service](/deployment-operations/running-as-service). \ No newline at end of file diff --git a/src/content/docs/cli-reference/validate.md b/src/content/docs/cli-reference/validate.md index 6ea0554..d37c828 100644 --- a/src/content/docs/cli-reference/validate.md +++ b/src/content/docs/cli-reference/validate.md @@ -1,195 +1,64 @@ --- -title: "validate Command" -description: "Validate a policy pack and its structure." +title: "sentrie validate" +description: "Validate pack structure, syntax, and types without executing policies." --- -# validate Command +When you want to catch policy and pack errors before running or deploying (e.g. in CI or before a release), you use `sentrie validate`. It loads the pack and checks structure, syntax, types, and references without executing any rules—so you get fast feedback and no side effects. -The `validate` command validates a policy pack's structure, syntax, and type correctness without executing policies. This is useful for checking that your policies are correctly formatted and can be loaded before deployment. - -## Syntax - -```bash -sentrie validate [OPTIONS] -``` - -## Description - -The `validate` command performs comprehensive validation of a policy pack: - -1. **Pack Loading**: Validates the pack file (`sentrie.pack.toml`) structure -2. **Program Loading**: Loads and parses all `.sentrie` policy files -3. **Index Validation**: Validates namespace, policy, and rule references -4. **Type Checking**: Validates type annotations and constraints -5. **Executor Creation**: Attempts to create an executor to verify the pack is executable - -If validation succeeds, the command exits with code 0. If any validation fails, it exits with a non-zero code and displays error messages. - -## Arguments - -### `FQN` (required) - -The Fully Qualified Name (FQN) that identifies the namespace and policy to validate. The rule component is optional but can be included for reference. - -**Format:** `namespace/policy` or `namespace/policy/rule` - -**Examples:** -- `user_management/user_access` - Validate the `user_access` policy -- `com/example/auth/access_control` - Validate a policy in a nested namespace -- `com/example/auth/access_control/check_permission` - Validate with rule reference (rule is not validated, only used for context) - -## Options - -### `--pack-location` - -Specifies the directory containing the policy pack to validate. +Here is the basic syntax: ```bash -sentrie validate user_management/user_access --pack-location ./my-policy-pack +sentrie validate [ --pack-location ] [ --facts ] ``` -**Default:** `./` (current directory) - -**Examples:** -- `--pack-location ./policies` - Validate policies from `./policies` directory -- `--pack-location /path/to/policy-pack` - Validate policies from absolute path +**TARGET** is required (`namespace/policy` or `namespace/policy/rule`). **--pack-location** and **--facts** are optional; facts are used for type-checking when provided. -### `--facts` +## Configuration & Arguments -Provides facts as a JSON string for type checking. This helps validate that fact declarations match expected types. - -```bash -sentrie validate user_management/user_access --facts '{"user":{"role":"admin","status":"active"}}' -``` +You can point validation at a specific policy and pack using the following options: -**Default:** `{}` (empty object) - -**Note:** The `--facts` flag is primarily used for type checking. The validation process will verify that: -- Fact types match their declarations -- Required facts are present -- Optional facts are correctly marked -- Shape constraints are satisfied - -## Examples - -### Validate a policy pack in the current directory - -```bash -sentrie validate user_management/user_access -``` +| Argument | Type | Required | What it does | +| :------- | :--- | :------- | :----------- | +| TARGET | string | Yes | `namespace/policy` or `namespace/policy/rule`. Namespace and policy must exist; rule is optional (used for context). | +| `--pack-location` | path | No | Directory containing the policy pack. Default: `./`. | +| `--facts` | JSON string | No | Facts for type checking. Validates that fact types and required/optional match declarations. Default: `{}`. | -### Validate a policy pack from a specific location +**Returns:** Exit 0 if validation succeeds; non-zero with error messages if pack loading, parsing, type checking, or executor creation fails. No decision output; validation only. -```bash -sentrie validate com/example/auth/access_control \ - --pack-location ./policy-pack -``` - -### Validate with facts for type checking - -```bash -sentrie validate user_management/user_access \ - --facts '{"user":{"role":"admin","status":"active"}}' -``` - -### Validate a nested namespace policy - -```bash -sentrie validate com/example/billing/pricing \ - --pack-location ./policies -``` - -## What Gets Validated - -The `validate` command checks: - -### 1. Pack File Structure -- Validates `sentrie.pack.toml` exists and is correctly formatted -- Checks pack metadata (name, version, schema version) - -### 2. Policy File Syntax -- Parses all `.sentrie` files in the pack -- Validates namespace declarations -- Checks policy and rule syntax -- Verifies shape definitions - -### 3. Type System -- Validates type annotations on facts, variables, and expressions -- Checks shape field types and constraints -- Verifies constraint validations (min, max, length, etc.) - -### 4. References -- Validates namespace, policy, and rule references -- Checks that imported rules exist -- Verifies exported shapes are accessible -- Validates rule imports and exports - -### 5. Executor Creation -- Attempts to create a runtime executor -- Validates that all TypeScript modules can be loaded -- Checks that all dependencies are resolvable - -## Exit Codes - -- **0** - Validation succeeded -- **1** - Validation failed (with error messages) - -## Error Messages - -Common validation errors include: - -- **Pack loading errors**: Invalid pack file structure or missing pack file -- **Syntax errors**: Invalid policy syntax or grammar violations -- **Type errors**: Type mismatches or invalid type annotations -- **Reference errors**: Missing namespaces, policies, or rules -- **Constraint violations**: Values that don't satisfy shape constraints -- **Module errors**: Missing or invalid TypeScript modules +--- -## Use Cases +## Examples in Action -### Pre-deployment Validation +### Validating a policy in the current directory -Validate policies before deploying to production: +You have just edited a policy and want to confirm it loads and type-checks before committing. ```bash -sentrie validate com/example/auth/access_control \ - --pack-location ./policies +sentrie validate com/example/user_management/user_access ``` -### CI/CD Integration +### Validating from a specific pack path -Use in CI/CD pipelines to catch errors early: +Your pack lives in a different directory (e.g. a monorepo subfolder). ```bash -#!/bin/bash -if ! sentrie validate com/example/auth/access_control; then - echo "Validation failed!" - exit 1 -fi +sentrie validate com/example/auth/access_control --pack-location ./policy-pack ``` -### Type Checking +### Type-checking facts against policy declarations -Validate that facts match expected types: +You want to ensure that the fact shapes and required/optional flags match the JSON you plan to send at runtime. ```bash -sentrie validate user_management/user_access \ +sentrie validate com/example/user_management/user_access \ --facts '{"user":{"role":"admin","status":"active"}}' ``` -## Differences from `exec` +--- -| Feature | `validate` | `exec` | -|---------|-----------|--------| -| Executes policies | ❌ No | ✅ Yes | -| Validates structure | ✅ Yes | ✅ Yes | -| Validates types | ✅ Yes | ✅ Yes | -| Shows results | ❌ No | ✅ Yes | -| Output format | Text errors | Table/JSON | -| Use case | Pre-deployment | Testing/Execution | +## Good to Know -## See Also +Before you rely on this in CI, keep a few boundaries in mind: -- [CLI Reference](/cli-reference) - Complete CLI documentation -- [Policy Language Reference](/reference) - Learn about writing policies -- [Structure of a Policy Pack](/structure-of-a-policy-pack/overview) - Learn about pack structure +- **Constraint:** Validation checks pack file structure, policy parsing and syntax, namespace/policy/rule references, type annotations and shape constraints, and executor creation (including TypeScript module loading). It does not execute rules. When `--facts` is provided, it validates that fact types and required/optional match declarations and that values satisfy shapes. Errors are printed to stderr; no table or JSON decision output. +- **Edge case:** Invalid or missing TARGET (unknown namespace/policy) → error. Pack load failure, parse error, type error, or reference error → non-zero exit. `--facts` must be valid JSON; invalid JSON → error. Unlike `exec`, validate does not run policies or produce decision output; use [sentrie exec](/cli-reference/exec) for execution. diff --git a/src/content/docs/deployment-operations/running-as-service.md b/src/content/docs/deployment-operations/running-as-service.md new file mode 100644 index 0000000..7ac55c8 --- /dev/null +++ b/src/content/docs/deployment-operations/running-as-service.md @@ -0,0 +1,131 @@ +--- +title: Running as a Service +description: "HTTP API for policy evaluation: endpoints, request/response schemas, and error format." +--- + + +Start the server with `sentrie serve`. The API exposes health and decision evaluation. Request/response are JSON. Errors use RFC 9457 Problem Details. + +## Syntax + +```bash +sentrie serve [ --port 7529 ] [ --pack-location ./ ] [ --listen local | all | IP... ] +``` + +## Endpoints + +### GET /health + +**Returns:** `200` with body: + +```json +{ + "status": "healthy", + "time": "" +} +``` + +### POST /decision/{target...} + +Evaluates a policy or rule. `{target...}` = path segments: `namespace/policy` or `namespace/policy/rule`. + +**Request:** `Content-Type: application/json` + +```json +{ + "facts": { + "": , + ... + } +} +``` + +| Field | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| `facts` | object | Yes | Map of fact name (or alias) to JSON value. Must satisfy policy fact types. | + +**Response:** `200` with body: + +```json +{ + "decisions": [ + { + "policy": "string", + "namespace": "string", + "rule": "string", + "decision": { + "state": "TRUE" | "FALSE" | "UNKNOWN", + "value": + }, + "attachments": { "": , ... }, + "trace": { ... } + } + ], + "error": "string" +} +``` + +| Field | Type | Description | +| :--- | :--- | :--- | +| `decisions` | array | One entry per evaluated rule. | +| `decision.state` | string | Trinary outcome. | +| `decision.value` | any | Rule yield value. | +| `attachments` | object | Exported attachments for that rule. | +| `error` | string | Non-empty if execution failed. | + +## Endpoints + +| Endpoint | Method | Path | Body | +| :--- | :--- | :--- | :--- | +| Health | GET | `/health` | none | +| Decision | POST | `/decision/{namespace}/{policy}[/{rule}]` | `{ "facts": { ... } }` | + +**Returns:** See schemas above. 400 for invalid JSON or path; 404 for unknown policy/rule; 405 for wrong method; 500 for evaluation error. + +## Examples in Action + +### Calling the decision endpoint with curl + +```bash +curl -X POST "http://localhost:7529/decision/com/example/auth/user/allow" \ + -H "Content-Type: application/json" \ + -d '{"facts":{"user":{"role":"admin"}}}' +``` + +### Passing multiple facts in the request body + +```bash +curl -X POST "http://localhost:7529/decision/com/example/auth/user" \ + -H "Content-Type: application/json" \ + -d '{"facts":{"user":{"id":"u1","role":"admin"},"context":{}}}' +``` + +## Error Responses (RFC 9457) + +`Content-Type: application/problem+json` + +```json +{ + "type": "https://sentrie.sh/problems/", + "title": "string", + "status": 400 | 404 | 405 | 500, + "detail": "string", + "instance": "string" +} +``` + +| Status | Meaning | +| :--- | :--- | +| 400 | Bad request (invalid JSON, missing path). | +| 404 | Policy or rule not found. | +| 405 | Method not allowed (e.g. GET on /decision). | +| 500 | Internal error during evaluation. | + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- Base URL: default `http://localhost:7529`. `--listen all` binds 0.0.0.0. Only POST is allowed for `/decision/{target...}`. Query parameters are parsed but not used for execution semantics. + + +- Facts must match policy declarations (required facts present; types/shapes valid). Missing or invalid facts can return 400 or 500. CORS headers are sent (`Access-Control-Allow-Origin: *` etc.). No built-in auth or rate limiting. diff --git a/src/content/docs/extensibility/writing-custom-typescript-modules.md b/src/content/docs/extensibility/writing-custom-typescript-modules.md new file mode 100644 index 0000000..f1b4648 --- /dev/null +++ b/src/content/docs/extensibility/writing-custom-typescript-modules.md @@ -0,0 +1,58 @@ +--- +title: Writing Custom TypeScript Modules +description: "How to add and use your own TypeScript modules in a policy pack: paths, exports, and use statement." +--- + + +You can add TypeScript files to your policy pack and import them in policies with the `use` statement. Paths are relative to the policy file or use `@local` (pack root). Built-in modules are `@sentrie/*`; local files use quoted relative paths. + +## Syntax + +```text +use { fn1, fn2 } from "./path/to/file.ts" [ as alias ] +use { fn1 } from "@local/path/to/module" [ as alias ] +``` + +Built-in: `use { fn1 } from @sentrie/module` (no quotes). Local: quoted path or `@local/...`; resolved relative to current file or pack root. + +## Configuration & Arguments + +| Element | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| `fn1`, `fn2` | identifiers | Yes | Exported function (or type) names from the module. | +| `"./file.ts"` or `@local/...` | path | Yes | Relative to `.sentrie` file or pack root. | +| `as alias` | identifier | No | Default alias: last segment of path (e.g. `utils` for `./utils.ts`). | + +**Returns:** N/A (import). Call as `alias.fn1(args)` in the policy. + +## Examples in Action + +### Typical use + +```text +use { calculateAge, validateEmail } from "./utils.ts" as utils +rule myrule = default false { + yield utils.calculateAge(user.birthDate) >= 18 and utils.validateEmail(user.email) +} +``` + +### Going further + +```text +use { calculateAge, validateEmail } from "../helpers/validation.ts" +fact user!: utils.User +yield utils.calculateAge(user.birthDate) >= 18 +``` + +Path resolution: `./file.ts` = same directory as policy; `../parent.ts` = parent; `./utils/helper.ts` = subdirectory. All normalized to `@local` internally. `@local/user/id` → `$PACKROOT/user/id.ts`. + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- TypeScript files must live inside the policy pack root. They can import other local modules and built-in `@sentrie/*` modules. +- Exported functions and types are available to policies. Relative imports in `.ts` files are resolved relative to that file and normalized to `@local`. +- Default alias is the last path segment (e.g. `utils` for `./utils.ts`). Use `as alias` to override. + + +- Module must export the requested names. Invalid path or missing export causes load error. Use [Built-in TypeScript Modules](/reference/typescript_modules/) for common operations; add custom modules for pack-specific logic. diff --git a/src/content/docs/getting-started.md b/src/content/docs/getting-started.md index 68aabfc..bb8ef98 100644 --- a/src/content/docs/getting-started.md +++ b/src/content/docs/getting-started.md @@ -3,8 +3,6 @@ title: "Getting Started" description: "Learn the basics of Sentrie and write your first policy." --- -# Getting Started - Welcome to Sentrie! This guide will help you get up and running with Sentrie quickly. ## What is Sentrie? @@ -218,9 +216,12 @@ This is equivalent to nested calls such as `str.toLower(str.trim(input))`. See [ ## Troubleshooting -### Common Issues +For a catalog of common problems and fixes, see: + +- [Troubleshooting Overview](/troubleshooting) +- [Common Errors](/troubleshooting/common-errors) -**Policy not found**: Make sure your namespace and policy names match exactly in the URL path. +You can also: **Invalid JSON**: Ensure your request body is valid JSON and matches the expected structure. diff --git a/src/content/docs/getting-started/enforcement.md b/src/content/docs/getting-started/enforcement.md index 79e9b8b..d2eef97 100644 --- a/src/content/docs/getting-started/enforcement.md +++ b/src/content/docs/getting-started/enforcement.md @@ -1,74 +1,73 @@ --- title: Enforcement -description: Enforcing policies using Sentrie +description: "Sentrie returns decisions only; enforcement is implemented by the caller." --- -## Enforcement is external +Sentrie is a deterministic policy engine: it takes facts and rules and returns decisions (e.g. true, false, or unknown). It does not block requests, call APIs, or change state. When you need to actually allow/deny, redact, or step up auth, your system (IAM, API gateway, backend) reads the decision and enforces it. That split keeps the engine simple and safe to plug into any stack. -Sentrie is a **deterministic policy decision engine**. It evaluates structured inputs against rules and returns `true`, `false`, or `unknown`. That's it. +Here is the basic idea: -The engine is **pure** and **side-effect-free**—no mutations, no API calls, no state changes. This makes it safe to embed deep in your stack: IAM systems, API gateways, control planes, multi-tenant services, anywhere you need fast, reliable authorization decisions. +**CLI:** Decisions are printed to stdout; exit code indicates success or failure. Your script or service interprets the output and enforces. -Because enforcement lives outside the policy, the same rules work across different systems with different enforcement modes - whether you're blocking requests, redacting data, or triggering step-up auth. +**HTTP:** You POST facts to an endpoint; the response body contains the decision(s). Your service enforces based on that. -:::note[Sentrie makes decisions] -Sentrie focuses on **decision quality** — your systems handle enforcement. -::: +```text +POST /decision/{namespace}/{policy}/{rule} +Content-Type: application/json +Request body: { "facts": { "factName": value, ... } } +Response: decision value(s); see Running as a Service for schema. +``` -## Why Enforcement is External +## Configuration & Arguments -Different platforms enforce differently: +These concepts clarify how decisions and enforcement relate: -- **IAM layers** allow, deny, or escalate auth flows -- **API gateways** throttle, rate-limit, or block routes -- **Multi-tenant systems** control features and per-org limits -- **Security layers** trigger reviews or step-up verification -- **Backend services** serve, redact, or transform data +| Concept | What it means | +| :------ | :------------ | +| Decision | The output of a rule: typically `true`, `false`, or `unknown`. Sentrie only returns this; it does not act on it. | +| Enforcement | What your system does with the decision: allow/deny, redact, step-up auth, throttle, etc. Implemented in IAM, gateway, backend, or feature flags. | +| Determinism | Same facts and policy → same decision. No mutable state or side effects inside the engine; safe to replay and audit. | +| Portability | Same policy runs the same via CLI or HTTP; enforcement logic lives in your integrating system. | -Keeping the engine pure gives you three guarantees: +**Returns:** Sentrie returns decision value(s). Exit code (CLI) or HTTP status indicates success or failure. Sentrie does not perform enforcement. -- **Determinism** — same input = same output, always -- **Reproducibility** — decisions are safe to replay and audit -- **Portability** — runs identically everywhere +--- -This is what makes Sentrie safe for **real-time**, **latency-sensitive** paths. +## Examples in Action -## How It Works +### Letting the caller enforce from the CLI -Call Sentrie's HTTP endpoint with context. Get a decision. **Enforce it**. +You run `sentrie exec` in a script; the script parses stdout and exit code and then allows or denies the operation. ```bash -curl -X POST https://sentrie.host:7529/decision/namespace/policy/rule \ +result=$(sentrie exec com/example/auth/access/allow --facts '{"user": {"role": "user"}}') +# Parse $result; if allow is true, proceed; else deny or redirect. +``` + +### Letting the caller enforce from the HTTP API + +Your backend or gateway calls the Sentrie HTTP endpoint and then enforces based on the response body. + +```bash +curl -s -X POST "https://sentrie.host:7529/decision/com/example/auth/access/allow" \ -H "Content-Type: application/json" \ - -d '{ - "facts": { - "user": "...", - "org": "...", - "roles": ["..."], - "plan": "pro" - } - }' + -d '{"facts": {"user": {"role": "admin", "status": "active"}}}' ``` -What happens next is up to you: +The caller reads the response; if the decision is allow, it allows the request; otherwise it denies, redacts, or escalates. -- Backends accept, reject, or redact responses -- IAM systems allow, deny, or trigger step-up auth -- Gateways block, throttle, or downgrade routes -- Feature gates enable, disable, or shadow-test features -- Entitlement systems enforce quotas or usage limits -- Data layers redact fields before returning them +### Typical enforcement roles -:::note[Philosophy] -Sentrie supplies truth. Systems apply consequences. -::: +- **IAM / Auth:** Allow, deny, or trigger step-up based on the decision. +- **API gateway:** Allow, throttle, or block the route based on the decision. +- **Backend:** Serve full response, redact fields, or return 403 based on the decision. +- **Feature / entitlement:** Enable, disable, or cap usage based on the decision. + +--- -## Why This Works +## Good to Know -Separating decision from action makes Sentrie: +Before you implement this, keep a few boundaries in mind: -- Safe for critical paths -- Consistent across systems -- Easy to test and replay -- Independently scalable -- Flexible for different enforcement modes +- **Constraint:** Sentrie does not mutate state, call external APIs, or perform enforcement. It only evaluates and returns decisions. The system that calls Sentrie (CLI script, API gateway, backend) must interpret the decision and enforce. Same inputs and policy always produce the same decision; safe for critical paths and replay. +- **Edge case:** Sentrie does not enforce. Missing or incorrect enforcement is a caller bug. Timeouts, network errors, and auth to the Sentrie endpoint are the caller’s responsibility. For HTTP request/response schema and status codes, see [Running as a Service](/deployment-operations/running-as-service). diff --git a/src/content/docs/getting-started/installation.md b/src/content/docs/getting-started/installation.md index a9657da..6c6c1b4 100644 --- a/src/content/docs/getting-started/installation.md +++ b/src/content/docs/getting-started/installation.md @@ -3,74 +3,88 @@ title: Installation description: Learn how to install Sentrie --- -Sentrie is a single binary executable. It has no external dependencies. +When you need to run Sentrie locally or on a server, you install a single binary. There are no external dependencies—just the executable. Convenience scripts are provided for macOS, Linux, and Windows so you can install or pin a version with one command. -On macOS, both M1 (arm64) and Intel (x64) executables are provided. On Windows and Linux, both x64 and arm64 executables are provided. +Here is the basic syntax: -Convenience scripts are provided for macOS, Linux, and Windows. - -## Installing the latest version - -### On macOS - -**Recommended: Homebrew Formula** +**macOS / Linux / WSL2:** ```bash -brew install sentrie-sh/tap/sentrie +curl -fsSL https://sentrie.sh/install.sh | bash ``` -> **Why Formulas instead of Casks?** macOS Gatekeeper flags binaries installed via Homebrew Casks, requiring users to manually remove quarantine attributes or approve the binary in System Settings. Formulas are treated as regular binaries by Gatekeeper, avoiding these security warnings and providing a seamless installation experience. - -**Alternative: Install script** +**Windows:** ```bash -curl -fsSL https://sentrie.sh/install.sh | bash +irm https://sentrie.sh/install.ps1 | iex ``` -### On Linux and WSL2 +**Verify:** ```bash -curl -fsSL https://sentrie.sh/install.sh | bash +sentrie --version ``` -### On Windows +## Configuration & Arguments -```bash -irm https://sentrie.sh/install.ps1 | iex -``` +You can install the latest or a specific version depending on your platform: -### Verify Installation +| Platform | Command (latest) | Command (specific version) | +| :------- | :--------------- | :-------------------------- | +| macOS, Linux, WSL2 | `curl -fsSL https://sentrie.sh/install.sh \| bash` | `curl -fsSL https://sentrie.sh/install.sh \| bash -s v0.1.0` | +| Windows | `irm https://sentrie.sh/install.ps1 \| iex` | `$v="0.1.0"; irm https://sentrie.sh/install.ps1 \| iex` | -```bash -sentrie --version -``` +**Returns:** N/A. The script installs the binary; `sentrie --version` confirms the install. + +--- + +## Examples in Action -This should display the current version of Sentrie. +### Installing the latest version on macOS or Linux -## Installing a specific version +You want the current release and are on macOS, Linux, or WSL2. + +```bash +curl -fsSL https://sentrie.sh/install.sh | bash +``` -### On macOS +### Installing the latest version on Windows -For specific versions, use the install script: +You are on Windows and want to use the PowerShell installer. ```bash -curl -fsSL https://sentrie.sh/install.sh | bash -s v0.1.0 +irm https://sentrie.sh/install.ps1 | iex ``` -### On Linux and WSL2 +### Pinning a specific version + +You need a fixed version (e.g. for CI or reproducibility). + +**Unix:** ```bash curl -fsSL https://sentrie.sh/install.sh | bash -s v0.1.0 ``` -### On Windows +**Windows:** ```bash $v="0.1.0"; irm https://sentrie.sh/install.ps1 | iex ``` -### Verify Installation +### Verifying the installation + +You want to confirm the binary is on your PATH and see the version. ```bash sentrie --version ``` + +--- + +## Good to Know + +Before you rely on this in automation, keep a few boundaries in mind: + +- **Constraint:** Sentrie is a single binary; no extra runtime. Supported: macOS (arm64, x64), Linux (x64, arm64), Windows (x64, arm64). +- **Edge case:** Scripts install to a standard location and update PATH as appropriate for the platform. For custom installs or air-gapped environments, you can download the binary directly from the release artifacts. diff --git a/src/content/docs/getting-started/introduction.md b/src/content/docs/getting-started/introduction.md new file mode 100644 index 0000000..7957012 --- /dev/null +++ b/src/content/docs/getting-started/introduction.md @@ -0,0 +1,56 @@ +--- +title: Introduction & Core Philosophy +description: Sentrie is a policy engine with deterministic evaluation. This page states what Sentrie is and why determinism is central. +--- + + +Sentrie is an open-source policy engine. You write business rules in a dedicated language; the engine evaluates them with deterministic, bounded execution. Use it when you need consistent policy decisions across services, auditability, and predictable performance. + +## What Sentrie Is + +- **Policy engine**: Evaluates declarative rules against input facts; returns decisions (e.g. allow/deny, or values). +- **Single binary**: No external runtime; supports macOS (arm64, x64), Linux (x64, arm64), Windows (x64, arm64). +- **Structured language**: Namespaces, policies, rules, facts, shapes; optional TypeScript modules for extra logic. + +## Determinism + +Evaluation is **deterministic**: same inputs and policy pack produce the same outputs. No hidden state, no nondeterministic APIs in the core language. + +- **Bounded execution**: Language is non–Turing complete (no arbitrary loops). All evaluations terminate. +- **Predictable performance**: No infinite loops or stack overflow from policy code. +- **Auditability**: Decisions are traceable; computation is bounded and reproducible. +- **Safe execution**: Designed for production; policy code cannot escape the evaluation sandbox. + +## Syntax + +Program structure (conceptual): + +```text +namespace + +shape { ... } + +policy { + fact : as + rule = default when { yield } + export decision of +} +``` + +- One namespace per file; namespace must be the first statement. +- Policies contain facts (inputs), rules (logic), and exports (rules exposed for evaluation). + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- **Facts**: Required by default; use `?` for optional. All facts are non-nullable. Optional facts may have a default. +- **Rules**: Must yield a value in the body when the `when` condition is true; otherwise the default (or `unknown` if no default) is used. +- **Exports**: A policy must export at least one rule for it to be evaluable via CLI or API. +- **TypeScript modules**: Allowed for extra functions; execution remains bounded by the engine. + + +- Policy logic cannot cause infinite loops or unbounded recursion in the Sentrie language. +- Required facts must be provided at evaluation time or evaluation fails. +- Constraint validation failures abort evaluation immediately. +- TypeScript modules are sandboxed; they cannot break determinism of the policy language itself. diff --git a/src/content/docs/getting-started/quick-start.md b/src/content/docs/getting-started/quick-start.md new file mode 100644 index 0000000..27cd451 --- /dev/null +++ b/src/content/docs/getting-started/quick-start.md @@ -0,0 +1,102 @@ +--- +title: Quick Start +description: Install Sentrie and run your first policy evaluation. +--- + +When you need to run policy checks from the command line or script a one-off evaluation, you install the Sentrie binary and use `sentrie exec` with a target and facts. This page gets you from zero to a first run. + +Here is the basic syntax: + +Install (macOS, Linux, WSL2): + +```bash +curl -fsSL https://sentrie.sh/install.sh | bash +``` + +Install (Windows): + +```bash +irm https://sentrie.sh/install.ps1 | iex +``` + +Verify and run a policy: + +```bash +sentrie --version +sentrie exec [or namespace/policy/rule] [ --facts '' ] +``` + +For a specific version, append it to the install script (e.g. `bash -s v0.1.0` on Unix; set `$v="0.1.0"` on Windows). + +## Configuration & Arguments + +You control what gets evaluated using the pack directory, target, and facts: + +| Argument | Required | What it does | +| :------- | :------- | :------------ | +| Policy pack path | No | Directory containing your `*.sentrie` files (and optional pack manifest). Default: current directory. | +| Target | Yes | `namespace/policy` runs all exported rules; `namespace/policy/rule` runs a single rule. | +| Facts | Depends | JSON object of fact names to values. Required if the policy declares required facts. | + +**Returns:** Exit code 0 on success; non-zero on evaluation or CLI error. Decision output is printed to stdout. + +--- + +## Examples in Action + +### Running a single rule with inline facts + +You have a policy that expects a `user` fact and exports a rule `allow`. You want to see the decision for one concrete user. + +```bash +mkdir my-pack && cd my-pack +sentrie init my-pack +``` + +Add a policy file (e.g. `policy.sentrie`): + +```sentrie +namespace com/example/app + +shape User { + role!: string + status!: string +} + +policy access { + fact user: User as u + + rule allow = default false { + yield u.role == "admin" or (u.role == "user" and u.status == "active") + } + + export decision of allow +} +``` + +From the pack directory, run the rule with facts: + +```bash +sentrie exec com/example/app/access/allow --facts '{"user": {"role": "user", "status": "active"}}' +``` + +### Evaluating every exported rule in a policy + +You want to see the outcome of all exported rules in one policy (e.g. for debugging or auditing). Omit the rule name from the target. + +```bash +sentrie exec com/example/app/access --facts '{"user": {"role": "admin", "status": "active"}}' +``` + +Facts JSON keys must match the fact names (or aliases) declared in the policy. Required facts must be present. + +--- + +## Good to Know + +Before you rely on this in scripts or CI, keep a few boundaries in mind: + +- **Single binary:** No extra runtime; the executable is self-contained. Supported platforms: macOS (arm64, x64), Linux (x64, arm64), Windows (x64, arm64). +- **Facts:** Required facts must be provided; otherwise evaluation fails. Optional facts may be omitted if they have defaults. +- **Target:** Use `namespace/policy` to evaluate all exported rules; use `namespace/policy/rule` for a single rule. Invalid or missing target (unknown namespace/policy/rule) causes an error. +- **Edge case:** Missing required fact → evaluation error. Invalid JSON in `--facts` → CLI error. At least one rule must be exported from a policy to be executable via `sentrie exec`. diff --git a/src/content/docs/getting-started/running-your-policy.md b/src/content/docs/getting-started/running-your-policy.md index f7ca68c..bcfe1ad 100644 --- a/src/content/docs/getting-started/running-your-policy.md +++ b/src/content/docs/getting-started/running-your-policy.md @@ -1,105 +1,91 @@ --- title: Running your first Policy -description: Learn how to run the policy you created using the exec command +description: "Evaluate policies with sentrie exec: target, facts JSON, and output format." --- -Now that you've created your first policy in the [Writing your first Policy](/getting-started/writing-your-first-policy/) guide, let's run it using the `sentrie exec` command. +When you want to run a policy from the command line (e.g. to test a rule or script a one-off check), you use `sentrie exec`. You give it a target (which policy and optionally which rule) and a JSON object of facts; the CLI prints the decision output and exit code. -## Your Policy +Here is the basic syntax: -Assuming you followed the previous guide, you should have a policy file `first-policy.sentrie` that looks like this: - -```sentrie -// first-policy.sentrie -namespace user_management +```bash +sentrie exec TARGET [ --facts JSON ] [ --pack-location PATH ] +``` -shape User { - role: string - status: string -} +**TARGET** is `namespace/policy` (all exported rules) or `namespace/policy/rule` (single rule). Run from the pack directory or pass `--pack-location`. **Facts** are a JSON object whose keys match fact names (or aliases); required facts must be present. -policy user_access { +## Configuration & Arguments - fact user: User +You can control what gets run and where the pack lives using these options: - rule allow_admin = { - yield user.role == "admin" - } +| Argument | Required | What it does | +| :------- | :------- | :----------- | +| Target | Yes | `namespace/policy` or `namespace/policy/rule`. Slashes match namespace and policy/rule identifiers. | +| Facts | Depends | JSON object. Required if the policy declares required facts. Keys match fact names (or aliases). | +| Pack path | No | Default: current directory. Use `--pack-location PATH` to point at a different pack root. | - rule allow_user = { - yield allow_admin or (user.role == "user" and user.status == "active") - } +**Returns:** Exit code 0 on success; non-zero on evaluation or CLI error. Rule names and decision values are printed to stdout (namespace, policy, rules, values, optional attachments). - export decision of allow_admin - export decision of allow_user +--- -} -``` +## Examples in Action -## Running the Policy +### Evaluating a single rule with inline facts -Use the `sentrie exec` command to run your policy against test data: +You have a policy that exports `allow_user` and requires a `user` fact. You want to see the decision for one concrete user. ```bash -sentrie exec user_management/user_access/allow_user --facts '{"user": {"role": "user", "status": "active"}}' +sentrie exec com/example/user_management/user_access/allow_user --facts '{"user": {"role": "user", "status": "active"}}' ``` -:::note -If no rule name is provided, the executor will evaluate all exported rules and return the results. -::: - -**Expected Output:** +Example stdout: -``` -Namespace: user_management +```text +Namespace: com/example/user_management Policy: user_access Rules: - ✓ allow_admin: ⨯ False ✓ allow_user: ✓ True Values: - ✓ allow_admin: false ✓ allow_user: true ``` -## Understanding the Output - -The `exec` command shows you: - -1. **Namespace**: The namespace of the policy -2. **Policy**: The policy name -3. **Rules**: Which rules matched (✓) or didn't match (✗) -4. **Values**: The final results of exported rules -5. **Attachments**: The attachments of the exported rules +### Evaluating every exported rule in a policy -## Providing Required Facts - -Since the `user` fact is required (no `?` modifier), you must provide it when executing: +You want to run all exported rules in one go (e.g. for debugging or auditing). Omit the rule name from the target. ```bash -sentrie exec user_management/user_access --facts '{"user": {"role": "admin", "status": "active"}}' +sentrie exec com/example/user_management/user_access --facts '{"user": {"role": "admin", "status": "active"}}' ``` -:::note[Note] -If a required fact is not provided, execution will fail with an error. Only optional facts (marked with `?`) can have default values and can be omitted. -::: - -**Expected Output:** +Example stdout: -``` -Namespace: user_management +```text +Namespace: com/example/user_management Policy: user_access Rules: - ✓ allow_user: ✓ True ✓ allow_admin: ✓ True + ✓ allow_user: ✓ True Values: - ✓ allow_user: true ✓ allow_admin: true + ✓ allow_user: true +``` + +### Using a specific pack directory + +Your policy pack lives outside the current directory; you want to point `exec` at it explicitly. + +```bash +sentrie exec com/example/user_management/user_access --pack-location /path/to/my-pack --facts '{"user": {"role": "user", "status": "active"}}' ``` -## Next Steps +--- + +## Good to Know + +Before you rely on this in scripts or CI, keep a few boundaries in mind: -Now that you can run policies, explore the [CLI Reference](/cli-reference/) to learn about all available commands and options. +- **Constraint:** Target must be `namespace/policy` or `namespace/policy/rule`; namespace and policy/rule must exist and the rule must be exported. Facts JSON keys must match fact names (or aliases); types must satisfy the declared shapes. If `--pack-location` is omitted, the current directory is the pack root (must contain `*.sentrie` and optionally `sentrie.pack.toml`). +- **Edge case:** Missing required fact → evaluation error; optional fact may be omitted if it has a default. Invalid or malformed JSON in `--facts` → CLI error. Invalid target (unknown namespace, policy, or rule) → error. At least one rule must be exported from the policy to be executable via `sentrie exec`. diff --git a/src/content/docs/getting-started/writing-your-first-policy.md b/src/content/docs/getting-started/writing-your-first-policy.md index 8a5947b..3b98b37 100644 --- a/src/content/docs/getting-started/writing-your-first-policy.md +++ b/src/content/docs/getting-started/writing-your-first-policy.md @@ -1,16 +1,24 @@ --- title: Writing your first Policy -description: Learn how to write your first Sentrie policy +description: "Minimal policy structure: namespace, policy, shape, facts, rules, exports." --- -This guide will walk you through creating your first Sentrie policy step by step. For full syntax, validation, and how multiple **`tag`** lines are indexed, see the [Policy metadata](/reference/policy-metadata/) reference. +When you need to define who can do what (or any decision your app will enforce), you describe it in a Sentrie policy: one namespace per file, shapes for your input data, and policies that declare facts, rules, and exports. This guide walks you through creating your first pack step by step. For full syntax, validation, and how multiple **`tag`** lines are indexed, see the [Policy metadata](/reference/policy-metadata/) reference. -## Basic Policy Structure +Here is the basic syntax: -A Sentrie policy file consists of exactly one namespace and at least one policy: +```text +namespace SLUG -- **Namespace**: A container for related policies. -- **Policy**: A named collection of rules. +shape ShapeName { field!: type field?: type } + +policy IDENT { + fact name: Type [ as alias ] + fact name?: Type [ as alias ] [ default expr ] + rule ruleName = [ default (true|false) ] [ when condition ] { yield expr } + export decision of ruleName +} +``` A policy consists of (in this order; see [Policy metadata](/reference/policy-metadata/) and [Policies](/reference/policies/)): @@ -155,45 +163,36 @@ policy user_access { fact context?: Context as ctx default {"environment": "production"} + rule allow_admin = { -+ yield user.role == "admin" ++ yield currentUser.role == "admin" + } + } ``` -## Add your second rule +The file must start with exactly one `namespace`. One or more `shape` and `policy` blocks follow. Each policy must have at least one `rule` and at least one `export decision of`. -```diff lang=sentrie -// first-policy.sentrie -namespace com/example/user_management +## Configuration & Arguments -shape User { - role: string - status: string -} +You can structure a policy using these elements: -policy user_access { - title "User access" - description "Admin and active-user access for the user management example." - version "1.0.0" - tag "domain" = "user_management" - tag "tier" = "example" +| Argument | Required | What it does | +| :------- | :------- | :----------- | +| Namespace | Yes | Single `namespace SLUG` per file; first statement. Slash-separated (e.g. `com/example/app`). | +| Shape | No | Data model for facts. Required fields: `field!`; optional: `field?`. | +| Policy | No | Named block containing facts, rules, and exports. | +| Fact | No | Input to the policy. Required by default; `?` makes optional. Only optional facts may have `default`. Non-nullable. | +| Rule | Yes (≥1) | Block that yields a decision. May reference other rules in the same policy. | +| Export | Yes (≥1) | `export decision of ruleName` makes the rule callable via CLI/API and importable. | - fact user: User as currentUser - fact context?: Context as ctx default {"environment": "production"} +**Returns:** N/A (authoring). At runtime, evaluation returns the exported rule decision(s) (e.g. boolean). - rule allow_admin = { - yield user.role == "admin" - } +--- -+ rule allow_user = { -+ yield user.role == "user" and user.status == "active" -+ } -} -``` +## Examples in Action -## Composing Rules +### Defining a minimal policy with one rule -Lets use the output of the `allow_admin` rule to update the `allow_user` rule. +You want a single allow/deny decision based on a user shape (e.g. role). You need one namespace, one shape, one rule, and one export. ```diff lang=sentrie // first-policy.sentrie @@ -215,12 +214,12 @@ policy user_access { fact context?: Context as ctx default {"environment": "production"} rule allow_admin = { - yield user.role == "admin" + yield currentUser.role == "admin" } rule allow_user = { -- yield user.role == "user" and user.status == "active" -+ yield allow_admin or user.role == "user" and user.status == "active" +- yield currentUser.role == "user" and currentUser.status == "active" ++ yield allow_admin or currentUser.role == "user" and currentUser.status == "active" } } ``` @@ -251,11 +250,11 @@ policy user_access { fact context?: Context as ctx default {"environment": "production"} rule allow_admin = { - yield user.role == "admin" + yield currentUser.role == "admin" } rule allow_user = { - yield allow_admin or user.role == "user" and user.status == "active" + yield allow_admin or currentUser.role == "user" and currentUser.status == "active" } + export decision of allow_admin @@ -272,12 +271,11 @@ Rules are exported to make them available for external evaluation. This includes Here's a complete policy that checks user access: ```sentrie -// first-policy.sentrie namespace com/example/user_management shape User { - role: string - status: string + role!: string + status!: string } policy user_access { @@ -290,12 +288,35 @@ policy user_access { fact user: User as currentUser fact context?: Context as ctx default {"environment": "production"} - rule allow_admin = { - yield user.role == "admin" + rule allow_admin = default false { + yield currentUser.role == "admin" } - rule allow_user = { - yield allow_admin or user.role == "user" and user.status == "active" + export decision of allow_admin +} +``` + +### Composing rules and exporting multiple rules + +You want to reuse one rule inside another (e.g. “admin or active user”) and expose both outcomes so the CLI or API can call either. + +```sentrie +namespace com/example/user_management + +shape User { + role!: string + status!: string +} + +policy user_access { + fact user: User as currentUser + + rule allow_admin = default false { + yield currentUser.role == "admin" + } + + rule allow_user = default false { + yield allow_admin or (currentUser.role == "user" and currentUser.status == "active") } export decision of allow_admin @@ -303,6 +324,28 @@ policy user_access { } ``` -## Next Steps +### Using an optional fact with a default + +You have a fact that is optional (e.g. context) and you want a default when the caller doesn’t supply it. + +```sentrie +policy user_access { + fact user: User as currentUser + fact context?: document as ctx default {"environment": "production"} + + rule allow = default false { + yield currentUser.role == "admin" + } + export decision of allow +} +``` + +--- + +## Good to Know + +Before you implement this, keep a few boundaries in mind: -Now that you've written your first policy, learn how to [run your policy](/getting-started/running-your-policy/) to see it in action. +- **Constraint:** One namespace per file; namespace must be the first statement. Facts are declared before rules. Required facts must be provided at evaluation time; optional facts may be omitted or use defaults. At least one rule must be exported for the policy to be executable. +- **Constraint:** Only exported rules are available to `sentrie exec` and the HTTP API. Rules in the same policy may reference each other by name (e.g. `yield allow_admin or ...`). +- **Edge case:** Namespace slug uses slashes (e.g. `com/example/app`); no leading or trailing slash. Shape: `field!: type` for required, `field?: type` for optional. Fact without `?` is required; missing required fact at evaluation → error. Optional fact (`?`) may have `default expr`. Rule body must yield a value; you can declare `default false` or `default true`. diff --git a/src/content/docs/guides/deployment-and-security.md b/src/content/docs/guides/deployment-and-security.md new file mode 100644 index 0000000..e193193 --- /dev/null +++ b/src/content/docs/guides/deployment-and-security.md @@ -0,0 +1,56 @@ +--- +title: Deployment and Security +description: "Running Sentrie safely in production: HTTP service, network exposure, and pack permissions." +--- + +Sentrie is a deterministic policy engine that evaluates rules and returns decisions. In production you typically run it as a separate service and call it over HTTP or from automation. This guide covers security-focused aspects of that setup. + +## Serving Sentrie in production + +Use `sentrie serve` to start the HTTP API: + +```bash +sentrie serve --pack-location /etc/sentrie/policies --http-listen 0.0.0.0 --http-port 8080 +``` + +Key points: + +- Sentrie serves plain HTTP; terminate TLS in a reverse proxy (e.g. nginx, Envoy) or API gateway. +- Add authentication and authorization at the proxy/gateway layer before requests reach Sentrie. +- Restrict which networks can reach the Sentrie service (firewalls, security groups, service mesh). + +See: + +- [CLI: `sentrie serve`](/cli-reference/serve) +- [Running as a Service](/deployment-operations/running-as-service) + +## Pack permissions (`fs_read`, `net`, `env`) + +Policy packs declare what they are allowed to reach at runtime in `sentrie.pack.toml`: + +```toml +[permissions] +fs_read = ["./data/**"] +net = ["https://api.example.com"] +env = ["APP_ENV", "API_KEY"] +``` + +Recommendations: + +- **Principle of least privilege**: Only grant the filesystem paths, hosts, and environment variables a pack truly needs. +- **Review permissions for each release**: Treat permission changes like code changes and review them in PRs. +- **Keep secrets in env, not facts**: Let Sentrie read secrets from permitted environment variables instead of embedding them in facts. + +See: [Security and Permissions](/reference/security-and-permissions) + +## Hardening the HTTP surface + +Combine Sentrie with your existing platform primitives: + +- **TLS and mTLS**: Terminate HTTPS at a reverse proxy; optionally enforce mTLS between callers and the proxy. +- **AuthN/AuthZ for callers**: Require auth tokens for access to the Sentrie endpoint and restrict which services can call which decisions. +- **Rate limiting and quotas**: Apply rate limits and timeouts at the proxy; treat Sentrie like any other critical backend. +- **Observability**: Export logs and metrics (through your wrapper or sidecar) so you can monitor error rates and latency. + +Because evaluation is deterministic and side-effect free, you can safely replay requests in non-production environments for debugging and audits. + diff --git a/src/content/docs/guides/policy-design-best-practices.md b/src/content/docs/guides/policy-design-best-practices.md new file mode 100644 index 0000000..87d27de --- /dev/null +++ b/src/content/docs/guides/policy-design-best-practices.md @@ -0,0 +1,53 @@ +--- +title: Policy Design Best Practices +description: Practical guidance for naming, structuring, and composing Sentrie policies. +--- + +This page collects practical advice for designing policies that stay readable and maintainable as your system grows. + +## Namespaces, policies, and rules + +- **Use namespaces for domains**: e.g. `com/example/auth`, `com/example/billing`. Keep related policies in the same namespace. +- **Name policies after subjects**: e.g. `userAccess`, `documentAccess`, `pricing`, `orders`. +- **Name rules after decisions**: e.g. `allow`, `canRead`, `calculatePrice`, `isAdmin`. + +This makes CLI and HTTP targets (`namespace/policy/rule`) self-explanatory. + +See: [Namespaces](/reference/namespaces), [Policies](/reference/policies), [Rules](/reference/rules) + +## Designing facts + +- **Model real inputs**: Facts should mirror the data your callers already have (e.g. `user`, `request`, `resource`, `context`). +- **Prefer shapes over `document`**: Use shapes and field modifiers (`!`, `?`, `!?`) to describe required vs optional data precisely. It improves validation and error messages. See [Shapes](/reference/shapes). +- **Use optional facts sparingly**: Make a fact optional (`fact name?`) only when callers truly may omit it. Provide a `default` where it simplifies callers. + +See: [Facts](/reference/facts) + +## When to compose vs inline + +Sentrie’s export/import model lets you share rules across policies. + +- **Extract shared logic**: Put cross-cutting checks (e.g. “is admin”, “is active user”) into a dedicated policy and export just those rules. +- **Import rather than duplicate**: Use `import decision of ... from ... with ...` instead of copying conditions into every policy. See [Policy Composition](/language-concepts/policy-composition) and [Exporting & Importing Rules](/reference/exporting-and-importing-rules). +- **Avoid deeply nested imports**: Prefer a small number of well-known base policies over long chains of imports that are hard to follow. + +## Using constraints and shapes effectively + +- **Capture validation in types**: Move repeated checks (UUIDs, email addresses, ranges) into constrained shapes like `shape ID string @uuid()` or `shape Percent number @min(0) @max(100)`. +- **Constrain collections at the element type**: e.g. `list[Permission]` where `Permission` is a constrained string shape. +- **Fail early**: Let constraint failures abort evaluation rather than encoding “invalid input” conditions in rules. + +See: [Constraints](/reference/constraints), [Types and Values](/reference/types-and-values) + +## Integrating TypeScript modules + +- **Keep policies declarative**: Use TypeScript for low-level helpers (parsing, normalization, complex calculations), and keep decision logic in Sentrie. +- **Use modules consistently**: Group helpers into a small set of modules (e.g. `utils/time.ts`, `auth/claims.ts`) and import them with clear aliases. +- **Prefer built-ins when available**: Use `@sentrie/*` modules first before adding custom equivalents. See [Using TypeScript](/reference/using-typescript) and the [TypeScript modules reference](/reference/typescript_modules). + +## Testing and versioning + +- **Test policies like code**: Use `sentrie exec` and `sentrie validate` in CI to catch regressions when policies change. See [Testing Policies](/guides/testing-policies). +- **Version packs**: Use the `version` field in `sentrie.pack.toml` to manage releases of your policy pack and coordinate with callers. See [`sentrie.pack.toml`](/structure-of-a-policy-pack/packfile). +- **Document contracts**: In comments near policies, describe what facts are required and what decisions mean so consumers can rely on them. + diff --git a/src/content/docs/guides/testing-policies.md b/src/content/docs/guides/testing-policies.md new file mode 100644 index 0000000..113a227 --- /dev/null +++ b/src/content/docs/guides/testing-policies.md @@ -0,0 +1,85 @@ +--- +title: Testing Policies +description: How to test Sentrie policies locally and in CI using the CLI. +--- + +Good tests make it safe to evolve policies. Sentrie’s CLI is designed to be scriptable, so you can run the same policy both during development and in CI. + +## When to use which command + +- **`sentrie exec`**: Execute rules and see decisions. Use for local development, debugging, and “golden” tests that assert specific outputs. +- **`sentrie validate`**: Load the pack and type-check policies (and optional facts) without executing rules. Use to catch syntax, type, and pack errors early—especially in CI. + +See: + +- [CLI: `sentrie exec`](/cli-reference/exec) +- [CLI: `sentrie validate`](/cli-reference/validate) + +## Quick local testing with `sentrie exec` + +Run a single exported rule with inline facts: + +```bash +sentrie exec com/example/auth/access/allow \ + --facts '{"user":{"role":"admin","status":"active"}}' +``` + +Or evaluate all exported rules in a policy: + +```bash +sentrie exec com/example/auth/access \ + --facts '{"user":{"role":"user","status":"active"}}' +``` + +Tips: + +- Use `--output json` when you want to parse results in scripts. +- Use `--fact-file` to keep larger fact payloads in separate JSON files and override specific keys with `--facts`. + +## Structuring tests with fact files + +For repeatable tests, keep fact files alongside your pack: + +```text +policy-pack/ + sentrie.pack.toml + policies/ + auth.sentrie + facts/ + admin.json + user.json +``` + +Example: + +```bash +sentrie exec com/example/auth/access/allow \ + --pack-location ./policy-pack \ + --fact-file ./policy-pack/facts/admin.json \ + --output json +``` + +Use a simple shell script or Makefile target to run a suite of such commands. + +## Using `sentrie validate` in CI + +Add a validation step to your CI pipeline to catch errors before deploy: + +```bash +# Validate the whole pack from the repo root +sentrie validate com/example/auth/access --pack-location ./policy-pack +``` + +You can optionally pass representative facts to catch shape and constraint issues: + +```bash +sentrie validate com/example/auth/access \ + --pack-location ./policy-pack \ + --facts '{"user":{"role":"admin","status":"active"}}' +``` + +Recommended pattern: + +1. Run `sentrie validate` on the main policies you care about. +2. Run a small set of `sentrie exec ... --output json` commands and assert on the JSON in your CI language of choice. + diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx index 27e3f2f..b8552fc 100644 --- a/src/content/docs/index.mdx +++ b/src/content/docs/index.mdx @@ -6,15 +6,15 @@ description: Sentrie is an open-source policy evaluation engine with a `Rich Exp template: splash pagefind: false hero: - tagline: Sentrie is a modern policy engine built for today’s applications — with a clean expression language, a powerful type system, first-class Typescript module support, and deterministic evaluation. + tagline: Sentrie is a modern policy engine built for today’s applications - with a clean expression language, a powerful type system, first-class Typescript module support, and deterministic evaluation. image: file: ../../assets/freeze.svg actions: - text: Quick Start - link: /getting-started/installation + link: /getting-started/quick-start icon: right-arrow - text: Documentation - link: getting-started/what-is-sentrie + link: getting-started/introduction icon: external variant: minimal --- @@ -24,41 +24,38 @@ import { Card, CardGrid } from "@astrojs/starlight/components"; ## Features - - Powerful expressions with arithmetic, logical, comparison, and collection - operations. Support for quantifiers, pattern matching, and conditional - expressions. + + Install Sentrie and run your first policy evaluation. Installation and basic + usage.
- Learn more + Learn more
- - Build complex policies from simpler policies by exporting decisions that can - be used by other policies. + + Comprehensive type system with shapes, constraints, and built-in + validations.
- Learn more + Learn more
- - Seamlessly integrate TypeScript modules for complex logic. Built-in modules - for data manipulation, cryptography, and more. + + Arithmetic, logical, comparison, and collection operations. Quantifiers, + pattern matching, and conditionals.
- Learn more + Learn more
- - Run Sentrie as a service to evaluate policies using a well-defined and - minimal HTTP API. + + Build complex policies from simpler policies by exporting and importing + decisions.
- Learn more + Learn more
- - The Sentrie CLI provides a powerful set of tools for managing your policies - and JavaScript modules. + + Built-in TypeScript modules and writing custom modules for complex logic.
- Learn more + Learn more
- - Comprehensive type system with shapes, constraints, and built-in - validations. + + Run Sentrie as a service with a well-defined HTTP API for policy evaluation.
- Learn more + Learn more
diff --git a/src/content/docs/language-concepts/pattern-matching-conditionals.md b/src/content/docs/language-concepts/pattern-matching-conditionals.md new file mode 100644 index 0000000..db379d3 --- /dev/null +++ b/src/content/docs/language-concepts/pattern-matching-conditionals.md @@ -0,0 +1,65 @@ +--- +title: Pattern Matching & Conditionals +description: How conditional selection (ternary, Elvis), pattern matching (matches), and state checks work in Sentrie. +--- + +Sentrie uses trinary logic (`true`, `false`, `unknown`) and provides the ternary and Elvis operators for conditional values, plus `matches` for regex and `is defined` / `is empty` for state checks. This page describes how these work. + +## Syntax + +**Ternary:** `condition ? trueValue : falseValue` + +**Elvis:** `expression ?: defaultValue` (equivalent to `expression ? expression : defaultValue`) + +**Pattern match:** `string matches pattern` + +**Definedness:** `value is defined` | `value is not defined` + +**Emptiness:** `value is empty` | `value is not empty` + +## Configuration & Arguments + +| Construct | Left | Right | Description | +| :----------------- | :------------------ | :-------------------- | :---------------------------------------------------------------- | +| `? :` | condition (trinary) | trueValue, falseValue | If condition is truthy, result is trueValue; else falseValue. | +| `?:` | expression | defaultValue | If expression is truthy, result is expression; else defaultValue. | +| `matches` | string | string (regex) | True if string matches the regex pattern. | +| `is [not] defined` | any | - | True if value is [not] undefined. | +| `is [not] empty` | string/list/map | - | True if [not] empty (e.g. `""`, `[]`, `{}`). | + +**Returns:** For ternary/Elvis: the selected value (any type). For `matches`, `is defined`, `is empty`: boolean (or trinary where applicable). Non-truthy for Elvis includes `false`, `null`, `undefined` (treated as unknown), `0`, `""`, empty collections. + +## Examples in Action + +### Typical use + +```sentrie +let status: string = age >= 18 ? "adult" : "minor" +let displayName: string = user.name ?: "Anonymous" +let valid: bool = email matches "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" +let hasPhone: bool = user.phone is defined +``` + +### Going further + +```sentrie +let final_price: number = product.in_stock + ? (product.category == "Electronics" ? product.price * 0.9 : product.price) + : 0.0 + +let safeItems: list[string] = items ?: [] +``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- **Ternary:** Condition is evaluated first; only the chosen branch is evaluated. Nested ternaries are allowed; precedence is right-associative. +- **Elvis:** Shorthand for “use this value or default.” `null`/`undefined` are non-truthy (unknown in trinary), so the default is used. +- **matches:** Right-hand side is a string regex. Patterns are compiled and cached. Match is against the whole string unless the pattern allows partial match. +- **is defined:** Use for optional shape fields or values that may be undefined. Accessing undefined without checking can propagate unknown. +- **is empty:** Applies to strings, lists, and maps. Non-empty means at least one character or element. + +- Unknown (trinary) in a condition is not truthy; the false branch of ternary is used, and Elvis returns the default. +- Regex syntax is that of the engine (e.g. escaping in strings). Invalid regex causes error. +- `in` / `contains` for collections are documented in the reference; use with conditionals as needed. diff --git a/src/content/docs/language-concepts/policy-composition.md b/src/content/docs/language-concepts/policy-composition.md new file mode 100644 index 0000000..8185483 --- /dev/null +++ b/src/content/docs/language-concepts/policy-composition.md @@ -0,0 +1,123 @@ +--- +title: Policy Composition +description: "How exporting and importing rules work: syntax, attachments, sandboxing, and namespace resolution." +--- + + +Policies export rules so they can be executed via CLI/API or imported by other policies. Importing a rule runs it in isolation with only the facts you supply via `with` clauses. This page describes how export and import work. + +## Syntax + +**Export:** +```text +export decision of ruleName +export decision of ruleName + attach name as expression + attach name2 as expression2 +``` + +**Import:** +```text +rule localName = import decision of ruleName + from namespace/policy + with targetFact as expression + with targetFact2 as expression2 +``` + +## Configuration & Arguments + +| Element | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| `ruleName` | identifier | Yes | Rule being exported or imported; must be exported in the target policy. | +| `namespace/policy` | path | Yes | Fully qualified namespace and policy (e.g. `com/example/auth/userAccess`). Same-namespace: policy name only. | +| `with fact as expr` | clause | Yes for each required fact | Maps current policy expression to target policy fact name (alias). | +| `attach name as expr` | clause | No | Evaluated when rule is executed; attached to decision for importers. | + +**Returns:** Exported rule’s decision (trinary or value). If attachments exist, import returns an object with the decision and attachment fields (e.g. `authResult.role`). + +## Examples in Action + +### Typical use + +```sentrie +namespace com/example/auth + +policy userAccess { + fact user: User as currentUser + + rule isAdmin = default false when user.role is defined { + yield user.role == "admin" or user.role == "super_admin" + } + + export decision of isAdmin +} +``` + +```sentrie +namespace com/example/resources + +policy documentAccess { + fact user: User as currentUser + fact document: Document as currentDocument + + rule hasAdminAccess = import decision of isAdmin + from com/example/auth/userAccess + with currentUser as user + + rule canRead = default false { + yield hasAdminAccess or document.owner == user.id + } + + export decision of canRead +} +``` + +### Going further + +Export with attachments; import and use them: + +```sentrie +export decision of isAdmin + attach role as user.role + attach permissions as user.permissions +``` + +```sentrie +rule authResult = import decision of isAdmin + from com/example/auth/userAccess + with currentUser as user + +rule canRead = default false { + let userRole = authResult.role + let userPerms = authResult.permissions + yield authResult and userRole == "admin" +} +``` + +Same-namespace import (policy name only): + +```sentrie +namespace com/example/auth + +policy resources { + rule admin = import decision of isAdmin + from userAccess + with u as user +} +``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- **Export:** A policy must export at least one rule. Only exported rules are executable via CLI/API and importable. Rules in the same policy can reference each other without export. +- **Import:** The imported rule runs in a sandbox: it sees only the facts provided by `with` clauses. It cannot access the calling policy’s other facts or context. Facts are type-checked against the target policy. +- **Attachments:** Evaluated at rule execution; any expression allowed. Import side accesses via dot notation on the imported rule’s result. +- **Recursion:** Circular imports (A imports B, B imports A) are prevented and will fail. +- **Namespace:** Use full `namespace/policy` or, in the same namespace, policy name only. Fact names in `with` must match the target policy’s fact alias. + + +- Rule not found: ensure the rule is exported and the namespace/policy path and rule name are correct (case-sensitive). +- Fact not found: use the target policy’s fact alias in `with`, not the original name. +- Type mismatch: the expression in `with` must evaluate to the type expected by the target policy’s fact. +- Missing attachments: check with `is defined` if an attachment may be absent before accessing it. diff --git a/src/content/docs/language-concepts/type-system-shapes.md b/src/content/docs/language-concepts/type-system-shapes.md new file mode 100644 index 0000000..7f7a85b --- /dev/null +++ b/src/content/docs/language-concepts/type-system-shapes.md @@ -0,0 +1,79 @@ +--- +title: Type System & Shapes Overview +description: "How types, shapes, and constraints work in Sentrie: structure, validation, and composition." +--- + + +The type system defines values (primitives, collections, documents) and structured data (shapes). Constraints validate values at runtime. Understanding how types and shapes work is required to define facts and rules correctly. + +## Syntax + +**Primitives:** `number` | `string` | `trinary` | `bool` | `document` + +**Collections:** `list[T]` | `map[T]` | `record[T1, T2, ...]` + +**Shape (data model):** + +```text +shape Name { + field!: type + field?: type + field: type +} +``` + +**Shape (alias):** `shape Name baseType @constraint1 @constraint2` + +**Composition:** `shape Child with Base { ... }` + +**Constraints:** Applied with `@` on types (e.g. `number @min(0) @max(100)`). + +## Concepts + +| Concept | Required | Description | +| :---------- | :------- | :----------------------------------------------------- | +| Field `!` | No | Required non-null; field must be present and not null. | +| Field `?` | No | Optional; field may be omitted. | +| No marker | - | Required but may be null. | +| `with Base` | No | Shape inherits all fields of Base plus its own. | + +**Returns:** N/A (type system). Constraint validation fails at runtime if a value does not meet the type or constraints; evaluation aborts. + +## Examples in Action + +### Typical use + +```sentrie +shape User { + name!: string + age: number + email?: string +} + +let u: User = { name: "Alice", age: 28 } +``` + +### Going further + +```sentrie +shape Base { id!: string } +shape Extended with Base { role!: string } + +let e: Extended = { id: "1", role: "admin" } +``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- **Primitives:** `number` (float64), `string`, `trinary` (`true`/`false`/`unknown`), `bool` (subset of trinary), `document` (JSON-like). +- **Collections:** `list[T]` index by number; `map[T]` keys are strings (dot or `["key"]` access); `record[T1,T2,...]` fixed-length tuple. +- **Shapes:** Define contracts for facts and `let` bindings. Optional fields may be omitted; use `is defined` to check. +- **Constraints:** Validate at runtime. Failing constraint validation aborts evaluation immediately. +- **Cast:** `cast expr as Type` converts and validates against the target type (and its constraints). + + +- Map keys must be strings. +- Type annotation on `let` is optional; if omitted, values are not validated against types. +- Circular shape composition is not allowed. +- Exported shapes are visible across namespaces; unexported shapes are namespace-local. diff --git a/src/content/docs/quick-glance.mdx b/src/content/docs/quick-glance.mdx index bc19f41..885fbf2 100644 --- a/src/content/docs/quick-glance.mdx +++ b/src/content/docs/quick-glance.mdx @@ -53,8 +53,8 @@ Sentrie is a powerful, flexible policy enforcement engine designed for modern ap Get up and running with Sentrie in minutes: ```bash -# Install Sentrie -go install github.com/sentrie-sh/sentrie@latest +# Install Sentrie (macOS, Linux, WSL2) +curl -fsSL https://sentrie.sh/install.sh | bash # Create your first policy mkdir my-policy && cd my-policy diff --git a/src/content/docs/reference/arithmetic-operations.md b/src/content/docs/reference/arithmetic-operations.md index bf4c0a4..f59366c 100644 --- a/src/content/docs/reference/arithmetic-operations.md +++ b/src/content/docs/reference/arithmetic-operations.md @@ -1,103 +1,78 @@ --- title: Arithmetic Operations -description: Arithmetic operations provide mathematical calculations on numeric values in Sentrie. +description: "Arithmetic operators (+, -, *, /, %), unary +/-, operand types, and edge cases including division by zero." --- -Sentrie supports standard arithmetic operations for working with numeric values. All numeric operations work with the unified `number` type, which is backed by float64. +Arithmetic operators operate on `number` (float64). All numeric operands are treated as `number`; there is no separate integer type. The result of every arithmetic operation is `number`. Division by zero and modulo by zero abort evaluation with an error. -## Basic Operations +## Syntax -### Addition (`+`) +**Binary:** `expr + expr` | `expr - expr` | `expr * expr` | `expr / expr` | `expr % expr` -```sentrie -let sum: number = 5 + 3 -- Result: 8 -let total: number = 2.5 + 1.5 -- Result: 4.0 -``` +**Unary:** `+ expr` | `- expr` -### Subtraction (`-`) +## Configuration & Arguments -```sentrie -let difference: number = 10 - 7 -- Result: 3 -let result: number = 5.5 - 2.5 -- Result: 3.0 -``` +| Operator | Description | Operands | Result | Edge case | +| :------- | :---------- | :------- | :----- | :-------- | +| `+` | Addition | number, number | number | — | +| `-` | Subtraction | number, number | number | — | +| `*` | Multiplication | number, number | number | — | +| `/` | Division | number, number | number | Divisor zero → error; evaluation aborts. | +| `%` | Modulo (remainder) | number, number | number | Divisor zero → error; evaluation aborts. | +| unary `+` | Unary plus | number | number | — | +| unary `-` | Unary minus (negation) | number | number | — | -### Multiplication (`*`) +**Returns:** `number`. Division or modulo by zero aborts evaluation. -```sentrie -let product: number = 4 * 6 -- Result: 24 -let area: number = 3.75 * 2.0 -- Result: 7.5 -``` +## Operand types and conversion -### Division (`/`) +All operands are interpreted as `number`. Mixed integer/float literals are allowed; they are unified as float64. Division is floating-point (e.g. `7 / 2` is `3.5`). Modulo uses the remainder after division; the exact behavior for negative operands follows the implementation (typically Go’s `math.Mod` or similar). -```sentrie -let quotient: number = 15 / 3 -- Result: 5.0 -let precise: number = 7 / 2 -- Result: 3.5 -``` +## Examples in Action -### Modulo (`%`) +### Basic arithmetic ```sentrie -let remainder: number = 10 % 3 -- Result: 1 -let even_check: number = 8 % 2 -- Result: 0 +let sum: number = 5 + 3 +let diff: number = 10 - 7 +let prod: number = 4 * 6 +let quot: number = 15 / 3 +let rem: number = 10 % 3 ``` -## Division and Precision - -### Integer Division +### Division and float result ```sentrie -let result: number = 8 / 3 -- Result: 2.6666666666666665 -let whole: number = 10 / 2 -- Result: 5.0 +let half: number = 7 / 2 -- 3.5 ``` -Division always returns a numeric result. - -### Zero Division +### Unary minus ```sentrie --- This will cause an error -let invalid: number = 5 / 0 -- Error: division by zero +let neg: number = -x +let pos: number = +y ``` -## Mixed Mode Arithmetic - -### Mixed Numeric Operations +### Guarding against division by zero ```sentrie -let mixed_add: number = 5 + 2.5 -- Result: 7.5 -let mixed_multiply: number = 3 * 1.5 -- Result: 4.5 -let result: number = 10 - 7.2 -- Result: 2.8 +let safe: number = divisor != 0 ? 10 / divisor : 0.0 ``` -All numeric values are handled uniformly as the `number` type. - -## Practical Examples - -### Shape Calculations +### Using arithmetic in expressions ```sentrie -shape Rectangle { - width!: number - height!: number -} - -fact rect: Rectangle - let area: number = rect.width * rect.height -let perimeter: number = 2 * (rect.width + rect.height) -let aspect_ratio: number = rect.width / rect.height ``` -### Percentages and Discounts +## Good to Know -```sentrie -let price: number = 100.0 -let discount_percent: number = 15 -let discount_amount: number = price * (discount_percent / 100.0) -let final_price: number = price - discount_amount --- Result: 85.0 -``` +Before you implement this, keep a few boundaries in mind: + +- **Operands:** All operands are `number` (float64). Mixed integer/float literals are allowed; there is no separate integer type. +- **Division:** Result is float (e.g. 7/2 = 3.5). Division by zero aborts evaluation. Use a guard (e.g. ternary or `when`) to avoid dividing by zero. +- **Modulo:** Remainder after division. Divisor zero aborts evaluation. Behavior for negative numbers is implementation-defined (e.g. Go’s `math.Mod`). ### Statistical Operations diff --git a/src/content/docs/reference/boolean-operations.md b/src/content/docs/reference/boolean-operations.md index 24f28d5..9497d57 100644 --- a/src/content/docs/reference/boolean-operations.md +++ b/src/content/docs/reference/boolean-operations.md @@ -1,13 +1,15 @@ --- title: Boolean Operations -description: Boolean operations provide powerful ways to evaluate conditions, compare values, and make decisions in Sentrie. +description: "Logical (and, or, xor, not), comparison (==, !=, <, <=, >, >=), pattern (matches), and conditional (ternary, Elvis) operators." --- -Sentrie provides a comprehensive set of boolean operations that allow you to evaluate conditions, compare values, and make logical decisions in your policies. These operations are essential for creating conditional logic and data validation. +Boolean operations combine conditions, compare values, and branch on truthiness. Logical and comparison operators use [trinary](/reference/trinary) semantics (true, false, unknown). The pattern operator `matches` works on strings and returns a boolean. The ternary (`? :`) and Elvis (`?:`) operators choose a value based on whether a condition is truthy; only `true` is truthy. -## Overview +## Syntax -Boolean operations in Sentrie include: +**Logical (binary):** `and` | `or` | `xor` + +**Negation (unary):** `not expr` | `! expr` - **Trinary Logic**: Three-valued logic with `true`, `false`, and `unknown` - **Conditional Operators**: Ternary (`? :`) and Elvis (`?:`) operators for conditional value selection @@ -17,78 +19,74 @@ Boolean operations in Sentrie include: - **Collection builtins and membership**: List quantifiers (`any`, `all`), plus `in` and `contains` - **State Checking**: Checking emptiness and definedness -## Conditional Operators +**Comparison:** `==` | `!=` | `is` | `is not` | `<` | `<=` | `>` | `>=` -Sentrie provides two operators for conditional value selection: the ternary operator and the Elvis operator. The Elvis operator is a shorthand for a common pattern using the ternary operator. +**Pattern:** `stringExpr matches patternExpr` (both operands strings; right is a regex pattern) -### Ternary Operator (`? :`) +**Conditional:** `condition ? trueValue : falseValue` | `expr ?: default` -The ternary operator allows you to conditionally select between two values based on a boolean condition. +## Configuration & Arguments -#### Syntax +| Operator | Operands | What it does | Returns | +| :------------------- | :------------------------------- | :----------------------------------------------------------------------------------------------------------------------------- | :---------------------- | +| `and` | expr, expr | Logical AND (Kleene). Short-circuits: if left is false, right is not evaluated. | trinary | +| `or` | expr, expr | Logical OR (Kleene). Short-circuits: if left is true, right is not evaluated. | trinary | +| `xor` | expr, expr | Logical XOR. True when exactly one operand is truthy. | trinary | +| `not`, `!` | expr | Logical NOT (unary). One operand; converted to trinary then negated (true↔false; unknown→unknown). | trinary | +| `==`, `is` | expr, expr | Equality. Both sides must be comparable. | trinary | +| `!=`, `is not` | expr, expr | Inequality. | trinary | +| `<`, `<=`, `>`, `>=` | expr, expr | Ordering. Both sides must be comparable (e.g. number, string). | trinary | +| `matches` | string, string | Left: value; right: regex pattern (Go [regexp](https://pkg.go.dev/regexp)). Invalid pattern → error. | bool | +| `? :` | condition, trueValue, falseValue | Ternary: if condition is truthy, result is trueValue; else falseValue. Only the chosen branch is evaluated. Right-associative. | type of chosen branch | +| `?:` | expr, default | Elvis: if expr is truthy, result is expr; else result is default. Non-truthy: false, unknown, null, 0, "", empty collection. | type of expr or default | -```sentrie -condition ? trueValue : falseValue -``` +**Returns:** Trinary for logical and comparison; bool for `matches`; type of the chosen value for ternary and Elvis. -#### Examples +## Truthiness (for ternary and Elvis) -```sentrie -let age: number = 25 -let status: string = age >= 18 ? "adult" : "minor" --- Result: "adult" +Only `true` is truthy. The following are treated as non-truthy: `false`, `unknown`, `null`, `0`, `""`, and empty collections. So `expr ?: default` yields `default` when `expr` is any of these. -let score: number = 85 -let grade: string = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F" --- Result: "B" +## Short-circuit and evaluation order -let user_role: string = "admin" -let can_access: bool = user_role == "admin" ? true : false --- Result: true -``` +- **and:** Left-to-right. If the left operand is false, the right is not evaluated. +- **or:** Left-to-right. If the left operand is true, the right is not evaluated. +- **? ::** Only the branch selected by the condition is evaluated. The condition is always evaluated first. -#### Complex Ternary Logic +## Examples in Action -```sentrie -shape Product { - name!: string - price!: number - category!: string - in_stock: bool -} +### Logical and comparison -fact product: Product +```sentrie +let a: bool = true and false +let b: bool = age >= 18 +let c: bool = not (user.role in allowed_roles) +``` --- Complex pricing logic -let final_price: number = product.in_stock ? - (product.category == "Electronics" ? product.price * 0.9 : product.price) : - 0.0 --- Result: 899.991 (10% discount for electronics) +### Ternary and Elvis --- Status message -let status_message: string = product.in_stock ? - "Available for $" + product.price.toString() : - "Out of stock" --- Result: "Available for $999.99" +```sentrie +let d: string = age >= 18 ? "adult" : "minor" +let e: string = user.name ?: "Anonymous" ``` -### Elvis Operator (`?:`) +### Pattern matching (regex) -The Elvis operator (`?:`) provides a shorthand way to provide a default value when the left-hand expression is not truthy. It is equivalent to using the ternary operator with the same expression on both sides of the `?`. +```sentrie +let g: bool = email matches `^[a-z]+@[a-z]+\\.com$` +``` -When the left-hand expression evaluates to a non-truthy value (including `false`, `null`, `undefined` → `unknown`, `0`, `""`, or empty collections), the default value is used. +Left and right must be strings. The right is interpreted as a Go regexp. Invalid pattern causes an evaluation error. -#### Syntax +### Combining conditions ```sentrie -expression ?: defaultValue +let f: bool = user.role == "admin" or (user.role == "user" and user.status == "active") +let h: bool = not (role in ["guest"]) and status == "active" ``` -This is equivalent to: +## Good to Know -```sentrie -expression ? expression : defaultValue -``` +Before you wire these into policies, keep a few boundaries in mind: #### Examples @@ -632,3 +630,10 @@ let is_admin: bool = user.role == "admin" -- Use 'in' for collection membership let can_read: bool = "read" in user.permissions ``` + +### Boundary conditions + +- **not / !:** Unary prefix; one operand. Converted to trinary then negated. `not unknown` yields `unknown`. You can write `not expr` or `! expr` (no space required after `!`). +- **Short-circuit:** `and` and `or` evaluate left-to-right; the right side may be skipped. The ternary evaluates only the chosen branch. +- **Comparison:** Both sides must be comparable; result is trinary. Equality with `unknown` yields `unknown`. +- **matches:** Left and right must be strings. Right is a [Go regexp](https://pkg.go.dev/regexp) pattern. Invalid pattern causes an evaluation error. For membership in collections or substring checks, use [in](/reference/membership-operations) or [contains](/reference/membership-operations). diff --git a/src/content/docs/reference/constraints.md b/src/content/docs/reference/constraints.md index 04f289e..11ea856 100644 --- a/src/content/docs/reference/constraints.md +++ b/src/content/docs/reference/constraints.md @@ -1,129 +1,176 @@ --- title: Constraints -description: Constraints provide a way to validate values against specific rules in Sentrie. +description: "Constraint syntax and all constraints (e.g. @min, @max, @email) on number, string, list, and trinary types." --- -Constraints provide a way to validate values against specific rules. They are applied using the `@` syntax and are intrinsically tied to the type of the field they validate. +Constraints validate values at runtime using the `@` syntax on types. They apply to primitives, collection elements, and shape fields. Validation failure aborts evaluation. -## Basic Usage +## Syntax ```text -let u: number @min(0) @max(100) = 50 +type @constraint1(args) @constraint2 ``` -:::note -In the example above, `number @min(0) @max(100)` forms the type of the variable `u`. Constraints validate values at runtime. If a value doesn't meet the constraint requirements, validation will fail: +Examples: `number @min(0) @max(100)`, `string @email`, `list[string @one_of("a","b")]`. Multiple constraints can be chained; all must pass. Constraint arguments use the same literal syntax as the language (numbers, strings, lists where applicable). + +## When constraints are checked + +Constraints are checked: + +- When a value is **assigned** to a variable or field (e.g. `let x: number @min(0) = 50` or a shape field). +- When a value is **cast** to a constrained type (e.g. `cast 50 as number @min(0) @max(100)`). +- When **facts** are supplied and matched to shape types that have constraints. + +If any constraint fails, evaluation aborts with an error. Order of application is defined by the runtime; all specified constraints must pass. + +## Configuration & Arguments + +### Numeric constraints (`number`) + +| Constraint | Arguments | Description | +| :-------------------- | :------------------- | :-------------------------------------------------------------------------- | +| `@min(value)` | 1 (number) | Value must be greater than or equal to the specified value. | +| `@max(value)` | 1 (number) | Value must be less than or equal to the specified value. | +| `@eq(value)` | 1 (number) | Value must equal the specified value. | +| `@neq(value)` | 1 (number) | Value must not equal the specified value. | +| `@gt(value)` | 1 (number) | Value must be greater than the specified value. | +| `@lt(value)` | 1 (number) | Value must be less than the specified value. | +| `@in(values...)` | 1 (list) or variadic | Value must be in the specified list of numbers. | +| `@not_in(values...)` | 1 (list) or variadic | Value must not be in the specified list of numbers. | +| `@range(min, max)` | 2 (numbers) | Value must be between min and max (inclusive). | +| `@multiple_of(value)` | 1 (number) | Value must be a multiple of the specified value (divisor must not be zero). | +| `@even()` | 0 | Value must be even. | +| `@odd()` | 0 | Value must be odd. | +| `@positive()` | 0 | Value must be positive (> 0). | +| `@negative()` | 0 | Value must be negative (< 0). | +| `@non_negative()` | 0 | Value must be non-negative (≥ 0). | +| `@non_positive()` | 0 | Value must be non-positive (≤ 0). | + +### Number-specific constraints (`number`) + +Numbers are represented as float64, which can represent ±∞ and NaN. Sentrie **does not produce** these values: there is no infinity literal, and division by zero aborts evaluation. ±∞ and NaN can only appear when a value comes from a TypeScript function (e.g. JS `1/0` → `Infinity`) or from external input. Use these constraints to accept or reject such values: + +| Constraint | Arguments | Description | +| :------------ | :-------- | :---------------------------------------------------------------------------- | +| `@finite()` | 0 | Value must be finite (not ±∞ and not NaN). Use to reject TS/input infinities. | +| `@infinite()` | 0 | Value must be ±∞. Rare; only relevant when TS or input can supply infinity. | +| `@nan()` | 0 | Value must be NaN. Rare; only relevant when TS or input can supply NaN. | + +### String constraints (`string`) + +| Constraint | Arguments | Description | +| :------------------------------ | :----------- | :----------------------------------------------------------------------------------------------- | +| `@length(value)` | 1 (number) | String length must equal the specified value. | +| `@minlength(value)` | 1 (number) | String length must be at least the specified value. | +| `@maxlength(value)` | 1 (number) | String length must be at most the specified value. | +| `@regexp(pattern)` | 1 (string) | String must match the specified regular expression (Go regexp). Invalid pattern causes an error. | +| `@starts_with(substring)` | 1 (string) | String must start with the specified substring. | +| `@ends_with(substring)` | 1 (string) | String must end with the specified substring. | +| `@has_substring(substring)` | 1 (string) | String must contain the specified substring. | +| `@not_has_substring(substring)` | 1 (string) | String must not contain the specified substring. | +| `@email()` | 0 | String must be a valid email address (pattern-based). | +| `@url()` | 0 | String must be a valid URL (pattern-based). | +| `@uuid()` | 0 | String must be a valid UUID. | +| `@alphanumeric()` | 0 | String must contain only alphanumeric characters (letters and digits). | +| `@alpha()` | 0 | String must contain only letter characters. | +| `@numeric()` | 0 | String must be parseable as a numeric value. | +| `@lowercase()` | 0 | String must be lowercase. | +| `@uppercase()` | 0 | String must be uppercase. | +| `@trimmed()` | 0 | String must not have leading or trailing whitespace. | +| `@not_empty()` | 0 | String must not be empty. | +| `@one_of(values...)` | 1+ (strings) | String must be one of the specified values. | +| `@not_one_of(values...)` | 1+ (strings) | String must not be one of the specified values. | + +### List constraints (`list[...]`) + +| Constraint | Arguments | Description | +| :------------- | :-------- | :---------------------- | +| `@not_empty()` | 0 | List must not be empty. | + +### Trinary constraints (`trinary` / `bool`) + +| Constraint | Arguments | Description | +| :--------------- | :---------- | :---------------------------------------------------------------------- | +| `@not_unknown()` | 0 | Value must not be `unknown` (i.e. must be `true` or `false`). | +| `@eq(value)` | 1 (trinary) | Value must equal the specified trinary (`true`, `false`, or `unknown`). | +| `@neq(value)` | 1 (trinary) | Value must not equal the specified trinary. | +| `@is_true()` | 0 | Value must be `true`. | +| `@is_false()` | 0 | Value must be `false`. | + +**Returns:** N/A. Constraint failure raises an error and aborts evaluation. + +## Collection constraints (element-level) + +Constraints can be applied to the **element type** of a list so that each element is validated: ```text -let u: number @min(0) @max(100) = 101 -- This will fail validation +list[string @one_of("read", "write", "delete")] +list[number @min(0) @max(100)] ``` -::: - -## Collection Constraints - -You can apply constraints to elements within collections: +For readability and reuse, define a [shape](/reference/shapes) and use it as the element type: ```text -let permissions: list[string @one_of("read", "write", "delete")] = ["read", "write"] +shape Permission string @one_of("read", "write", "delete") +let permissions: list[Permission] = ["read", "write"] ``` -For better readability, consider using [shapes](/reference/shapes): +## Type conversion with constraints + +When converting with `cast expr as type`, the result is validated against the target type and **all constraints** on that type. If validation fails, evaluation aborts. ```text -shape Permission string @one_of("read", "write", "delete") +let u: number @min(0) @max(100) = cast "50" as number +``` -let permissions: list[Permission] = ["read", "write"] +```text +let u: string @email = cast "user@example.com" as string ``` -:::tip -Use contraints for runtime validation of data. +Casting to a constrained type is equivalent to assigning the cast result to a variable of that type; the same constraint rules apply. -```sentrie -shape Positive100 number @min(0) @max(100) -let y = 50 -let c:Positive100 = y -``` +## Examples in Action -In the above example, the value of `y` is validated against the constraints before being assigned to `c`. -::: - -## Available Constraints - -### Numeric Constraints (`number`) - -| Constraint | Description | -| --------------------- | ----------------------------------------------- | -| `@eq(value)` | Value must equal the specified value | -| `@neq(value)` | Value must not equal the specified value | -| `@gt(value)` | Value must be greater than the specified value | -| `@lt(value)` | Value must be less than the specified value | -| `@in(values...)` | Value must be in the specified list | -| `@not_in(values...)` | Value must not be in the specified list | -| `@range(min, max)` | Value must be between min and max (inclusive) | -| `@multiple_of(value)` | Value must be a multiple of the specified value | -| `@even()` | Value must be even | -| `@odd()` | Value must be odd | -| `@positive()` | Value must be positive | -| `@negative()` | Value must be negative | -| `@non_negative()` | Value must be non-negative (≥ 0) | -| `@non_positive()` | Value must be non-positive (≤ 0) | - -### Number-Specific Constraints - -| Constraint | Description | -| ------------- | ---------------------- | -| `@finite()` | Value must be finite | -| `@infinite()` | Value must be infinite | -| `@nan()` | Value must be NaN | - -### String Constraints - -| Constraint | Description | -| ------------------------------- | --------------------------------------------------- | -| `@length(value)` | String must be exactly the specified length | -| `@minlength(value)` | String must be at least the specified length | -| `@maxlength(value)` | String must be at most the specified length | -| `@regexp(pattern)` | String must match the specified regular expression | -| `@starts_with(substring)` | String must start with the specified substring | -| `@ends_with(substring)` | String must end with the specified substring | -| `@has_substring(substring)` | String must contain the specified substring | -| `@not_has_substring(substring)` | String must not contain the specified substring | -| `@email()` | String must be a valid email address | -| `@url()` | String must be a valid URL | -| `@uuid()` | String must be a valid UUID | -| `@alphanumeric()` | String must contain only alphanumeric characters | -| `@alpha()` | String must contain only letter characters | -| `@numeric()` | String must contain only numeric characters | -| `@lowercase()` | String must be lowercase | -| `@uppercase()` | String must be uppercase | -| `@trimmed()` | String must not have leading or trailing whitespace | -| `@not_empty()` | String must not be empty | -| `@one_of(values...)` | String must be one of the specified values | -| `@not_one_of(values...)` | String must not be one of the specified values | - -### List Constraints - -| Constraint | Description | -| -------------- | ---------------------- | -| `@not_empty()` | List must not be empty | - -## Type Conversion with Constraints - -When converting between types using the `cast .. as` construct, the result is validated against the new type constraints before returning the result. +### Typical use ```text -let u: number = cast "50" as number +let u: number @min(0) @max(100) = 50 +shape Permission string @one_of("read", "write", "delete") ``` +Validation runs at assignment. For example, `let u: number @min(0) @max(100) = 101` would fail. + +### Collection and shapes + ```text -let u: string = cast 50 as string +let permissions: list[string @one_of("read", "write", "delete")] = ["read", "write"] ``` +Using a shape for the element type: + ```text -let u: bool = cast "true" as bool +shape Positive100 number @min(0) @max(100) +let y = 50 +let c: Positive100 = y ``` +Here `y` is validated against `Positive100` (and thus `@min(0) @max(100)`) before assignment to `c`. + +### Trinary constraints + ```text -let u: document = cast { "name": "John", "age": 30 } as document +let flag: trinary @is_true = true +let known: trinary @not_unknown = some_expression ``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- Constraints are checked at runtime when a value is assigned or cast to the constrained type (and when facts are matched to constrained shape types). +- Order of application is defined by the runtime. All specified constraints must pass; failure aborts evaluation immediately. +- Use shapes to reuse constrained types (e.g. `shape ID string @uuid()`) and to name element types in collections. +- Numeric constraints use float64; `@even` and `@odd` use modulo. `@multiple_of` uses a small epsilon for floating-point remainder checks. +- **Infinity and NaN:** The language has no infinity or NaN literal, and division by zero aborts (it does not produce `±∞`). So `@infinite()` and `@nan()` only apply when a number comes from TypeScript or external input (e.g. a JS function returning `Infinity`). +- String `@regexp` uses Go’s [regexp](https://pkg.go.dev/regexp) semantics. Invalid pattern strings cause an evaluation error. +- Map types currently have no built-in constraints; only list and element types (e.g. `list[T]` with constrained `T`) are covered above. diff --git a/src/content/docs/reference/errors.md b/src/content/docs/reference/errors.md new file mode 100644 index 0000000..d4fb820 --- /dev/null +++ b/src/content/docs/reference/errors.md @@ -0,0 +1,66 @@ +--- +title: Error Reference +description: Overview of Sentrie error categories, CLI exit codes, and HTTP problem responses. +--- + +This page summarizes how Sentrie reports errors across the CLI, pack loading, evaluation, and the HTTP service. + +## Error categories + +- **Parse and pack errors**: Syntax issues in `*.sentrie` files or invalid `sentrie.pack.toml`. +- **Type and constraint errors**: Type mismatches, missing required facts, and constraint violations. +- **Evaluation errors**: Failures while evaluating a rule (often surfaced as type or constraint errors at a specific expression). +- **Service/API errors**: HTTP 4xx/5xx responses when using `sentrie serve`. + +## CLI exit codes + +All CLI commands follow a simple convention: + +- Exit code **0**: success. +- Non-zero: failure (message written to stderr). + +Common cases: + +- `sentrie exec`: non-zero on pack load failure, invalid target, missing required facts, or evaluation error. +- `sentrie validate`: non-zero on pack load failure, parse or type error, or invalid facts passed for validation. +- `sentrie serve`: non-zero on startup if the pack cannot be loaded or the port/listen configuration is invalid. + +See: + +- [CLI Reference](/cli-reference) +- [sentrie exec](/cli-reference/exec) +- [sentrie validate](/cli-reference/validate) +- [sentrie serve](/cli-reference/serve) + +## HTTP problem details + +When running as a service, Sentrie uses [RFC 9457 problem+json](https://www.rfc-editor.org/rfc/rfc9457) for error responses: + +```json +{ + "type": "https://sentrie.sh/problems/", + "title": "string", + "status": 400, + "detail": "string", + "instance": "string" +} +``` + +Typical status codes: + +| Status | Meaning | +| :----- | :------ | +| `400` | Bad request (invalid JSON, missing or malformed path, bad `facts` shape). | +| `404` | Policy, namespace, or rule not found. | +| `405` | Method not allowed (e.g. non-POST on `/decision`). | +| `500` | Internal error during evaluation. | + +See: [Running as a Service](/deployment-operations/running-as-service) + +## Relationship to troubleshooting + +Use this page as a high-level map of how Sentrie reports errors. For a task-oriented list of frequent issues, concrete messages, and fixes, see: + +- [Troubleshooting Overview](/troubleshooting) +- [Common Errors](/troubleshooting/common-errors) + diff --git a/src/content/docs/reference/exporting-and-importing-rules.md b/src/content/docs/reference/exporting-and-importing-rules.md index f4dbda3..668c904 100644 --- a/src/content/docs/reference/exporting-and-importing-rules.md +++ b/src/content/docs/reference/exporting-and-importing-rules.md @@ -3,9 +3,8 @@ title: "Exporting and Importing Rules" description: "Learn how to export rules from policies and import them into other policies for code reuse and composition." --- -# Exporting and Importing Rules -Sentrie allows you to export rules from one policy and import them into another, enabling code reuse and policy composition. This guide covers how to export rules, import them, and work with attachments. +Sentrie allows you to export rules from one policy and import them into another, enabling code reuse and policy composition. This guide focuses on patterns, best practices, and troubleshooting. For the core syntax and evaluation semantics of export/import, see [Policy Composition](/language-concepts/policy-composition). ## Why Export and Import? diff --git a/src/content/docs/reference/facts.md b/src/content/docs/reference/facts.md index 47d46ce..14779bc 100644 --- a/src/content/docs/reference/facts.md +++ b/src/content/docs/reference/facts.md @@ -1,35 +1,50 @@ --- title: Facts -description: Facts are input data declarations that provide external values to policy evaluation. +description: "Fact declaration syntax (required/optional, type, alias, default) and binding at evaluation." --- -Facts are named input values that can be injected into policy evaluation. They serve as the primary mechanism for providing external data to policies, enabling them to make decisions based on runtime information. +Facts are named, typed inputs to a [policy](/reference/policies). They are declared at the top of the policy (before [let](/reference/let), [use](/reference/functions), and [rules](/reference/rules)). Each fact has a name, a type (shape or primitive), an optional alias used in the policy body, and—only for optional facts—an optional default expression. Required facts must be supplied at evaluation time or evaluation fails. Facts are non-nullable when supplied: null is not allowed as a fact value. :::note[Note] Facts live in the **facts** section of a policy: after optional [metadata](/reference/policy-metadata/) and before any `use`. Comments may appear anywhere. See [Policies](/reference/policies/) for full ordering. ::: -## Fact Declaration +## Syntax -### Basic Syntax +```text +fact name : type [ as alias ] [ default expr ] -- required +fact name? : type [ as alias ] [ default expr ] -- optional +``` -```sentrie --- Required fact (default behavior) -fact : ('as' )? +- **name / name?:** Identifier. Trailing `?` makes the fact optional; otherwise it is required. +- **type:** Any type: primitive (`number`, `string`, `trinary`, `bool`, `document`), collection (`list[T]`, `map[T]`, `record[...]`), or a [shape](/reference/shapes). The supplied value is validated against this type (and any constraints) at evaluation. +- **as alias:** Optional. Identifier used in the policy body to refer to this fact. If omitted, the body uses `name`. +- **default expr:** Optional. Allowed only for **optional** facts (`name?`). Used when the fact is omitted from the input. The expression is evaluated in the policy context (e.g. can reference other facts or policy-level let if order allows). --- Optional fact (can have default) -fact ?: ('as' )? ('default' )? -``` +## Configuration & Arguments -:::note[Note] -- The `exposed_name` (via `as`) is the name that will be used to reference the fact when the policy is evaluated -- If no `as` clause is provided, the fact name itself is used as the exposed name -- The `default` clause is only allowed for optional facts (marked with `?`) -- Facts are **required by default** - use `?` to make them optional -- Facts are **always non-nullable** - null values are not allowed -::: +| Part | Type | Required | Description | +| :--- | :--- | :------- | :---------- | +| `name` | identifier | Yes | Declaration name. Used in `with` bindings when another policy [imports](/language-concepts/policy-composition) this policy: the binding key must match the **alias** (or name if no `as`). | +| `name?` | - | No | `?` makes the fact optional. Optional facts may be omitted from the input; if omitted, `default expr` is used if present. | +| `type` | type ref | Yes | Type of the fact value. Can be a shape, primitive, or collection. Validation (and [constraints](/reference/constraints)) runs when the fact is bound. | +| `as alias` | identifier | No | Name used in the policy body. If absent, body uses `name`. Import `with` must use the alias (or name if no alias). | +| `default expr` | expression | No | Only for optional facts. Evaluated when the fact is omitted. Type should match the fact type (and constraints). | + +**Returns:** N/A (declaration). At evaluation time, the fact name (or alias) is bound to the provided value (or default). The policy body refers to the fact by alias or name. + +## Required vs optional + +- **Required (`fact name : type`):** The evaluator must receive a value for this fact. If it is missing, evaluation fails (e.g. `ErrRequiredFact`). The value must not be null and must conform to the declared type and constraints. +- **Optional (`fact name? : type`):** The evaluator may omit this fact. If omitted, the fact is bound to `default expr` if present; otherwise the behavior is tooling-defined (e.g. undefined or a type-specific default). If supplied, the value must be non-null and conform to the type and constraints. + +## Import binding (policy composition) -### Required vs Optional Facts +When another policy imports a decision from this policy (e.g. `import decision of RuleName from "ns" with ...`), the `with` clause binds values to the **target policy’s facts**. The key in `with` must be the **alias** of the fact (or the fact name if no `as` was used). The value must match the fact’s type and constraints. + +## Examples in Action + +### Required fact with alias ```sentrie -- Required fact (must be provided during execution, default behavior) @@ -85,53 +100,32 @@ shape User { -- Required shape fact fact user: User as currentUser - --- Optional shape fact with default -fact user?: User as currentUser default { "id": "", "role": "guest", "permissions": [] } ``` -## Fact Modifiers - -### Required Facts (Default) +In the policy body, use `currentUser`. When importing this policy’s decision, bind with the key `currentUser` (e.g. `with currentUser = someValue`). -Facts are **required by default**. They must be provided during policy execution, and they cannot have default values. +### Optional fact with default ```sentrie --- Required fact - must be provided during evaluation -fact user: User as user - --- This will cause an error: required facts cannot have defaults --- fact user: User as user default { "id": "", "role": "guest" } -- Error! +fact context?: Context as ctx default {} ``` -### Optional Facts (`?`) +If the caller omits `ctx`, it is bound to `{}`. If provided, it must be non-null and of type `Context`. -Use the `?` operator to mark a fact as optional. Optional facts can be omitted during execution, and they can have default values. +### Primitive and document facts ```sentrie --- Optional fact - can be omitted during evaluation -fact context?: Context as context - --- Optional fact with default value -fact context?: Context as context default { "key": "value" } - --- If not provided, the default value will be used (if specified) --- If no default is provided and the fact is omitted, it simply won't be available +fact userId: string as id +fact config?: document as settings default { "env": "production" } ``` -:::warning[Important] -- Facts are **always non-nullable** - Even if a fact is optional, if it is provided, it cannot be null -- The `!` operator is **not supported** for facts - facts are always non-nullable by design -- Only **optional facts** (`?`) can have default values -::: - -## Default Values - -### Literal Defaults - -Default values can only be used with **optional facts** (marked with `?`). +### Multiple facts, mix of required and optional ```sentrie +fact user: User as currentUser +fact request: Request as req +fact options?: Options as opts default {} + -- String defaults (optional facts) fact name?: string as userName default "anonymous" @@ -144,106 +138,13 @@ fact enabled?: bool as isEnabled default true -- Collection defaults (optional facts) fact tags?: list[string] as itemTags default [] -fact config?: dict[string] as settings default {} -``` - -:::warning[Error] -Required facts cannot have default values. This will cause a compilation error: - -```sentrie --- This will cause an error -fact name: string as userName default "anonymous" -- Error: required fact cannot have default -``` -::: - -### Shape Defaults - -```sentrie -shape Product { - id!: string - name!: string - price!: number -} - --- Optional fact with shape default -fact product?: Product as currentProduct default { - "id": "", - "name": "Unknown", - "price": 0.0 -} -``` - -## Fact Injection - -### During Policy Import - -```sentrie --- Import policy with fact injection -rule authResult = import decision of canAccess - from com/example/auth/userAccess - with user as { "id": "123", "role": "admin", "permissions": ["read", "write"] } -``` - -### Multiple Fact Injection - -```sentrie --- Inject multiple facts -rule result = import decision of processOrder - from com/example/orders/orderProcessing - with user as currentUser - with order as orderData - with context as requestContext -``` - -## Type Safety - -### Runtime Validation - -```sentrie --- Type validation: This will cause a type error -fact user?: User as user default { "id": 123 } -- Error: string expected, got number - --- Correct usage -fact user?: User as user default { "id": "123" } - --- Null validation: Facts cannot be null --- This will cause a runtime error if null is provided -fact user: User as user -- If null is provided, error: "fact 'user' cannot be null" +fact config?: map[string]any as settings default {} ``` -## Best Practices +## Good to Know -### Use Descriptive Names - -```sentrie --- Good: Clear, descriptive fact names -fact currentUser?: User as user default { "id": "", "role": "guest" } -fact orderData?: Order as order default { "id": "", "items": [] } +Before you implement this, keep a few boundaries in mind: --- Avoid: Generic or unclear names -fact data?: User as d default { "id": "", "role": "guest" } -``` - -### Provide Sensible Defaults - -```sentrie --- Good: Meaningful default values for optional facts -fact user?: User as user default { "id": "anonymous", "role": "guest", "permissions": [] } - --- Avoid: Confusing or invalid defaults -fact user?: User as user default { "id": "", "role": "invalid", "permissions": null } -- null not allowed -``` - -### Use Required Facts Appropriately - -```sentrie --- Good: Use required facts for data that must always be provided -fact userId: string as id -- Must be provided, no default allowed - --- Good: Use optional facts with defaults for data that has sensible fallbacks -fact context?: Context as ctx default { "environment": "production" } - --- Avoid: Making everything required when defaults would be appropriate -fact userId: string as id -- If this often has a default, consider making it optional -fact userId?: string as id default "anonymous" -- Better if default makes sense -``` +- **Constraint:** Facts must be declared before rules; only facts or comments may precede fact declarations. Required facts must be supplied at evaluation or evaluation fails. Optional facts may be omitted; if supplied they must be non-null. Null is not allowed for any fact. Default is used when an optional fact is omitted. +- **Import:** When using [policy composition](/language-concepts/policy-composition), the fact name in the `with` clause must match the **alias** (or name if no `as`) in the target policy. The type of the supplied value must match the fact type (and constraints) or evaluation fails. +- **Order:** Fact declarations may reference only prior facts in their `default expr` (if the implementation allows it); typically defaults are literals or simple expressions. diff --git a/src/content/docs/reference/functions.md b/src/content/docs/reference/functions.md index b7553f0..e3e18cf 100644 --- a/src/content/docs/reference/functions.md +++ b/src/content/docs/reference/functions.md @@ -5,71 +5,71 @@ description: How to call functions in Sentrie, import TypeScript modules, and us Functions are a fundamental part of Sentrie that allow you to perform operations, transform data, and extend functionality. Sentrie supports **built-in functions** (see [Built-in Functions](/reference/built-in-functions)) that are always available without imports, and **TypeScript module functions** that you import using the `use` statement. -Functions in Sentrie enable you to: +## Syntax -- Perform common operations using built-in reusable utilities -- Extend functionality by importing functions from TypeScript modules -- Optimize performance with function **memoization** +**Call (direct):** `functionName(arg1, arg2, ...)` -## Function Call Syntax +**Call (via alias):** `alias.functionName(arg1, arg2, ...)` -### Basic Syntax +**Import:** `use { fn1, fn2, ... } from source [ as alias ]` -Functions are called using the standard function call syntax with parentheses: +- **source:** Either a built-in module reference (e.g. `@sentrie/hash`, no quotes) or a relative path string (e.g. `"./utils.ts"`, in quotes). +- **as alias:** Optional. Identifier used as the namespace for the imported functions. If omitted, the default alias is typically the last path segment of the source (e.g. `hash` for `@sentrie/hash`). -```sentrie -let name = functionName(argument1, argument2, ...) -``` +## Configuration & Arguments -### Module Functions +| Element | Type | Required | Description | +| :------ | :--- | :------- | :---------- | +| `source` | `@sentrie/module` or `"./path.ts"` | Yes | Built-in: `@sentrie/name` (no quotes). Relative: quoted string path from the policy file (or pack root). | +| `as alias` | identifier | No | Namespace for the imported functions. Omitted: default alias is last segment of source (e.g. `hash` for `@sentrie/hash`). | +| Function args | expressions | Per function | Typed and documented per module. See [Built-in TypeScript modules](/reference/typescript_modules/) and [Writing custom TypeScript modules](/extensibility/writing-custom-typescript-modules). | -Functions imported from TypeScript modules are called using the module alias followed by a dot: +**Returns:** Per function; see the module’s documentation. Invalid arguments or runtime errors in the function abort evaluation. -```sentrie -namespace com/example/auth +## Where use is allowed + +`use` is a policy-level statement. It must appear inside a [policy](/reference/policies) block, after [facts](/reference/facts) and before or alongside [let](/reference/let) and [rules](/reference/rules), per the policy statement order. The imported functions are visible to all rules and let bindings in that policy. They are not visible in other policies unless those policies declare their own `use`. + +## Resolution of source + +- **Built-in:** `@sentrie/name` refers to a built-in module (e.g. `@sentrie/hash`, `@sentrie/time`). The runtime resolves these to the provided implementations. +- **Relative:** A string path (e.g. `"./utils.ts"`) is resolved relative to the policy file or the pack root, per tooling. Only paths that the [permissions](/reference/security-and-permissions) allow can be loaded. +### Example + +```sentrie policy mypolicy { fact data: string use { sha256, now } from @sentrie/hash as hash - use { parse } from @sentrie/json as json + use { parse } from @sentrie/js as json rule processData = default false { let hashValue = hash.sha256(data) let currentTime = hash.now() let parsed = json.parse(data) - yield hashValue != "" and currentTime > 0 + yield hashValue != "" and currentTime > 0 and parsed is defined } - - export decision of processData } ``` -:::note -If no alias is specified, the default alias is the last part of the module path: +## Examples in Action + +### Import and call (no alias, default alias) ```sentrie use { sha256 } from @sentrie/hash --- Default alias is "hash", so you call: hash.sha256(data) +let h = sha256(data) ``` -::: - -## TypeScript Module Functions - -Sentrie allows you to import and use functions from TypeScript modules, including built-in `@sentrie/*` modules and your own local TypeScript files. This provides extensive functionality for cryptography, data manipulation, time operations, and more. - -### Importing Functions - -Functions are imported using the `use` statement: +### Import with alias ```sentrie -use { function1, function2 } from @sentrie/module as alias +use { now } from @sentrie/time as time +let t = time.now() ``` -### Built-in Modules - -Sentrie provides comprehensive built-in TypeScript modules under the `@sentrie/*` namespace: +### Multiple functions and custom module ```sentrie namespace com/example/crypto @@ -79,8 +79,7 @@ policy security { fact timestamp: number use { sha256, md5 } from @sentrie/hash - use { now, parse } from @sentrie/time as time - use { isValid } from @sentrie/json as json + use { now } from @sentrie/time as time rule validatePassword = default false { let hash = hash.sha256(password) @@ -92,9 +91,7 @@ policy security { } ``` -### Local TypeScript Modules - -You can also import functions from your own TypeScript files: +### Local TypeScript modules ```sentrie namespace com/example/utils @@ -242,3 +239,12 @@ Sentrie provides builtins such as `count`, `merge`, and list helpers (`any`, `al - [Built-in TypeScript Modules](/reference/typescript_modules) - Complete reference for all built-in modules - [Intermediate Values](/reference/let) - Learn about `let` declarations where functions are commonly used - [Rules](/reference/rules) - Learn how to use functions in rule bodies + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- **Memoization:** Functions may be memoized per (function, arguments) when applicable; repeated calls with the same arguments may return a cached result. This is implementation-defined. +- **Scope:** `use` is per policy. The alias (or default) is used only in that policy. Other policies must declare their own `use` to call the same or other modules. +- **Errors:** Missing or wrong-type arguments, or runtime errors inside the function, can cause evaluation to abort. See [Built-in TypeScript modules](/reference/typescript_modules/) and [Writing custom TypeScript modules](/extensibility/writing-custom-typescript-modules) for function contracts and types. +- **Permissions:** Custom modules (e.g. `"./file.ts"`) are subject to [security and permissions](/reference/security-and-permissions) (e.g. filesystem read). Built-in modules run with the same permissions as the pack. diff --git a/src/content/docs/reference/identifiers.md b/src/content/docs/reference/identifiers.md new file mode 100644 index 0000000..ee851cb --- /dev/null +++ b/src/content/docs/reference/identifiers.md @@ -0,0 +1,84 @@ +--- +title: Identifiers +description: "What counts as a valid identifier: syntax rule, allowed characters, and reserved keywords." +--- + +Identifiers are names used for namespaces, policies, rules, facts, let bindings, shape and field names, aliases, and for map dot access keys. The lexer recognizes a **valid identifier** by a single rule. + +## Syntax + +An identifier is a token that: + +1. **Starts with** a letter (`a`–`z`, `A`–`Z`) or an underscore (`_`). +2. **Continues with** zero or more of: letters (`a`–`z`, `A`–`Z`), digits (`0`–`9`), or underscores (`_`). + +So: only ASCII letters, digits, and underscores. No spaces, hyphens, or other characters. The first character must not be a digit. + +## Examples + +| Valid | Invalid (reason) | +| :------ | :----------------------- | +| `x` | `2x` (starts with digit) | +| `name` | `my-name` (hyphen) | +| `_priv` | `my name` (space) | +| `a1` | `a.b` (dot) | +| `com` | | + +## Reserved keywords + +Language keywords are reserved and cannot be used as identifiers. Using them as names (for rules, facts, shapes, etc.) will result in parse errors. + +### Core keywords + +| Keyword | Notes | +| :------ | :---- | +| `namespace` | Namespace declaration. | +| `policy` | Policy declaration. | +| `shape` | Shape declaration. | +| `fact` | Fact declaration. | +| `rule` | Rule declaration. | +| `export` | Exporting decisions or shapes. | +| `use` | Importing TypeScript modules. | +| `let` | Intermediate values. | +| `when` | Rule guard. | +| `default` | Default value for rules. | +| `yield` | Value returned from a rule body. | + +### Literals and logical operators + +| Keyword | Notes | +| :------ | :---- | +| `true` | Boolean literal. | +| `false` | Boolean literal. | +| `unknown` | Trinary literal. | +| `and` | Logical AND. | +| `or` | Logical OR. | +| `xor` | Logical XOR. | +| `not` | Logical NOT. | +| `in` | Membership operator. | +| `is` / `is not` | Type and existence checks. | + +### Built-in types + +| Keyword | Notes | +| :------ | :---- | +| `string` | String type. | +| `number` | Numeric type. | +| `bool` | Boolean type. | +| `trinary` | Three-valued logic type. | +| `document` | Unstructured JSON-like value. | +| `list` | List collection type. | +| `map` | Map collection type. | + +Do not use any of these as identifiers. For example, avoid naming a rule `let` or a fact `default`. Prefer descriptive names such as `userRule`, `defaultDecision`, or `currentUser` instead. + +## Where identifiers are used + +- **Namespace FQN segments:** Each part of a fully qualified name (e.g. `com` / `example` / `auth`) must be a valid identifier. +- **Policy, rule, shape, fact, let names:** The name after the keyword must be a valid identifier (and not a keyword). +- **Shape field names, aliases (e.g. `as alias`), map dot access:** Must be valid identifiers when used in source. Map keys at runtime are strings; dot access in source (e.g. `map.key`) requires `key` to be a valid identifier. + +## Good to Know + +- Identifiers are ASCII-only: letters, digits, underscore. No Unicode letters in identifiers. +- If the lexer sees a keyword where an identifier is expected (e.g. a policy named `let`), it reports a parse error. Choose names that are not reserved. diff --git a/src/content/docs/reference/index.md b/src/content/docs/reference/index.md index 147bee7..c72e595 100644 --- a/src/content/docs/reference/index.md +++ b/src/content/docs/reference/index.md @@ -1,11 +1,11 @@ --- -title: "Policy Language Reference" -description: "Complete reference for the Sentrie policy language syntax and features." +title: Policy Language Reference +description: "Reference for Sentrie language syntax, types, operators, and constructs." --- -This is the complete reference for the Sentrie policy language. It covers all language features, syntax, and semantics. +This section is a reference for the Sentrie policy language: syntax, types, operators, and constructs. Pages include syntax variants, options and arguments in tables, where each feature applies, edge cases, and examples. For conceptual overviews and “how it works,” see [Language Concepts](/language-concepts/type-system-shapes). -## Table of Contents +## Program structure - [Program Structure](#program-structure) - [Namespaces](#namespaces) @@ -54,46 +54,52 @@ Namespaces organize your policies and shapes hierarchically and prevent naming c ```text namespace FQN +policy IDENT { fact ... let ... use ... rule ... export decision of IDENT } +shape IDENT { ... } | shape IDENT baseType @constraint +export shape IDENT ``` -Where `FQN` (Fully Qualified Name) is a slash-separated identifier: +## Reference Pages -```text -namespace com/example/auth -namespace com/example/billing/v2 -namespace mycompany/policies/security -``` +### Core -### Namespace statements +- **[Namespaces](/reference/namespaces):** One namespace per file, FQN syntax, placement, visibility of shapes and policies, cross-namespace reference. +- **[Policies](/reference/policies):** Policy block, statement order (facts, let, use, rules, export), evaluation. +- **[Rules](/reference/rules):** Rule syntax, when/default/body, outcome type, truthiness, cross-references. +- **[Facts](/reference/facts):** Required/optional facts, type, alias, default, binding at evaluation, import binding. +- **[Intermediate values (let)](/reference/let):** let syntax, scoping, immutability, type validation. +- **[Identifiers](/reference/identifiers):** What counts as a valid identifier (characters, reserved keywords). -A namespace can contain: +### Types -- **policies**: `policy IDENT { ... }` -- **shapes**: `shape IDENT { ... }` -- **shape exports**: `export shape IDENT` +- **[Types and values](/reference/types-and-values):** Primitives, collections, indexing, cast, validation. +- **[Constraints](/reference/constraints):** Constraint syntax, full tables per type (number, string, list, trinary), when checked, collection and cast behavior. +- **[Trinary](/reference/trinary):** true/false/unknown, truthiness, Kleene AND/OR/NOT, use in when and conditionals. +- **[Shapes](/reference/shapes):** Data models, field modifiers (required/optional, nullable), composition, type aliases, export. -### Rules +### Operations -- Namespaces must be declared at the top of the file (only comments can be placed before the namespace declaration) -- Only one namespace per file -- Namespace names must be valid identifiers -- Use slash-separated (`/`) hierarchical names for organization -- Multiple root namespaces are allowed in a policy pack -- Namespace forms the visibility boundary for unexported shapes +- **[Arithmetic](/reference/arithmetic-operations):** +, -, *, /, %, unary +/-, types, division by zero. +- **[Boolean](/reference/boolean-operations):** and, or, xor, not, comparison, matches, ternary, Elvis. +- **[Built-in Functions](/reference/built-in-functions):** List helpers (`any`, `all`, `filter`, …), `count`, `merge`, and related builtins. +- **[Functions](/reference/functions):** Function calls, use (import), aliasing, memoization. +- **[Precedence](/reference/precedence):** Operator precedence table, associativity, parentheses. +- **[Membership](/reference/membership-operations):** in, contains (list, map, string). -## Policies +### Other + +- **[Security and permissions](/reference/security-and-permissions):** Pack permissions (fs_read, net, env), defaults, configuration. Policies are containers for rules, facts, and other declarations. See [Policy metadata](/reference/policy-metadata/) for `title`, `description`, `version`, and `tag`, and for the required **metadata → facts → uses → body** ordering. -### Syntax +### TypeScript -```text -policy IDENT { - policyStatement* -} -``` +- **[Built-in TypeScript modules](/reference/typescript_modules/):** Overview and per-module reference. + +### Composition and extensibility -### Policy Statements +- **[Policy composition](/language-concepts/policy-composition):** Export/import of rules and facts. +- **[Writing custom TypeScript modules](/extensibility/writing-custom-typescript-modules):** Custom modules and use. A policy can contain (in grouped order; comments anywhere): diff --git a/src/content/docs/reference/let.md b/src/content/docs/reference/let.md index b30de2f..92c600d 100644 --- a/src/content/docs/reference/let.md +++ b/src/content/docs/reference/let.md @@ -1,87 +1,76 @@ --- -title: Intermediate Values -description: Let declarations are containers for intermediate values within blocks. They are scoped to their immediate block and cannot be exported. +title: Intermediate Values (let) +description: "let declaration syntax, scoping, immutability, and type validation." --- -`let` declarations allow you to create intermediate values within a policy or rule block. They are useful for breaking down complex expressions into more readable and maintainable code. +`let` binds a name to an expression inside a block. It is used for intermediate values in a [policy](/reference/policies) or inside a [rule](/reference/rules) body. The binding is scoped to the block, is immutable (no reassignment), and cannot be exported. Only [rules](/reference/rules) can be exported. If a type annotation is present, the value is validated at runtime (including [constraints](/reference/constraints)); validation failure aborts evaluation. -:::note[Important] +## Syntax -`let` declarations are: +```text +let name = expr +let name : type = expr +``` -- **Scoped to their immediate block** (`{}`) -- **Cannot be exported** - only rules can be exported -- **Immutable** - once declared, their value cannot be changed -- **Used for intermediate calculations** within a policy or rule block +- **name:** Identifier. Must be unique within the same block (inner blocks can shadow outer names). +- **type:** Optional. Any type reference (primitive, collection, or [shape](/reference/shapes)), optionally with [constraints](/reference/constraints). When present, the value of `expr` is validated against this type at runtime. +- **expr:** Any expression. Evaluated once; the result is bound to `name`. -::: +## Configuration & Arguments -## Let Declaration Syntax +| Part | Type | Required | Description | +| :--- | :--- | :------- | :---------- | +| `name` | identifier | Yes | Variable name. Visible in the rest of the block (and inner blocks unless shadowed). | +| `type` | type ref | No | If present, the assigned value is validated against this type and any constraints. Failure aborts evaluation. | +| `expr` | expression | Yes | Initial value. Evaluated once at the point of the `let`. | -### Basic Syntax +**Returns:** N/A (binding). The name evaluates to the bound value wherever it is used in scope. -```sentrie -let = +## Scoping --- With optional type annotation -let : = -``` +- **Policy-level let:** Declared in the policy block (after [facts](/reference/facts), alongside [use](/reference/functions)). Visible to all [rules](/reference/rules) in that policy. Not visible in other policies. +- **Rule-level let:** Declared inside a rule body (before the `yield`). Visible only within that rule body. Policy-level let is also visible inside the rule unless shadowed. +- **Shadowing:** A `let` in an inner block (e.g. inside a rule) can reuse the same name as an outer binding; the inner name shadows the outer one in the inner scope. -### Simple Examples +## Immutability and export -```sentrie --- Simple calculations -let isAdmin = user.role is "admin" -let totalPrice = item.price * quantity +- **Immutability:** There is no syntax to reassign a `let` binding. The name always refers to the value computed at the `let` site. +- **Export:** Only rules can be exported. `let` bindings cannot be exported; they are for intermediate computation only. --- With type annotations -let count: number = 10 -let name: string = "example" -let isActive: bool = true +## Examples in Action --- With type annotations and constraints -let count: number @min(0) @max(100) = 50 -let name: string @minlength(3) @maxlength(20) = "example" +### Untyped and typed let --- can be shapes as well -let user: User = { name: "John Doe", age: 30 } +```sentrie +let adminRoles = ["admin", "super_admin"] +let totalPrice = item.price * quantity +let count: number = 10 ``` -## Block Scoping +Without a type, the value is not validated against a type. With a type, the value must conform (and satisfy constraints if any). -`let` declarations are scoped to their immediate block (`{}`). This means: - -- A `let` declaration inside a block is only accessible within that block -- A `let` declaration at the policy level is accessible throughout the policy -- A `let` declaration inside a rule's block is only accessible within that rule - -### Policy-Level `let` Declarations +### Typed let with constraints ```sentrie -namespace com/example/auth - -policy userAccess { - fact user: User as currentUser - - -- Policy-level let declaration - accessible throughout the policy - let adminRoles = ["admin", "super_admin"] +let count: number @min(0) @max(100) = 50 +``` - rule canRead = default false { - -- Can access adminRoles here - yield user.role in adminRoles - } +If `50` were outside `[0, 100]`, evaluation would abort. - rule canWrite = default false { - -- Can also access adminRoles here - yield user.role in adminRoles and user.verified - } +### Policy-level let used in rules - export decision of canRead - export decision of canWrite +```sentrie +policy P { + fact user: User + let roles = ["admin", "editor"] + rule allow = default false { yield user.role in roles } + export decision of allow } ``` -### Block-Level `let` Declarations +`roles` is visible in the rule body. + +### Let inside a rule body ```sentrie namespace com/example/billing @@ -413,97 +402,25 @@ let y = auth.verifySignature(user.id, resource.id) rule calculatePrice = default 0 { let basePrice = product.price let discount = user.isPremium ? 0.1 : 0.05 - let tax = basePrice * 0.08 - let finalPrice = basePrice * (1 - discount) + tax - - yield finalPrice -} - --- Avoid: One-line complex expression -rule calculatePrice = default 0 { - yield product.price * (1 - (user.isPremium ? 0.1 : 0.05)) + product.price * 0.08 + yield basePrice * (1 - discount) } ``` -### Use Type Annotations for Clarity +`base` and `discount` are visible only until the `yield`. ```sentrie -- Good: Explicit types make code clearer let count: number = 10 let items: list[string] = ["item1", "item2"] -let config: dict[string] = {"key": "value"} - --- Acceptable: Type inference works, but explicit types are clearer -let count = 10 -let items = ["item1", "item2"] +let config: map[string]any = {"key": "value"} ``` -### Keep Let Declarations Close to Usage - -```sentrie --- Good: Let declarations close to where they're used -rule calculateTotal = default 0 { - let subtotal = price * quantity - let tax = subtotal * 0.08 - let total = subtotal + tax - - yield total -} - --- Avoid: Policy-level lets that are only used in one rule -policy example { - let subtotal = price * quantity -- Only used in one rule below - - rule calculateTotal = default 0 { - let tax = subtotal * 0.08 - let total = subtotal + tax - yield total - } -} -``` - -## Common Patterns - -### Conditional Values - -```sentrie -rule processUser = default false { - let userRole = user.role is defined ? user.role : "guest" - let accessLevel = userRole == "admin" ? "full" : "limited" - - yield accessLevel == "full" -} -``` - -### Intermediate Calculations - -```sentrie -rule calculateDiscount = default 0 { - let basePrice = product.price - let quantityDiscount = quantity > 10 ? 0.1 : 0.0 - let memberDiscount = user.isMember ? 0.15 : 0.0 - let totalDiscount = quantityDiscount + memberDiscount - let finalPrice = basePrice * (1 - totalDiscount) - - yield finalPrice -} -``` - -### Data Transformation - -```sentrie -rule transformData = default false { - let normalizedName = user.name.lower() - let sanitizedEmail = user.email.trim() - let formattedRole = user.role.upper() - - yield normalizedName != "" and sanitizedEmail != "" -} -``` +## Good to Know -## See Also +Before you implement this, keep a few boundaries in mind: -- [Policies](/reference/policies) - Learn about policies and their structure -- [Rules](/reference/rules) - Learn about rules and how they work -- [Facts](/reference/facts) - Learn about facts and input data declarations -- [Expressions](/reference/index#expressions) - Learn about expressions and operators +- **Scope:** Let is scoped to the immediate block. Policy-level let is visible to all rules in the policy. Rule-level let is visible only in that rule body. Same name in an inner block shadows the outer binding. +- **Immutability:** Let bindings cannot be reassigned. The name always refers to the initial value. +- **Export:** Only rules can be exported. Let cannot be exported. +- **Type annotation:** Optional. If present, the value is validated at runtime against the type and any constraints; constraint or type failure aborts evaluation. +- **Shadowing:** Inner blocks can declare the same name as an outer let; the inner name shadows the outer in the inner scope. diff --git a/src/content/docs/reference/membership-operations.md b/src/content/docs/reference/membership-operations.md new file mode 100644 index 0000000..74445a0 --- /dev/null +++ b/src/content/docs/reference/membership-operations.md @@ -0,0 +1,74 @@ +--- +title: Membership Operations +description: "Containment operators (in, contains) for list, map, and string; semantics and edge cases." +--- + +The `in` and `contains` operators test containment: whether a value appears in a list, a key exists in a map, a substring appears in a string, or (for maps) one map is a “subset” of another. Both return a boolean. `needle in haystack` and `haystack contains needle` are equivalent; only the argument order differs. + +## Syntax + +```text +needle in haystack +haystack contains needle +``` + +- **in:** Left operand = needle (the value to find), right operand = haystack (the container). +- **contains:** Left operand = haystack, right operand = needle. Same result as `in` with arguments swapped. + +## Configuration & Arguments + +| Operator | Left | Right | Result | +| :------- | :--- | :---- | :----- | +| `in` | needle | haystack | true if haystack contains needle; otherwise false. | +| `contains` | haystack | needle | Same as above; argument order reversed. | + +**Returns:** bool. Not trinary. Type mismatch or unsupported combination yields false (or key-mismatch for map-subset case). + +## Semantics by type + +### String + +- **Haystack:** string. **Needle:** string. +- **Result:** true if needle is a **non-empty** substring of haystack; false if needle is empty, or not a substring, or types are wrong. + +### List + +- **Haystack:** list. **Needle:** any value. +- **Result:** true if any element of the list is equal to needle (by `==`). Element-wise equality; no deep structural comparison beyond the language’s equality. Empty list → false. + +### Map — key presence + +- **Haystack:** map. **Needle:** string. +- **Result:** true if the map has a key equal to needle; false otherwise. Only key presence is checked; the value is irrelevant. + +### Map — subset + +- **Haystack:** map. **Needle:** map. +- **Result:** true if every key in needle exists in haystack and the corresponding values are equal (by `==`). Extra keys in haystack are allowed. If any key in needle is missing from haystack or any value differs, result is false. The value comparison is **recursive**: when values are themselves maps or lists, they are compared recursively (nested maps must match key-for-key; nested lists element-for-element). + +## Examples in Action + +### Substring and list membership + +```sentrie +let in_str: bool = "foo" in "xfoobar" +let has_role: bool = "admin" in user.roles +let has_guest: bool = roles contains "guest" +``` + +### Map key presence and subset + +```sentrie +let key_exists: bool = "plan" in subscription +let subset: bool = {"a": 1, "b": 2} contains subscription +``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- **Strings:** Needle must be non-empty for a true result; empty needle → false. Empty string haystack → false (unless needle is also empty; then typically false by “non-empty substring” rule). +- **Lists:** Element-wise equality; no deep comparison beyond the language’s `==`. Empty list → false. +- **Maps (key):** Needle string checks key presence only. Map keys are strings. +- **Maps (subset):** Needle map: every key in needle must exist in haystack with the same value; extra keys in haystack are allowed. Value equality is recursive (nested maps/lists are compared recursively). Type mismatch (e.g. haystack not a map, or needle not string/map) → false. +- **Regex:** For pattern matching on strings, use [matches](/reference/boolean-operations) in Boolean Operations. diff --git a/src/content/docs/reference/namespaces.md b/src/content/docs/reference/namespaces.md index 7745815..0dee126 100644 --- a/src/content/docs/reference/namespaces.md +++ b/src/content/docs/reference/namespaces.md @@ -1,68 +1,80 @@ --- title: Namespaces -description: Namespaces are a way to organize your policies and shapes. +description: "Namespace syntax, placement rules, visibility of shapes and policies, and cross-file behavior." --- -Namespaces are a way to organize your policies and shapes. These MUST be declared at the top of a `*.sentrie` file. +Namespaces group [policies](/reference/policies) and [shapes](/reference/shapes) and define visibility boundaries. Each `*.sentrie` file has exactly one namespace declaration. The namespace must be the first statement in the file (only comments may appear before it). Unexported shapes are visible only within the same namespace; exported shapes can be referenced from other namespaces. -Namespaces form a natural hierarchy using the `/` delimiter. +## Syntax + +```text +namespace fully/qualified/name +``` -A `namespace` is a container for related: +The name is a **fully qualified name (FQN)**: one or more [identifiers](/reference/identifiers) separated by forward slashes (e.g. `com/example/auth`, `mycompany/policies/security`, `app/v1`). No leading or trailing slash. Each segment must be a valid identifier. -- policies -- shapes -- child namespaces +## Configuration & Arguments -## Syntax +| Element | Type | Required | Description | +| :------ | :--- | :------- | :---------- | +| FQN | identifier path | Yes | Slash-separated identifiers. Forms the namespace for every declaration (policies, shapes) in this file. | + +**Returns:** N/A (declaration). The namespace does not produce a value; it attaches a scope to the declarations that follow. + +## Placement and ordering rules + +- **First statement:** The namespace declaration must be the first statement in the file. Only comments (line or block) may appear before it. +- **One per file:** Exactly one namespace per `*.sentrie` file. Multiple files in a pack may declare different namespaces; the pack can therefore contain multiple root namespaces. +- **No nesting syntax:** The language does not have separate “nested” namespace blocks. Hierarchy is by naming convention: e.g. `a/b/c` is a single FQN; there is no separate declaration for `a` or `a/b`. + +## Visibility and cross-namespace reference + +- **Policies and shapes** declared in a file belong to that file’s namespace. They are visible by simple name within the same namespace (same file or other files in the same pack that share the same namespace, if the tooling supports multiple files per namespace). +- **Exported shapes:** A shape can be exported with `export shape Name`. Exported shapes are visible to other namespaces for use as types (e.g. fact types, [composition](/reference/shapes) base). See [Policy composition](/language-concepts/policy-composition) and [Shapes](/reference/shapes) for how other namespaces refer to them. +- **Exported rules:** Rules are exported via `export decision of RuleName` inside a policy. Other namespaces can import and call those decisions; the namespace and policy identify the source. + +## Examples in Action + +### Single-segment and multi-segment namespaces ```text -// policy.sentrie --- This is a namespace declaration -namespace fully/qualified/namespace/name +namespace auth +namespace com/example/auth +namespace mycompany/policies/billing/v2 ``` -Where `FQN` (Fully Qualified Name) is a slash-separated identifier: +### Comments before namespace (allowed) ```text +// This file defines the auth policies. namespace com/example/auth -namespace com/example/billing -namespace com/company/finance/reports -namespace mycompany/policies/security -namespace mycompany/policies/privacy/gdpr + +policy allow { ... } ``` -In the above example, the namespace nesting looks like this: +### Multiple namespaces in a pack (different files) + +File `auth.sentrie`: +```text +namespace com/example/auth +policy P { ... } ``` -com -|-- example -| |-- auth -| |-- billing -|-- company -| |-- finance -| |-- reports - -mycompany -|-- policies -| |-- security -| | |-- ... -| |-- privacy -| | |-- gdpr -| | |-- ... + +File `billing.sentrie`: + +```text +namespace com/example/billing +policy Q { ... } ``` -## Rules +The pack contains two namespaces. Import and resolution rules determine how one namespace references policies or exported shapes in another. -- Namespaces must be declared at the top of the file (only comments can be placed before the namespace declaration) -- Only one namespace per file -- Namespace names must be valid identifiers -- Use slash-separated (`/`) hierarchical names for organization -- Multiple root namespaces are allowed in a policy pack -- Namespace forms the visibility boundary for unexported shapes +## Good to Know -## Best Practices +Before you implement this, keep a few boundaries in mind: -- Use descriptive namespace names that reflect their purpose -- Group related policies and shapes in namespaces - refer to [Policies](/reference/policies/) and [Shapes](/reference/shapes/) -- Keep individual policies and shapes within a namespace focused on specific domains or features -- Consider using consistent naming patterns across your team +- **Constraint:** Only comments may appear before the namespace declaration. One namespace per file. Multiple root namespaces are allowed across the pack (in different files). +- **Naming:** Each segment of the FQN must be a [valid identifier](/reference/identifiers). No leading or trailing slash. Child “namespaces” (e.g. `a` vs `a/b`) are not separate language constructs; hierarchy is by convention in the FQN string. +- **Resolution:** When another namespace references an exported shape or imports a decision, the full namespace path (and policy name, for rules) is used by the runtime/tooling to resolve the target. See [Policy composition](/language-concepts/policy-composition) for import syntax and behavior. +- **Files and namespaces:** The relationship between physical files and namespace FQNs is tooling-dependent; typically one file per namespace, but the reference does not forbid multiple files sharing the same namespace if the tooling supports it. diff --git a/src/content/docs/reference/policies.md b/src/content/docs/reference/policies.md index 97bd335..714b97f 100644 --- a/src/content/docs/reference/policies.md +++ b/src/content/docs/reference/policies.md @@ -1,11 +1,11 @@ --- title: Policies -description: Policies are the core organizational unit in Sentrie, containing rules, facts, and business logic. +description: "Policy syntax, statement order (facts, let, use, rules, export), and evaluation behavior." --- -Policies are the fundamental building blocks of Sentrie policy packs. They encapsulate business rules, define input data structures, and export decision outcomes that can be consumed by applications or other policies. +A policy is a named block inside a [namespace](/reference/namespaces) that groups [facts](/reference/facts), optional [let](/reference/let) bindings, optional [use](/reference/functions) imports, [rules](/reference/rules), and one or more `export decision of` declarations. It defines the inputs and decision logic that the CLI or HTTP API can execute. To be runnable or importable, a policy must have at least one rule and at least one exported decision. -## Policy Structure +## Syntax ### Basic Requirements @@ -49,23 +49,34 @@ policy policyName { } ``` -### Complete Example +- **IDENT:** Policy name (identifier). Must be unique within the namespace (or follow tooling rules for overloads, if any). +- **Body:** Statements in a fixed order (see Configuration & Arguments). Facts first (if any), then let, use, rules, and finally export declarations. -```sentrie -// user-access.sentrie -namespace com/example/auth +## Configuration & Arguments -shape User { - id!: string - role!: string - permissions!: list[string] -} +| Statement | Required | Order | Description | +| :-------- | :------- | :---- | :---------- | +| `fact` | No | First | Declare input facts. All fact declarations must appear before any `let`, `use`, or `rule`. Only facts (and comments) may precede facts. | +| `let` | No | After facts | Intermediate values. Scoped to the policy block; visible to all rules in the policy. | +| `use` | No | After let | Import TypeScript modules (e.g. `use { sha256 } from @sentrie/hash`). See [Functions](/reference/functions). | +| `rule` | Yes (≥1) | After use | Decision logic. At least one rule must be declared. | +| `export decision of` | Yes (≥1) | After rules | Expose a rule for execution (CLI/API) or for [import](/language-concepts/policy-composition) from other policies. The identifier after `of` must be the name of a rule in the same policy. | -shape Resource { - id!: string - type!: string - owner!: string -} +**Returns:** N/A (container). Evaluation returns the result of the invoked rule (the one targeted by the CLI, API, or import). The policy itself does not “return” a value; the exported rule does. + +## Statement order and constraints + +- **Facts first:** If the policy has any `fact` declarations, they must come first. Only comments may appear before the first fact. No `let`, `use`, or `rule` may appear before a fact when facts exist. +- **Let and use:** After all facts, any `let` and `use` statements. Order between multiple `let` or multiple `use` is significant only where one binding references another (e.g. a later `let` may refer to an earlier `let` or to a fact). +- **Rules:** All rules follow. Rules can reference facts (by name or alias), policy-level `let` bindings, and other rules in the same policy by name. They cannot reference rules in other policies without import. +- **Export:** Each `export decision of RuleName` exposes that rule. The same rule may be exported once. The CLI and API select which exported rule to run (e.g. by name). For `import decision of` from another policy, the binding (e.g. `with` facts) uses the rule name as the target. + +## Examples in Action + +### Minimal policy (one fact, one rule, one export) + +```sentrie +namespace com/example/auth policy userAccess { title "User access control" @@ -80,15 +91,15 @@ policy userAccess { use { verifySignature, isBusinessHours } from "./auth-utils.ts" as auth - let isResourceOwner = user.id == resource.owner - let hasValidSignature = auth.verifySignature(user.id, resource.id) + let isResourceOwner = currentUser.id == currentResource.owner + let hasValidSignature = auth.verifySignature(currentUser.id, currentResource.id) let isWithinBusinessHours = auth.isBusinessHours() - rule canRead = default false when (resource.type == "document") { + rule canRead = default false when (currentResource.type == "document") { yield isResourceOwner and hasValidSignature } - rule canWrite = default false when (resource.type == "document") { + rule canWrite = default false when (currentResource.type == "document") { yield isResourceOwner and hasValidSignature and isWithinBusinessHours } @@ -98,396 +109,43 @@ policy userAccess { } ``` -## Policy Components - -### Use Statements - -```sentrie --- Using TypeScript functions -use { function1, function2 } from "./utils.ts" as utils -use { validateEmail } from "./validation.ts" as validators -``` - -### Fact Declarations - -```sentrie --- Required facts (default behavior, must be provided) -fact user: User as currentUser -fact resource: Resource as currentResource - --- Optional facts (marked with ?, can have defaults) -fact context?: Context as ctx default { "key": "value" } -fact config?: Config as settings default { "environment": "production" } -``` - -:::note[Important] - -- Facts are **required by default** - must be provided during execution -- Use `?` to mark facts as **optional** - can be omitted -- Only **optional facts** (`?`) can have default values -- Facts are **always non-nullable** - null values are not allowed - ::: - -:::note -Read more on facts [here](/reference/facts). -::: - -### Variable Declarations - -```sentrie --- Simple calculations -let isAdmin = user.role == "admin" -let totalPrice = item.price * quantity - --- Complex logic -let canAccess = user.active and - (user.role == "admin" or user.permissions contains "read") -``` - -:::note[Important] - -- `let` declarations are **scoped to their immediate block** (`{}`) -- `let` declarations **cannot be exported** - only rules can be exported -- `let` declarations are **immutable** - once declared, their value cannot be changed -- `let` declarations are used for **intermediate calculations** within a policy or rule block - ::: - -:::note -Read more on let declarations [here](/reference/let). -::: - -### Rule Declarations - -```sentrie --- Basic rule -rule canRead = default false { - yield user.role == "admin" -} - --- Conditional rule -rule canWrite = default false when (user.active) { - yield user.role == "admin" and user.verified -} - --- Composite rule -rule canAccess = canRead or canWrite -``` - -:::note -Read more on rules [here](/reference/rules). -::: - -### Export Rule outcomes - -```sentrie --- Export rule outcome -export decision of canAccess - --- Export multiple outcomes -export decision of canRead -export decision of canWrite - --- Export with attachments -export decision of canAccess - attach reason as "Access granted" - attach level as user.role -export decision of canRead - attach permissions as user.permissions - attach timestamp as currentTime() -``` - -:::note -For a complete guide on exporting and importing rules, see [Exporting and Importing Rules](/reference/exporting-and-importing-rules). -::: - -### Import Rule outcomes of other policies - -```sentrie --- Import rule from another policy -rule externalRule = import decision of ruleName - from com/example/other/policy - with factName as expr - --- Example -rule userPermission = import decision of canAccess - from com/example/auth/userAccess - with user as currentUser - --- Accessing attachments from imported rules -rule authResult = import decision of canAccess - from com/example/auth/userAccess - with user as currentUser -let accessReason = authResult.reason -- Access the 'reason' attachment -let accessLevel = authResult.level -- Access the 'level' attachment -let userPermissions = authResult.permissions -- Access the 'permissions' attachment -``` - -:::note -For detailed information on importing rules, fact injection, and accessing attachments, see [Exporting and Importing Rules](/reference/exporting-and-importing-rules). -::: - -## Rule Attachments - -Rule attachments allow you to include additional metadata with exported rule outcomes. This metadata can be accessed when the rule is imported by other policies or executed via the HTTP API. - -### Exporting with Decision Attachments - -```sentrie --- Basic attachment syntax -export decision of ruleName (attach name as expression)* - --- Examples -export decision of canAccess - attach reason as "Access granted" - -export decision of canRead - attach permissions as user.permissions - attach timestamp as currentTime() - -export decision of canWrite - attach level as user.role - attach department as user.department -``` - -### Importing and Accessing Decision Attachments - -```sentrie --- Import rule with attachments -rule importedRule = import decision of ruleName - from com/example/policy - with factName as expr - --- Access attachments using field accessors -let attachmentValue = importedRule.attachmentName - --- Example -rule authResult = import decision of canAccess - from com/example/auth/userAccess - with user as currentUser -let accessReason = authResult.reason -- Access the 'reason' attachment -let accessLevel = authResult.level -- Access the 'level' attachment -let userPermissions = authResult.permissions -- Access the 'permissions' attachment -``` - -### Practical Use Cases - -```sentrie --- Export with debugging information -export decision of canAccess attach debugInfo as "User verified" attach timestamp as currentTime() - --- Export with business context -export decision of canApprove attach approverLevel as user.role attach approvalLimit as user.maxAmount - --- Export with audit trail -export decision of canDelete attach auditReason as "Data retention policy" attach retentionDate as item.createdDate -``` - -## Best Practices for Attachments - -### Use Descriptive Names - -```sentrie --- Good: Clear, descriptive attachment names -export decision of canAccess - attach accessReason as "User has admin role" - attach accessLevel as user.role - --- Avoid: Generic or unclear names -export decision of canAccess - attach info as "ok" - attach data as user.role -``` - -### Keep Attachments Relevant - -```sentrie --- Good: Only include necessary metadata -export decision of canApprove - attach approverLevel as user.role - attach approvalLimit as user.maxAmount - --- Avoid: Including unnecessary or sensitive data -export decision of canApprove - attach approverLevel as user.role - attach userPassword as user.password -``` - -### Use Consistent Naming Conventions - -```sentrie --- Good: Consistent naming pattern -export decision of canRead - attach readReason as "User has read permission" - attach readLevel as user.role - -export decision of canWrite - attach writeReason as "User has write permission" - attach writeLevel as user.role - --- Good: Use meaningful prefixes -export decision of canAccess - attach accessReason as "Access granted" - attach accessLevel as user.role -``` - -### Handle Missing Attachments Gracefully - -```sentrie --- Check if attachment exists before accessing -rule authResult = import decision of canAccess - from com/example/auth/userAccess - with user as currentUser - -let reason = authResult.reason is defined ? authResult.reason : "No reason provided" -let level = authResult.level is defined ? authResult.level : "unknown" -``` - -### Use Attachments for Debugging +### Multiple facts, optional fact with default, let, and use ```sentrie --- Include debugging information in development -export decision of canAccess - attach debugInfo as "User verified: " + user.id - attach timestamp as currentTime() - --- In production, you might remove or simplify debug attachments -export decision of canAccess - attach timestamp as currentTime() -``` - -### Document Attachment Usage - -```sentrie --- Document what attachments are available policy userAccess { - -- Exports canAccess with attachments: - -- - reason: Human-readable explanation - -- - level: User's access level - -- - timestamp: When decision was made - export decision of canAccess - attach reason as "Access granted" - attach level as user.role - attach timestamp as currentTime() + fact user: User as currentUser + fact context?: Context as ctx default {} + use { sha256 } from @sentrie/hash + let adminRoles = ["admin", "super_admin"] + rule canRead = default false when currentUser.role is defined { + yield currentUser.role in adminRoles + } + rule canWrite = default false when currentUser.role is defined { + yield currentUser.role == "admin" + } + export decision of canRead + export decision of canWrite } ``` -### Avoid Overusing Attachments - -```sentrie --- Good: Focused, essential attachments -export decision of canAccess - attach reason as "Access granted" - attach level as user.role - --- Avoid: Too many attachments that clutter the interface -export decision of canAccess - attach reason as "Access granted" - attach level as user.role - attach timestamp as currentTime() - attach debugInfo as "User verified" - attach sessionId as user.sessionId - attach requestId as request.id -``` +### Rules referencing other rules -### Use Proper Alignment +Rules in the same policy can call each other by name (no import needed): ```sentrie --- Good: Align attachments for readability -export decision of canAccess - attach reason as "Access granted" - attach level as user.role - attach timestamp as currentTime() - --- Good: Align import statements for readability -rule authResult = import decision of canAccess - from com/example/auth/userAccess - with user as currentUser - --- Avoid: Single line when it becomes too long -export decision of canAccess attach reason as "Access granted" attach level as user.role attach timestamp as currentTime() attach debugInfo as "User verified" -``` - -## Policy Interactions - -### Rule Composition - -```sentrie --- Combine multiple rules - -let isBusinessHours = ... - -rule canRead = ... -rule canWrite = ... -rule complexAccess = { yield canRead and canWrite and isBusinessHours } - --- Conditional composition -rule conditionalAccess = { yield user.role == "admin" ? canAccess : canRead } -``` - -### Cross-Policy Dependencies - -```sentrie --- Import from another policy -rule authResult = import decision of authenticate - from com/example/auth/login - with user as currentUser - --- Use imported result -rule canProceed = authResult and user.verified -``` - -:::note -Learn more about cross-policy dependencies, sandboxing, and best practices in [Exporting and Importing Rules](/reference/exporting-and-importing-rules). -::: - -## Best Practices - -### Clear Naming - -```sentrie --- Good: Descriptive policy and rule names -policy userAccessControl { - rule canReadDocument = default false { /* ... */ } - rule canWriteDocument = default false { /* ... */ } -} - --- Avoid: Generic names -policy policy1 { - rule rule1 = default false { /* ... */ } -} -``` - -### Logical Organization - -```sentrie --- Group related rules -policy documentAccess { - rule canRead = default false { /* read logic */ } - rule canWrite = default false { /* write logic */ } - rule canDelete = default false { /* delete logic */ } - - rule canAccess = canRead or canWrite or canDelete +policy P { + fact user: User + rule isAdmin = default false { yield user.role == "admin" } + rule canEdit = default false { yield isAdmin } + export decision of canEdit } ``` -### Error Handling +## Good to Know -```sentrie --- Provide sensible defaults -rule canAccess = default false when (user is defined and resource is defined) { - yield user.active and user.verified -} -``` - -### Documentation +Before you implement this, keep a few boundaries in mind: -```sentrie --- Document complex logic -policy complexBusinessLogic { - -- This rule implements the company's access control policy - -- Users must be active, verified, and have appropriate role - rule canAccess = default false { - yield user.active and user.verified and user.role in ["admin", "manager"] - } -} -``` +- **Constraint:** Facts must be declared before rules; only facts or comments may precede fact declarations. At least one rule must be exported for the policy to be executable or importable. Rules in the same policy may reference each other by name without import. +- **Policy name:** The policy name is an identifier. The same namespace may contain multiple policies. Each has its own facts, let, use, and rules. +- **Export target:** Exported rule names are the targets for the CLI/API and for `import decision of` from other policies. When importing, the binding (e.g. `with` clause) uses the rule name as defined in the target policy (and fact names/aliases as in that policy). +- **Evaluation:** When a decision is requested, the runtime evaluates the chosen rule (with facts bound). Only that rule’s body or default is run; other rules are not evaluated unless the chosen rule references them. diff --git a/src/content/docs/reference/precedence.md b/src/content/docs/reference/precedence.md index 42943aa..e988e83 100644 --- a/src/content/docs/reference/precedence.md +++ b/src/content/docs/reference/precedence.md @@ -1,11 +1,11 @@ --- title: Precedence -description: Operator precedence determines the order of operations in expressions. +description: "Operator precedence (highest to lowest), associativity, and use of parentheses." --- -Operator precedence defines which operations are performed first when multiple operators appear in an expression. Higher precedence operations are evaluated before lower precedence ones. +Operators are evaluated in order of precedence: higher precedence binds first. When two operators have the same precedence, they are usually evaluated left-to-right; the ternary operator `? :` is right-associative. Parentheses `( ... )` override precedence and explicitly group subexpressions. -## Precedence Table (highest to lowest) +## Syntax | Precedence | Operators | Description | | ---------- | ------------------------------------------------- | ------------------------------------------------------------ | @@ -21,34 +21,62 @@ Operator precedence defines which operations are performed first when multiple o | 10 | `? :` | Ternary conditional | | 11 | `|>` | Pipeline operator (lowest precedence) | -## Examples +## Configuration & Arguments (precedence table) -### Arithmetic Precedence +| Level | Precedence | Operators | Description | +| :---- | :--------- | :-------- | :---------- | +| 1 | Highest | `( )`, `[ ]`, `.` | Primary: function call `f(...)`, indexing `e[i]`, member access `e.f`. Literals and identifiers are atoms. | +| 2 | | `not`, `!`, unary `+`, unary `-` | Unary: logical not, unary plus/minus. | +| 3 | | `*`, `/`, `%` | Multiplicative. | +| 4 | | `+`, `-` | Additive (binary). | +| 5 | | `<`, `<=`, `>`, `>=`, `in`, `matches`, `contains` | Comparison and containment. | +| 6 | | `==`, `!=`, `is`, `is not` | Equality. | +| 7 | | `and` | Logical AND. | +| 8 | | `xor` | Logical XOR. | +| 9 | | `or` | Logical OR. | +| 10 | | `? :` | Ternary conditional. **Right-associative.** | +| 11 | Lowest | `|>` | Pipeline: left operand is piped into the next call. **Left-associative.** | + +**Returns:** N/A (ordering rule). The result type is the type of the top-level expression after all operators are applied. + +## Associativity + +- **Left-to-right:** Same-precedence operators (e.g. `+` and `-`, `*` and `/`, `and`, `or`, `xor`) group from the left unless overridden by parentheses. Example: `a - b - c` is `(a - b) - c`. +- **Right-associative:** The ternary `? :` groups from the right. Example: `a ? b : c ? d : e` is `a ? b : (c ? d : e)`. + +## Parentheses + +Parentheses override precedence. Any subexpression can be wrapped in `( ... )` to force that subexpression to be evaluated first. Use parentheses when the intended grouping is not obvious. + +## Examples in Action + +### Precedence in arithmetic ```sentrie -let result: number = 2 + 3 * 4 -- Result: 14 (not 20) -let calculation: number = 10 / 2 + 3 -- Result: 8.0 (not 1.25) +let result: number = 2 + 3 * 4 -- 14 (multiplication before addition) +let valid: bool = 5 + 3 > 7 -- true (arithmetic before comparison) ``` -### Comparison vs Arithmetic +### Logical precedence ```sentrie -let is_valid: bool = 5 + 3 > 7 -- Result: true -let check: bool = 10 * 2 == 20 -- Result: true +let complex: bool = true and false or true -- true (and before or; (true and false) or true) ``` -### Logical Precedence +### Ternary right-associativity ```sentrie -let complex: bool = true and false or true -- Result: true -let mixed: bool = 5 > 3 and 2 < 4 -- Result: true +let value: number = 5 > 3 ? 10 : 20 -- 10 +let nested: string = true ? (false ? "A" : "B") : "C" -- "B" ``` -### Ternary Precedence +Without parentheses, `a ? b : c ? d : e` is parsed as `a ? b : (c ? d : e)`. + +### Using parentheses to clarify ```sentrie -let value: number = 5 > 3 ? 10 : 20 -- Result: 10 -let nested: string = true ? (false ? "A" : "B") : "C" -- Result: "B" +let safe: bool = (user.role == "admin") and (age >= 18) +let sum: number = (a + b) * (c + d) ``` ### Pipeline Precedence and Associativity @@ -68,63 +96,10 @@ let c = cond ? x : y |> len For pipeline syntax, valid targets, and memoization, see [Function chaining](/reference/function-chaining). -## Using Parentheses - -### Override Precedence - -```sentrie --- Without parentheses (follows precedence) -let result1: number = 2 + 3 * 4 -- Result: 14 - --- With parentheses (override precedence) -let result2: number = (2 + 3) * 4 -- Result: 20 -``` - -### Complex Expressions - -```sentrie --- Clear grouping for readability -let access: bool = (user.age >= 18 and user.verified) or - (user.role == "admin" and user.active) - --- Mixed operations -let calculation: number = (10 + 5) * (3 - 1) / 2 -- Result: 15.0 +## Good to Know --- Same precedence is evaluated from left to right -let calculation: number = (10 + 5) * (5 - 2) / 2 -- Result: 22.5 (15 * 3 / 2) +Before you implement this, keep a few boundaries in mind: -``` - -## Best Practices - -### Use Parentheses for Clarity - -```sentrie --- Good: Clear intent -let result: bool = (a and b) or (c and d) - --- Avoid: Relying on precedence knowledge -let result: bool = a and b or c and d -``` - -### Group Related Operations - -```sentrie --- Good: Logical grouping -let price: number = (base_price + tax) * discount_rate - --- Good: Comparison grouping -let valid: bool = (age >= 18) and (income > 50000) -``` - -### Break Complex Expressions - -```sentrie --- Good: Step-by-step calculation -let temp1: number = base_price + tax -let temp2: number = temp1 * discount_rate -let final_price: number = temp2 - shipping - --- Avoid: One complex expression -let final_price: number = (base_price + tax) * discount_rate - shipping -``` +- **Parentheses:** Override precedence and associativity. Use them when in doubt to make intent explicit. +- **Same level:** Left to right except for ternary (`? :`), which is right-associative. +- **Primary:** Function calls, indexing, and dot access have the highest precedence so that `a.f()`, `a[i]`, and `a.b` are grouped as expected. diff --git a/src/content/docs/reference/rules.md b/src/content/docs/reference/rules.md index 5ae8955..dd39df3 100644 --- a/src/content/docs/reference/rules.md +++ b/src/content/docs/reference/rules.md @@ -1,200 +1,86 @@ --- title: Rules -description: Rules are a way to organize your rules and facts. +description: "Rule syntax, when/default/body evaluation, outcome type, and cross-references." --- -Rules form the core principle of policies in Sentrie. A policy is essentially a collection of rules and their outcome decisions that work together to make business decisions. Understanding how rules function is fundamental to building effective Sentrie policies. +A rule defines a single named decision: an optional `when` guard, an optional `default` value when the guard is not truthy, and a body that must contain exactly one `yield`. If the `when` expression is truthy, the body is evaluated and its `yield` value is the rule’s outcome; otherwise the outcome is the `default` (or `unknown` if no default is given). Rules are the only construct that can be exported for execution or import. -## The Foundation of Policies +## Syntax -Since rules are the building blocks that define business logic, every Sentrie policy must contain at least one exported rule. - -A rule must be exported to be executed by the runtime or to be imported and used in other policies. This export mechanism enables modular policy design where complex business logic can be broken down into reusable component rules. - -## Rule Structure and Evaluation - -Rules consist of three essential components: - -- a **body** (required) -- a **default** (optional) -- a **when predicate** (optional) - -The `when` predicate acts as a gatekeeper, determining whether the rule's `body` should be evaluated or if the `default` should be used instead. - -When the `when` condition evaluates to [truthy](/reference/trinary), the rule's body is executed to produce the final decision. If the `when` condition is not [truthy](/reference/trinary), the rule falls back to its `default` value. A rule can have one of the three possible outcomes any rule can have: `TRUE`, `FALSE`, or `UNKNOWN`. - -```typescript -is_truthy(when) ? body : default -``` - -:::note[Remember] -If no `default` is provided and `when` is not [truthy](/reference/trinary), the rule's outcome falls back to `unknown`. -::: - -### Example Rule - -```sentrie -namespace com/example/auth - -policy user { - rule isAdmin = default false when user.role is defined { - yield user.role is "admin" or user.role is "super_admin" - } - export decision of isAdmin attach role as user.role -} +```text +rule IDENT = [ default expr ] [ when expr ] { stmt* yield expr } ``` -:::note[Remember] - -- `when` and `default` are optional -- `when` is `true` by default -- `default` is `unknown` by default -- If `when` is not [truthy](/reference/trinary) and no `default` is provided, the rule's outcome is `unknown` +- **IDENT:** Rule name (identifier). Must be unique within the [policy](/reference/policies). Used when exporting (`export decision of IDENT`) and when other rules in the same policy reference it. +- **default expr:** Optional. Evaluated only when `when` is not truthy; result becomes the rule outcome. If omitted and `when` is not truthy, outcome is `unknown`. +- **when expr:** Optional. Guard; must evaluate to a [trinary](/reference/trinary) or value treated as truthy/falsy. If omitted, treated as truthy (body always runs). If present and not truthy, body is skipped and `default` (or `unknown`) is used. +- **Body:** Block of statements ending with exactly one `yield expr`. When the body runs, it must reach that `yield`; the expression’s value is the rule outcome. -::: +## Configuration & Arguments -## Sharing Rules Across Policies +| Part | Type | Required | Description | +| :--- | :--- | :------- | :---------- | +| `default expr` | expression | No | Fallback outcome when `when` is not truthy. Type can be any value (e.g. bool, number, string). If omitted, non-truthy `when` yields `unknown`. | +| `when expr` | expression (trinary/truthy) | No | Guard. Evaluated first. Only if [truthy](/reference/trinary) (i.e. `true` in trinary semantics) is the body evaluated. Default when omitted: treated as true. | +| body | block | Yes | Statements (e.g. `let`) followed by exactly one `yield expr`. Only the chosen branch (body or default) is evaluated; the other is not. | -Rules can be exported as decisions, making them available for consumption in other policies. This enables powerful policy composition where complex business decisions can be built by combining simpler, focused rules. When exporting a rule as a decision, you can attach named data that callers can access and consume. +**Returns:** When the body runs: the value of `yield expr`. When the guard is not truthy: the value of `default expr`, or `unknown` if there is no default. The rule’s result type is therefore the type of the yielded expression or the default expression (or trinary when outcome is `unknown`). -Exported rules can include named attachments that provide additional context or metadata about the decision. These attachment values must be proactively consumed by the calling policy, ensuring that important information isn't lost in the decision-making process. +## Evaluation semantics -### Example +1. **Evaluate `when`** (if present). If absent, proceed as if truthy. +2. **Truthiness:** Only `true` is truthy. `false` and `unknown` are non-truthy. So `when user.role is defined` is truthy only when the expression evaluates to `true`. +3. **If truthy:** Evaluate the body. The body must contain exactly one `yield expr`; that expression’s value is the rule outcome. Any `let` or other statements in the body are evaluated in order before the `yield`. +4. **If not truthy:** Do not evaluate the body. The outcome is `default expr` if present, otherwise `unknown`. -```sentrie -// auth/auth.sentrie -namespace com/example/auth +## Examples in Action -policy base { - fact user!:User as u -- alias for the user fact - - rule isAdmin = default false when user.role is defined { - yield user.role is "admin" or user.role is "super_admin" - } - export decision of isAdmin attach role as user.role -} -``` +### Rule with default only (no when) ```sentrie -// user/user.sentrie -namespace com/example/user - -policy user_access { - fact user!:User as user - - rule isAdmin = import decision of isAdmin from com/example/auth/base with u as user -} +rule allow = default false { yield true } ``` -## Importing and Sandboxing - -Rules can be imported from other policies, but this process involves careful isolation to maintain policy boundaries. When importing a rule decision, you must inject the necessary facts that the imported rule needs to evaluate. Crucially, imported rules are evaluated in their own sandboxed environment, which means they cannot affect or modify the context of the calling policy. - -This sandboxing only applies to rules imported from other policies. Rules within the same policy can reference each other directly without import restrictions, allowing for efficient internal policy organization while maintaining security boundaries between different policy modules. - -## Practical Examples - -Let's explore how these concepts work in practice through various rule patterns and scenarios. +Guard is effectively true; body runs and yields `true`. -### Basic Rule Patterns - -The simplest rule pattern involves explicit boolean outcomes. Here's a rule that always allows login: +### Rule with when and default ```sentrie -policy auth { - rule allow_login = { - yield true - } - export decision of allow_login +rule isAdmin = default false when user.role is defined { + yield user.role == "admin" } ``` -More sophisticated rules use the `when` predicate to conditionally evaluate expensive operations. Consider a feature flag rule that defaults to false for non-beta users, avoiding unnecessary computation: +If `user.role` is not defined (or expression is false/unknown), outcome is `false`. Otherwise outcome is the result of `user.role == "admin"`. -```sentrie -policy feature { - rule enable_feature = default false when user.is_beta == false { - yield someExpensiveCheck() - } - export decision of enable_feature -} -``` - -When a rule's `when` condition is false and no `default` is provided, the outcome becomes UNKNOWN. This pattern is useful for gated functionality: +### Rule yielding a value (not just bool) ```sentrie -policy gated { - rule gated_rule = when system.ready == false { - yield true - } - export decision of gated_rule +rule getPrice = default 0 when product.price is defined { + let base = product.price + let discount = user.isPremium ? 0.1 : 0.05 + yield base * (1 - discount) } ``` -Rules can also leverage truthiness evaluation for cleaner code. This session validation rule returns true if the session ID is non-empty: - -```sentrie -policy auth { - rule has_session = default false when true { - yield session.id // Truthy if non-empty string - } - export decision of has_session -} -``` - -### Policy Composition and Attachments - -Rules become powerful when composed across policies. Here's how to export a decision with additional context through attachments: - -```sentrie -// shapes.sentrie -namespace com/example/auth - -shape Account { - balance!: number -} +Outcome type is number. Default `0` is used when `product.price` is not defined or when the guard is otherwise non-truthy. -shape Invoice { - total!: number -} -``` +### Rule referencing another rule in the same policy ```sentrie -// billing.sentrie -namespace com/example/billing - -policy billing { - fact account!: Account as account - fact invoice!: Invoice as invoice - - let reason = account.balance >= invoice.total ? "insufficient_funds" : "sufficient_funds" - let balance = account.balance - - rule payment_ok = default false { - yield reason - } - - export decision of payment_ok - attach reason as reason - attach balance as balance -} +rule isAdmin = default false when user.role is defined { yield user.role == "admin" } +rule canEdit = default false { yield isAdmin } +export decision of canEdit ``` -When importing this decision, you inject the necessary facts and receive the decision in a sandboxed environment: - -```sentrie -policy shipping { - fact account!: Account as account - fact invoice!: Invoice as invoice +`canEdit`’s body evaluates `isAdmin`, which runs with the same facts. So the outcome of `canEdit` is the outcome of `isAdmin` in this example. - rule payment_reason_consumed = import decision of payment_ok from com/example/billing - with account as account - with invoice as invoice +## Good to Know - export decision of payment_reason_consumed - attach reason as payment_ok.reason -- access the reason attachment - attach balance as payment_ok.balance -- access the balance attachment -} -``` +Before you implement this, keep a few boundaries in mind: -This pattern enables rich policy composition where decisions carry not just boolean outcomes but also contextual information that can inform downstream authorization logic. +- **Evaluation:** Semantics are `is_truthy(when) ? body_result : default`. Truthiness follows [trinary](/reference/trinary) semantics: only `true` is truthy. +- **Body:** Must contain exactly one `yield expr` when the body is evaluated. Only the chosen branch (body or default) is evaluated; the other is not. +- **Rules in same policy:** Can reference other rules by name (e.g. `yield isAdmin`). No import needed. Exported rules can be invoked from the CLI/API or [imported](/language-concepts/policy-composition) from other policies. +- **No default and non-truthy when:** If there is no `default` and `when` is not truthy, the outcome is `unknown`. +- **Recursion:** Rule evaluation is bounded by the language; unbounded recursion is not allowed. diff --git a/src/content/docs/reference/security-and-permissions.md b/src/content/docs/reference/security-and-permissions.md index c7a50e4..0ee3c44 100644 --- a/src/content/docs/reference/security-and-permissions.md +++ b/src/content/docs/reference/security-and-permissions.md @@ -1,31 +1,73 @@ --- -title: Permissions -description: Permissions are the security permissions and capabilities that can be used in a policy pack. +title: Security and Permissions +description: "Policy pack permissions (filesystem read, network, environment variables); configuration and defaults." --- -Permissions are the security permissions and capabilities that can be used in a policy pack. +Permissions define what a policy pack can access at runtime: which filesystem paths can be read, which network hosts can be contacted, and which environment variable names are exposed to the pack. They are configured in the pack configuration file (e.g. `sentrie.pack.toml`) under a `[permissions]` section. Default behavior is restrictive: typically only the pack root is readable; no network access; no environment variables. Explicit entries grant access only to the listed paths, hosts, and variable names. -## Purpose +## Syntax -Permissions are used to define the security permissions and capabilities that can be used in a policy pack. This includes access to the filesystem and network. Permissions are defined in the `sentrie.pack.toml` file in the `permissions` section. +In the pack configuration file (e.g. `sentrie.pack.toml`): -By default, policies and it's modules have access to +```text +[permissions] +fs_read = ["path1", "path2", ...] +net = ["host1", "host2", ...] +env = ["VAR1", "VAR2", ...] +``` -- the filesystem rooted at the policy pack root -- no network access. -- no access to the environment variables. +- **fs_read:** List of paths (or path prefixes) that the pack is allowed to read. Paths can be absolute or relative to the pack root, per tooling. +- **net:** List of hosts (or patterns) allowed for outbound network access. Format is tooling-dependent (e.g. hostname, host:port, or pattern). +- **env:** List of environment variable names that are exposed to the pack. Only these names are visible; other environment variables are not. -## Syntax +## Configuration & Arguments -```text +| Key | Type | Required | Description | +| :--- | :--- | :------- | :---------- | +| `fs_read` | list of strings | No | Paths (or prefixes) the pack can read. Default: typically pack root only (e.g. `["."]` or the pack directory). Paths outside this set cause a runtime error if access is attempted. | +| `net` | list of strings | No | Hosts (or patterns) allowed for network access. Default: none. Attempts to contact hosts not in the list (or not matching a pattern) fail at runtime. | +| `env` | list of strings | No | Environment variable names exposed to the pack. Default: none. Only these names are visible; other env vars are hidden. | + +**Returns:** N/A (configuration). Violations (e.g. reading a file outside `fs_read`, contacting a host not in `net`, or reading an env var not in `env`) cause runtime failure or denial. + +## Default behavior + +- **Filesystem:** If `fs_read` is omitted or empty, the default is typically the pack root only. The pack can read files under that root (e.g. `.sentrie` files, local TypeScript modules). Reading outside the allowed set fails. +- **Network:** If `net` is omitted or empty, no network access is granted. TypeScript modules or built-ins that perform network I/O will fail unless the required host is listed. +- **Environment:** If `env` is omitted or empty, no environment variables are exposed. The pack cannot read `process.env` (or equivalent) except for the names explicitly listed. + +## Examples in Action + +### Minimal (pack root only) + +```toml +[permissions] +fs_read = ["."] +``` + +### Explicit paths and network + +```toml [permissions] -fs_read = ["/etc/passwd"] -net = ["example.com"] -env = ["ORG_DSN", "REDIS_PASSWORD"] +fs_read = [".", "/etc/app/config"] +net = ["api.example.com", "cdn.example.com"] ``` -In the above example, the policy pack has +### Exposing environment variables + +```toml +[permissions] +fs_read = ["."] +env = ["ORG_DSN", "REDIS_PASSWORD", "API_KEY"] +``` + +Only these variable names are visible to the pack; others are not exposed. + +## Good to Know + +Before you implement this, keep a few boundaries in mind: -- read access to the `/etc/passwd` file -- network access to `http://example.com` -- access to the `ORG_DSN` and `REDIS_PASSWORD` environment variables. +- **Default:** Filesystem access is typically limited to the policy pack root; no network; no environment variables. Explicit entries grant access only to the listed paths, hosts, and variable names. +- **Scope:** Permissions apply to the entire pack. All policies and TypeScript modules in the pack run with the same permissions. There is no per-policy or per-module permission granularity in this model. +- **Validation:** Invalid or unsupported paths/hosts/names may be rejected at load time or at first use. Restrict permissions to the minimum required for the pack to function. +- **Security:** Do not grant broader filesystem, network, or env access than necessary. Use explicit allowlists rather than wildcards unless the tooling documents safe patterns. diff --git a/src/content/docs/reference/shapes.md b/src/content/docs/reference/shapes.md index d2480b2..4bd416e 100644 --- a/src/content/docs/reference/shapes.md +++ b/src/content/docs/reference/shapes.md @@ -1,64 +1,162 @@ --- title: Shapes -description: Shapes are a way to define type aliases and data models in sentrie. +description: "Shape syntax (data models, field modifiers, composition, type aliases, export) and validation." --- -Shapes are Sentrie's primary mechanism for defining custom data models and type aliases. They provide a powerful way to create reusable, validated types that can be composed together to build complex data models. +Shapes define structured data (field-based models) or type aliases (a base type plus optional [constraints](/reference/constraints)). They are used as the type of [facts](/reference/facts), [let](/reference/let) bindings, and nested fields. Shape values are validated at runtime when assigned or passed as input. -Shapes help in providing clear contracts for data, thus providing a built in validation layer for data that flows through your policies. +## Syntax -## What are Shapes? +### Data model (complex shape) -Shapes serve two main purposes in Sentrie: +A shape with a body of named, typed fields: -- **Data Models**: Define structured objects with named fields and constraints -- **Type Aliases**: Create custom names for existing types with additional constraints +```text +shape Name { + fieldName!: type + fieldName?: type + fieldName: type + fieldName!?: type +} +``` -## Defining Shapes +Each field has a name, an optional modifier suffix (`!`, `?`, or both in either order), a colon, and a type (any [type or shape](/reference/types-and-values)). -### Type Aliases +### Type alias (simple shape) -Shapes can be used to create type aliases for built-in types with additional constraints: +A shape that is an alias for a single type, optionally with constraints: -```sentrie -shape ID string @uuid() -shape Email string @email() -shape Username string @minlength(3) @maxlength(20) @alphanumeric() -shape UserAge number @min(13) @max(120) +```text +shape Name baseType +shape Name baseType @constraint1 @constraint2 ``` -This creates constrained types that can be used throughout your policies: +Examples: `shape ID string @uuid()`, `shape Score number @min(0) @max(100)`. -```sentrie -let id: ID = "123e4567-e89b-12d3-a456-426614174000" -- Valid -let email: Email = "alice@example.com" -- Valid -let username: Username = "alice123" -- Valid -let age: UserAge = 25 -- Valid -let invalid_username: Username = "ab" -- Validation error: too short +### Composition (extending a base shape) + +A complex shape can extend exactly one other shape with `with BaseName` before the opening `{`. The base shape must be in scope (same namespace or [exported](/reference/namespaces) from another namespace). + +```text +shape Child with Base { + field!: type +} +``` + +The child shape’s effective fields are the union of the base’s fields and the child’s own fields. Duplicate field names between base and child are not allowed. + +### Exporting a shape + +To make a shape visible to other namespaces (for use in [policy composition](/language-concepts/policy-composition) or as a fact type), declare: + +```text +export shape ShapeName ``` -### Simple Data Models +Only shapes declared in the same file (same namespace) can be exported. The shape name is the identifier used in the same file; other namespaces refer to it by the exported name. + +## Configuration & Arguments + +### Field modifiers (complex shapes) + +Every field in a shape body has two logical properties: whether the field is **required** (must be present in the value) and whether it is **nullable** (may be `null` when present). Modifiers are written as suffixes on the field name. + +| Modifier | Required? | Nullable? | Meaning | +| :--------- | :-------- | :-------- | :------ | +| (none) | Yes | Yes | Field must be present; value may be `null`. | +| `!` | Yes | No | Field must be present and must not be `null`. | +| `?` | No | Yes | Field may be omitted; if present, value may be `null`. | +| `!?` or `?!` | No | No | Field may be omitted; if present, value must not be `null`. | + +- **Required:** If the field is missing from the value at runtime, validation fails. +- **Nullable:** If the field is present and its value is `null`, validation fails when the field is non-nullable (`!` or `!?`/`?!`). + +Field type can be any type reference: primitives (`number`, `string`, `trinary`, `bool`, `document`), collections (`list[T]`, `map[T]`, `record[...]`), or another shape. Constraints on the type are validated when the value is assigned or when the value is supplied as a fact. + +### Composition rules + +| Aspect | Rule | +| :----- | :--- | +| Base shape | Exactly one `with BaseName`. The base must be a complex shape (or a type-alias shape that the implementation treats as a structural base where applicable). | +| Visibility | Base must be in the same namespace or exported from another namespace. Cross-namespace base requires the base shape to be declared with `export shape BaseName` in its file. | +| Duplicate fields | A child cannot declare a field with the same name as a field in the base. | +| Circular composition | Not allowed (e.g. A with B, B with A). | + +### Type alias (simple shape) rules + +| Aspect | Rule | +| :----- | :--- | +| Base type | Any single type: primitive, collection, or shape. | +| Constraints | Optional; use the same [constraint](/reference/constraints) syntax as on types (e.g. `@min(0)`, `@uuid()`). | +| Use | The shape name can be used anywhere a type is expected (facts, let, other shape fields, collections). | + +**Returns:** N/A (type definition). Values are validated when assigned to a variable or field of that shape, or when passed as facts. Validation failure aborts evaluation. + +## Where shapes can be used + +- **Facts:** A fact’s type can be a shape (e.g. `fact user: User`). The supplied JSON is validated against the shape’s fields and constraints. +- **Let bindings:** A let can be typed with a shape (e.g. `let u: User = someExpr`). The expression result is validated against the shape. +- **Nested types:** A shape field’s type can be another shape or `list[ShapeName]`, `map[ShapeName]`, etc. +- **Policy composition:** Imported policies can bind facts using shapes defined in another namespace if those shapes are exported. -The most common use of shapes is to define structured data models: +## Examples in Action + +### Data model with all modifier variants ```sentrie shape User { - name: string + name!: string age: number - email: string + email?: string + phone!?: string } ``` -This creates a `User` shape with three fields. You can then use this shape to create and validate user data: +- `name` must be present and non-null. +- `age` must be present but may be null. +- `email` may be omitted; if present, may be null. +- `phone` may be omitted; if present, must not be null. + +### Checking optional fields in rules + +Optional or nullable fields should be checked before use. Use `is defined` to test presence (and often combine with a null check if the field is nullable). ```sentrie -let user: User = { - name: "Alice", - age: 28, - email: "alice@example.com" +shape Item { name!: string; price?: number } + +rule showPrice = default "n/a" when item.price is defined { + yield cast item.price as string } ``` +### Type aliases with constraints + +```sentrie +shape ID string @uuid() +shape Permission string @one_of("read", "write", "delete") +shape Percent number @min(0) @max(100) +``` + +Use them in facts or let: + +```sentrie +fact userId: ID as id +let p: Percent = 50 +``` + +### Composition + +```sentrie +shape Base { id!: string } +shape Extended with Base { role!: string } +``` + +A value of type `Extended` must have both `id` and `role`, and both must be non-null (given `!`). + +### Composition with exported base (cross-namespace) + +Exported shapes from another file can be used as composition bases. See [Exporting Shapes](#exporting-shapes) and [Policy composition](/language-concepts/policy-composition). + #### Field Nullability and Optionality Shapes support different field requirements using special markers: @@ -130,36 +228,7 @@ let phone_message: string = user.phone is defined ? "Phone: " + user.phone : "No ::: -## Shape Composition - -Shapes can be composed from other shapes using the `with` keyword, allowing you to build complex data models. - -### Basic Composition - -```sentrie -shape User { - name: string - age: number -} - -shape AdminUser with User { - permissions: list[string] - department: string -} -``` - -The `AdminUser` shape includes all fields from `User` and adds additional fields: - -```sentrie -let admin: AdminUser = { - name: "John", - age: 30, - permissions: ["read", "write", "delete"], - department: "Engineering" -} -``` - -### Composition Rules +### Composition rules - **Composition**: Composed shapes include all fields from base shapes - **No Circular Dependencies**: Shapes cannot reference themselves directly or indirectly @@ -230,7 +299,7 @@ shape User { name!: string @minlength(2) @maxlength(100) age: number @min(13) @max(120) -- Can be null if unknown contact!: ContactInfo - preferences?: dict[string] -- Optional preferences + preferences?: map[string]any -- Optional preferences } ``` @@ -308,107 +377,29 @@ policy example { To make shapes available to other namespaces, use the `export` keyword: ```sentrie -// auth/auth.sentrie namespace com/example/auth -shape User { - id: string @uuid() - username: string @minlength(3) - email: string @email() -} - -export shape User -``` - -### Importing Shapes - -Other namespaces can import and use exported shapes by using the fully qualified name: - -```sentrie -// billing/billing.sentrie -namespace com/example/billing - -policy process_payment { - -- Import the User shape using the fully qualified name from auth namespace - shape User with com/example/auth/User { - billing_address: string - } - - let user: User = { - id: "123e4567-e89b-12d3-a456-426614174000", - username: "alice", - email: "alice@example.com", - billing_address: "456 Oak Ave" - } -} -``` - -### Policy Local Shapes - -Shapes defined in a policy are only available within that policy and **take precedence** over namespace shapes: - -```sentrie -// billing.sentrie -namespace com/example/billing - -shape User { - id!: string @uuid() - email!: string @email() -} +shape Base { id!: string } +export shape Base -policy process_payment_1 { - -- Policy-local shape (most specific) - shape User { - id!: string @uuid() - billing_address!: string - } - - -- This resolves to the policy-local `User` shape - let user: User = { - id: "123e4567-e89b-12d3-a456-426614174000", - billing_address: "123 Main St" - } - - ... -} - -policy process_payment_2 { - -- This resolves to the namespace `User` shape - let user: User = { - id: "123e4567-e89b-12d3-a456-426614174000", - billing_address: "123 Main St" - } - - ... +policy P { + -- ... } ``` -## Best Practices - -### Naming Conventions - -- Use PascalCase for shape names: `UserProfile`, `PaymentInfo` -- Use descriptive names that clearly indicate the shape's purpose -- Avoid generic names like `Data` or `Info` unless they're truly generic +In another file in a different namespace, a shape can extend `Base` by referencing the exported name (see [Policy composition](/language-concepts/policy-composition) and namespace resolution). The base shape must be exported for cross-namespace composition. -### Organization +### Shape literals and field order -- Group related shapes in the same namespace -- Export only shapes that need to be used by other namespaces -- Use composition to avoid duplicating common fields +When constructing a value (e.g. in a let or as input), field order in the literal does not affect type checking. All required fields must be present and all constraints must hold. -### Best Practices +## Good to Know -- Apply appropriate constraints to shape fields -- Use meaningful constraint messages for better error reporting -- Use alias shapes for better readability +Before you implement this, keep a few boundaries in mind: -```sentrie -shape Category string @one_of("electronics", "clothing", "books", "home") -shape Product { - name!: string @minlength(1) @maxlength(100) @not_empty() - price!: number @positive() @range(0.01, 999999.99) - category!: Category - in_stock: bool -} -``` +- **Validation timing:** Shape (and constraint) validation runs when a value is assigned to a variable or field of that shape, or when facts are bound. Failure aborts evaluation. +- **Optional fields:** Use `is defined` (and null checks if the field is nullable) before using an optional or nullable field in expressions. Accessing a missing optional field yields `unknown` in expressions; the shape validator only checks that required fields are present and non-nullable fields are non-null when present. +- **Composition:** The composed shape effectively has all base fields plus its own. Circular composition (A with B, B with A, or longer cycles) is not allowed. Duplicate field names between base and child are not allowed. +- **Export:** Only shapes declared in the same file can be exported with `export shape Name`. Exported shapes are visible to other namespaces for use as types or as base shapes in composition. Unexported shapes are namespace-local. +- **Field order:** The order of fields in a shape literal does not affect type checking; only presence and types (and constraints) matter. +- **Naming:** Shape names and field names are [identifiers](/reference/identifiers). The same namespace can contain multiple shapes; policies in that namespace can reference any shape in the same namespace or any exported shape from another namespace (per import/resolution rules). diff --git a/src/content/docs/reference/trinary.md b/src/content/docs/reference/trinary.md index 04c582d..591ef58 100644 --- a/src/content/docs/reference/trinary.md +++ b/src/content/docs/reference/trinary.md @@ -1,233 +1,100 @@ --- title: Trinary Values -description: Trinary values are a type of value that can be true, false, or unknown. +description: "Three-valued logic (true, false, unknown), truthiness, Kleene AND/OR/NOT tables, and use in when, ternary, and Elvis." --- -Sentrie uses **trinary logic** (also known as three-valued logic), which extends traditional boolean logic with a third value: `unknown`. This is essential for handling cases where information may be incomplete or unavailable. +Sentrie uses three values for logic and conditions: `true`, `false`, and `unknown`. `unknown` represents an indeterminate result (e.g. an optional field that is not present, or a comparison involving `unknown`). [Rule](/reference/rules) `when` guards, the [ternary](/reference/boolean-operations) operator (`? :`), and the Elvis operator (`?:`) use **truthiness**: only `true` is truthy; `false` and `unknown` are non-truthy. Logical operators (`and`, `or`, `xor`, `not`/`!`) follow **Kleene** three-valued logic so `unknown` propagates in defined ways. -### Trinary Values +## Syntax -Sentrie supports three trinary values: +**Literals:** `true` | `false` | `unknown` -- **`true`** - The condition is definitely true -- **`false`** - The condition is definitely false -- **`unknown`** - The condition's truth value cannot be determined +**Logical operators (binary):** `and` | `or` | `xor` -### Kleene Truth Tables +**Logical negation (unary):** `not expr` | `! expr` -Sentrie implements **Kleene's three-valued logic**, which provides a consistent way to handle `unknown` values in logical operations. +## Configuration & Arguments -#### Logical AND (`and`) +### Truthiness -The `and` operator follows Kleene's AND truth table: +Used by `when`, ternary (`? :`), and Elvis (`?:`): -| **AND** | **true** | **false** | **unknown** | -| ----------- | --------- | --------- | ----------- | -| **true** | `true` | `false` | `unknown` | -| **false** | `false` | `false` | `false` | -| **unknown** | `unknown` | `false` | `unknown` | +| Value | Truthy? | +| :-------- | :------ | +| `true` | Yes | +| `false` | No | +| `unknown` | No | -**Key behaviors:** +So a rule with `when cond` runs its body only if `cond` evaluates to `true`; if `cond` is `false` or `unknown`, the rule uses its default (or `unknown`). -- `true and x` = `x` (true is the identity element) -- `false and x` = `false` (false dominates) -- `unknown and true` = `unknown` (cannot determine if both are true) -- `unknown and false` = `false` (false dominates) -- `unknown and unknown` = `unknown` (cannot determine) +### NOT (unary) -#### Logical OR (`or`) +One operand. The operand is interpreted as trinary; the result is trinary. -The `or` operator follows Kleene's OR truth table: +| Input | Output | +| :-------- | :------- | +| `true` | `false` | +| `false` | `true` | +| `unknown` | `unknown` | -| **OR** | **true** | **false** | **unknown** | -| ----------- | -------- | --------- | ----------- | -| **true** | `true` | `true` | `true` | -| **false** | `true` | `false` | `unknown` | -| **unknown** | `true` | `unknown` | `unknown` | +### Kleene AND (binary) -**Key behaviors:** +| AND | true | false | unknown | +| :-------- | :------ | :------ | :------ | +| **true** | true | false | unknown | +| **false** | false | false | false | +| **unknown** | unknown | false | unknown | -- `true or x` = `true` (true dominates) -- `false or x` = `x` (false is the identity element) -- `unknown or true` = `true` (true dominates) -- `unknown or false` = `unknown` (cannot determine if either is true) -- `unknown or unknown` = `unknown` (cannot determine) +### Kleene OR (binary) -#### Logical NOT (`not` or `!`) +| OR | true | false | unknown | +| :-------- | :------ | :------ | :------ | +| **true** | true | true | true | +| **false** | true | false | unknown | +| **unknown** | true | unknown | unknown | -The `not` operator follows this truth table: +### XOR (binary) -| **Input** | **Output** | -| ----------- | ---------- | -| **true** | `false` | -| **false** | `true` | -| **unknown** | `unknown` | +XOR is defined so that exactly one of the two operands is truthy for the result to be true. When either operand is `unknown`, the result is typically `unknown` (implementation-defined). See the runtime for the exact table. -**Key behavior:** +**Returns:** Trinary. For `when` and conditionals, only `true` is truthy. -- `not unknown` = `unknown` (cannot determine the opposite of unknown) +## When unknown arises -### Trinary Logic Examples +- **Optional or missing field:** Accessing a field that is not present (e.g. on an optional shape field) may yield `unknown` or a type-specific default depending on context. +- **Comparisons:** Equality or ordering with `unknown` often yields `unknown` (e.g. `unknown == true` → `unknown`). +- **Logical propagation:** As in the Kleene tables, `and`/`or`/`not` propagate `unknown` instead of treating it as true or false. -#### Basic Trinary Operations +## Examples in Action -```sentrie --- Unknown from undefined field access -let value = user.nonexistent.field -let result = value == "test" -- unknown (operation on unknown) - --- AND with unknown -let a = true -let b = unknown -let result1 = a and b -- unknown - -let c = false -let d = unknown -let result2 = c and d -- false (false dominates) - --- OR with unknown -let e = true -let f = unknown -let result3 = e or f -- true (true dominates) - -let g = false -let h = unknown -let result4 = g or h -- unknown -``` - -#### Practical Use Cases +### Propagating unknown ```sentrie -shape User { - name!: string - email?: string - age?: number -} - -fact user: User - --- Check if user has email (handles unknown gracefully) -let has_email: trinary = user.email is defined and user.email is not empty --- Result: true if email exists and is not empty --- false if email is defined but empty --- unknown if email is not defined - --- Age verification with unknown handling -let can_vote: trinary = user.age is defined ? (user.age >= 18) : unknown --- Result: true if age >= 18 --- false if age < 18 --- unknown if age is not defined - --- Complex logic with unknown propagation -let is_verified: trinary = user.email is defined and - user.email matches "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" --- Result: true if email is defined and valid --- false if email is defined but invalid --- unknown if email is not defined +let a = true and unknown -- unknown +let b = false or unknown -- unknown +let c = not unknown -- unknown ``` -#### Handling Unknown in Rules +### When guard and unknown -```sentrie -shape Account { - username!: string - email?: string - verified: bool -} - -fact account: Account - --- Rule that handles unknown gracefully -rule can_access = default false when account.email is defined { - let email_valid = account.email matches "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$" - yield account.verified and email_valid -} - --- If email is not defined, the 'when' clause prevents evaluation --- If email is defined, the rule evaluates normally -``` - -#### Unknown Propagation +If a `when` expression evaluates to `unknown` (e.g. optional field missing), the rule does not run its body; it uses its default or returns `unknown`: ```sentrie --- Unknown propagates through operations -let a = user.nonexistent.field -- undefined -let b = a + 1 -- undefined (operation on undefined) -let c = b > 10 -- undefined (comparison with undefined) -let d = c and true -- unknown (AND with undefined) -let e = d or false -- unknown (OR with unknown) -``` - -## Non-Trinary Value Interpretation - -Sentrie can work with various data types, and when a value that isn't explicitly `true`, `false`, or `unknown` is used in a context that requires a `trinary` value, the system infers the `trinary` value based on the following rules: - -- `null` / `undefined` → `unknown` -- Numeric primitives (`int`, `float`, `uint`, etc.) evaluate to `false` when zero, `true` otherwise. -- `string` values are checked for textual keywords first (see table below), otherwise `true` when they are non-empty. -- Structs, channels, functions, and any other non-`nil` value evaluate to `true` - -### String coercion - -Before falling back to the "non-empty string" rule, strings are normalized to lowercase and matched against the following keywords: - -| Input | Result | -| ------------------------------------------------------------ | --------- | -| `"true"`, `"1"`, `"t"` | `true` | -| `"false"`, `"0"`, `"f"` | `false` | -| `"unknown"`, `"-1"`, `"n"`, `"nil"`, `"null"`, `"undefined"` | `unknown` | -| Any other non-empty string | `true` | -| Empty string (`""`) | `false` | - -### Collections - -For collections like `lists`, `maps`, `slices`, and `arrays`, truthiness depends on whether they contain elements. Empty collections are `false`; non-empty collections are `true`. - -## Trinary vs Boolean - -Sentrie does not distinguish between trinary and boolean values: - -- `true` (bool) → `true` (trinary) -- `false` (bool) → `false` (trinary) - -:::note -A `bool` value is a special case of a `trinary` value which can never be `unknown`. -::: - -## Best Practices for Trinary Logic - -1. **Check for definedness before operations:** - -```sentrie --- Good: Check if value is defined first -let result = user.email is defined ? str.length(user.email) > 0 : false - --- Avoid: Operations on potentially undefined values -let result = str.length(user.email) > 0 -- unknown if email is undefined +rule allow = default false when user.role is defined { + yield user.role == "admin" +} ``` -2. **Use `is defined` to handle unknown:** +Here `user.role is defined` is false or unknown when the field is missing; the outcome is then `default false` (or `unknown` if no default). -```sentrie --- Explicitly handle unknown cases -let can_proceed = user.age is defined and user.age >= 18 -``` +### Trinary constraints -3. **Understand unknown propagation:** +For runtime validation of trinary values (e.g. require `true` or forbid `unknown`), use [Constraints](/reference/constraints): trinary supports `@not_unknown()`, `@eq`, `@neq`, `@is_true()`, and `@is_false()`. -```sentrie --- Unknown propagates through all operations -let value = user.missing.field -- unknown -let result = value + 1 -- unknown -let comparison = result > 10 -- unknown -``` +## Good to Know -4. **Use default values in rules:** +Before you implement this, keep a few boundaries in mind: -```sentrie --- Provide defaults for unknown cases -rule can_access = default false when user.role is defined { - yield user.role == "admin" -} --- Returns false if role is not defined (unknown case) -``` +- **Constraint:** Undefined or missing field access can yield `unknown`. Operations on `unknown` propagate according to the Kleene tables. Rule `when` and ternary/Elvis use truthiness: only `true` is truthy. +- **Edge case:** If a `when` evaluates to `unknown`, the rule uses its default or returns `unknown`. `not unknown` is `unknown`, so it is still non-truthy. +- **Trinary constraints:** See [Constraints](/reference/constraints) for `@not_unknown()`, `@eq`, `@neq`, `@is_true()`, `@is_false()` on trinary/bool types. diff --git a/src/content/docs/reference/types-and-values.md b/src/content/docs/reference/types-and-values.md index bf9b334..8cadee3 100644 --- a/src/content/docs/reference/types-and-values.md +++ b/src/content/docs/reference/types-and-values.md @@ -1,44 +1,100 @@ --- title: Types and Values -description: Sentrie has a comprehensive type system with primitive types, collections, and user-defined shapes. +description: "Built-in primitive and collection types, type declarations, indexing, cast, and validation." --- -Sentrie provides a robust type system that includes primitive types, collection types, and [user-defined shapes](/reference/shapes/). The type system is enhanced by a powerful [constraint system](/reference/constraints) that allows you to validate values against specific rules. +Sentrie provides primitive types (`number`, `string`, `trinary`, `bool`, `document`) and collection types (`list[T]`, `map[T]`, `record[T1,T2,...]`). User-defined [shapes](/reference/shapes) extend these. Types can be used in [let](/reference/let), [facts](/reference/facts), and shape fields. Types may carry [constraints](/reference/constraints) for runtime validation. -## Primitive Types +## Syntax -Sentrie supports five fundamental primitive types: +**Primitives:** `number` | `string` | `trinary` | `bool` | `document` -| Type | Description | -| ---------- | --------------------------------------------------------------- | -| `number` | Numeric values | -| `string` | Text strings | -| `trinary` | Trinary values (`true` / `false` / `unknown`) | -| `bool` | Boolean values (`true` / `false`) - a special case of `trinary` | -| `document` | Arbitrary JSON-like objects | +**Collections:** `list[T]` | `map[T]` | `record[T1, T2, ...]` -### Declaring a Primitive Type +**Cast:** `cast expr as type` + +**Indexing:** +- List/record: `expr[number]` (zero-based index). +- Map: `expr[string]` or `expr.identifier` (key must be a string; dot form when the key is a [valid identifier](/reference/identifiers)). + +## Configuration & Arguments + +### Primitive types + +| Type | Description | Notes | +| :--- | :---------- | :---- | +| `number` | 64-bit floating-point (float64). | All numeric literals and arithmetic use this. No separate integer type. | +| `string` | UTF-8 string. | Literals: double-quoted or backtick. | +| `trinary` | Three-valued logic: `true`, `false`, `unknown`. | Used for conditions and [boolean operations](/reference/boolean-operations). See [Trinary](/reference/trinary). | +| `bool` | Two-valued: `true`, `false`. | Subset of trinary; no `unknown` literal for bool, but expressions can produce trinary. | +| `document` | JSON-like object (string keys, arbitrary values). | Used for untyped or semi-structured data. | + +### Collection types + +| Type | Description | Index type | Notes | +| :--- | :---------- | :--------- | :---- | +| `list[T]` | Ordered sequence of values of type `T`. | `number` (zero-based). | Index out of range is an error or undefined behavior per implementation. | +| `map[T]` | Key-value map; keys are strings, values are type `T`. | `string` or `.key` (identifier). | Keys must be strings. Dot access: `map.key` when key is a [valid identifier](/reference/identifiers). | +| `record[T1,T2,...]` | Fixed-length tuple with typed positions. | `number` (zero-based position). | Length and types are fixed at type declaration. | + +### Cast + +`cast expr as type` converts the value of `expr` to the given type. The result is validated against the type and any [constraints](/reference/constraints) on that type. If conversion or validation fails, evaluation aborts. Supported conversions include string↔number, number↔string, bool↔string, and literal document to `document`. + +**Returns:** N/A for type declarations. For `cast`: the value after conversion and validation; failure aborts evaluation. + +## Indexing and access + +- **List:** `listExpr[indexExpr]`. `indexExpr` must be a number (or evaluate to a number). Zero-based. Bounds behavior is implementation-defined (error or undefined). +- **Record:** `recordExpr[indexExpr]`. Same as list; index is the position (0, 1, 2, …). +- **Map:** `mapExpr[keyExpr]` or `mapExpr.key`. Key must be a string. Dot form is equivalent to bracket form when the key is a [valid identifier](/reference/identifiers) (e.g. `m.one` ≈ `m["one"]`). + +## Where types are used + +- **Facts:** Fact type is a shape, primitive, or collection (e.g. `fact user: User`, `fact id: string`). +- **Let:** Optional type annotation: `let x: number = 5`. When present, value is validated at runtime. +- **Shape fields:** Each field has a type (primitive, collection, or shape). +- **Constraints:** Types can be constrained (e.g. `number @min(0) @max(100)`). See [Constraints](/reference/constraints). + +## Examples in Action + +### Primitives and collections ```text let u: number = 50 +let s: string = "hello" +let b: bool = true +let arr: list[number] = [1, 2, 3] +let m: map[number] = { "one": 1, "two": 2 } +let r: record[string, number, bool] = ["one", 1, true] ``` -```text -let u: number = 50.5 -``` +### Indexing ```text -let u: string = "hello" +let first: number = arr[0] +let one: number = m["one"] +let oneAlt: number = m.one ``` -```text -let u: bool = true -``` +### Cast ```text -let u: document = { "name": "John", "age": 30 } +let x: number = cast "50" as number +let y: string = cast 50 as string +let z: bool = cast "true" as bool +let doc: document = cast { "name": "John", "age": 30 } as document ``` +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- **Type annotation on let:** Optional. When omitted, the value is not validated against a type. When present, the value is validated (and constraints apply); failure aborts evaluation. +- **Map keys:** Must be strings. Access with `[index]`: numeric index for list/record, string key for map. Dot access for maps only when the key is a [valid identifier](/reference/identifiers). +- **Division by zero:** Aborts evaluation. [Constraint](/reference/constraints) failure also aborts evaluation. +- **Cast:** Validates against the target type and any constraints; failure aborts evaluation. Not all type combinations may be supported; see implementation for allowed conversions. + ## Collection Types Collections allow you to work with groups of related values: @@ -46,7 +102,7 @@ Collections allow you to work with groups of related values: | Type | Description | | --------------------- | ------------------------------------------- | | `list[T]` | Lists of type `T` | -| `dict[T]` | Maps with `string` keys and type `T` values | +| `map[T]` | Maps with `string` keys and type `T` values | | `record[T1, T2, ...]` | Tuples with specific types | ### Declaring a Collection Type @@ -56,7 +112,7 @@ let u: list[number] = [1, 2, 3] ``` ```text -let u: dict[number] = { "one": 1, "two": 2, "three": 3 } +let u: map[number] = { "one": 1, "two": 2, "three": 3 } ``` ```text @@ -74,7 +130,7 @@ However, it is recommended to declare the type for better readability and to avo ### Accessing Collection Elements -You can access collection elements using the `[index]` syntax. The index must be a `string` for dicts and a `number` for lists and records. +You can access collection elements using the `[index]` syntax. The index must be a `string` for maps and a `number` for lists and records. ```text let u: list[number] = [1, 2, 3] @@ -82,14 +138,14 @@ let first: number = u[0] ``` ```text -let u: dict[number] = { "one": 1, "two": 2, "three": 3 } +let u: map[number] = { "one": 1, "two": 2, "three": 3 } let first: number = u["one"] ``` -For dicts, you can also access elements using the `.` syntax. +For maps, you can also access elements using the `.` syntax. ```text -let u: dict[number] = { "one": 1, "two": 2, "three": 3 } +let u: map[number] = { "one": 1, "two": 2, "three": 3 } let first: number = u.one ``` diff --git a/src/content/docs/reference/typescript_modules/index.md b/src/content/docs/reference/typescript_modules/index.md index fd11875..047f156 100644 --- a/src/content/docs/reference/typescript_modules/index.md +++ b/src/content/docs/reference/typescript_modules/index.md @@ -1,15 +1,20 @@ --- title: Built-in TypeScript Modules -description: Complete reference for all built-in TypeScript modules in Sentrie. +description: "Reference for built-in @sentrie/* modules: import syntax and module list." --- -Sentrie provides a comprehensive set of built-in TypeScript modules for common operations. These modules are available under the `@sentrie/*` namespace and can be imported using the `use` statement in your policies. -## Quick Start +Built-in modules provide functions for hashing, encoding, time, JSON, regex, and more. Import with `use { ... } from @sentrie/module`; no quotes. For custom modules see [Writing custom TypeScript modules](/extensibility/writing-custom-typescript-modules). -Import and use built-in modules in your policies: +## Syntax ```text +use { fn1, fn2 } from @sentrie/module [ as alias ] +``` + +### Example policy + +```sentrie namespace com/example/mypolicy policy mypolicy { @@ -25,23 +30,29 @@ policy mypolicy { let hash = sha256(data) let currentTime = now() let jsonData = json.parse(data) - let isValid = jsonUtil.isValid(data) - yield hash != "" and currentTime > timestamp and isValid + let ok = jsonUtil.isValid(data) + yield hash != "" and currentTime > timestamp and ok } export decision of processData } ``` -## Module Categories +Built-in modules: `@sentrie/module` (no quotes). Default alias is the last path segment (e.g. `time` for `@sentrie/time`). -### Data Manipulation +## Configuration & Arguments -#### [Collection](./sentrie/collection) +| Element | Type | Required | Description | +| :--- | :--- | :--- | :--- | +| `fn1`, `fn2` | identifiers | Yes | Exported function names from the module. | +| `@sentrie/module` | path | Yes | Module name (e.g. `@sentrie/hash`, `@sentrie/time`). | +| `as alias` | identifier | No | Name used in policy (e.g. `alias.fn1()`). | -List and dict manipulation utilities. Functions are prefixed with `list_` for array operations and `map_` for plain-object (dict) operations. +**Returns:** N/A (import). Function return types are per module; see linked pages. -**Key Functions:** +The [@sentrie/collection](./sentrie/collection) module provides list and map manipulation utilities. Functions are prefixed with `list_` for array operations and `map_` for map (object) operations. + +## Examples in Action - `list_includes`, `list_sort`, `list_unique`, `list_chunk` - `map_keys`, `map_values`, `map_get`, `map_merge` @@ -359,26 +370,44 @@ When importing from a module, the default alias is the last part of the module p ```text use { now } from @sentrie/time --- Default alias is "time" (last part of @sentrie/time) --- Use as: time.now() +use { sha256 } from @sentrie/hash +use { isValid } from @sentrie/json as jsonUtil ``` -## Best Practices +### Going further -1. **Use appropriate modules** - Choose the right module for your use case (e.g., use `@sentrie/hash` for secure hashing instead of `@sentrie/crypto`) +```text +use { now } from @sentrie/time +use { sha256 } from @sentrie/hash +use { parse } from @sentrie/js as json +let t = time.now() +let h = sha256(data) +let ok = jsonUtil.isValid(data) +``` -2. **Security considerations** - Use SHA-256 or SHA-512 for secure hashing. Avoid MD5 and SHA-1 for security-sensitive operations. +## Module List -3. **Performance** - Regex patterns are compiled and cached, so repeated use of the same pattern is efficient. +| Module | Description | +| :--- | :--- | +| [@sentrie/collection](/reference/typescript_modules/sentrie/collection) | List/map utilities (`list_*`, `map_*`) | +| [@sentrie/crypto](/reference/typescript_modules/sentrie/crypto) | SHA-256 | +| [@sentrie/encoding](/reference/typescript_modules/sentrie/encoding) | Base64, hex, URL encode/decode | +| [@sentrie/hash](/reference/typescript_modules/sentrie/hash) | MD5, SHA-1, SHA-256, SHA-512, HMAC | +| [@sentrie/js](/reference/typescript_modules/sentrie/js) | Math, String, Number, Date, JSON, Array globals | +| [@sentrie/json](/reference/typescript_modules/sentrie/json) | `isValid` (JSON validation) | +| [@sentrie/jwt](/reference/typescript_modules/sentrie/jwt) | Decode/verify JWT (HS256/384/512) | +| [@sentrie/net](/reference/typescript_modules/sentrie/net) | CIDR, parseIP, isPrivate, etc. | +| [@sentrie/regex](/reference/typescript_modules/sentrie/regex) | match, find, replace, split | +| [@sentrie/semver](/reference/typescript_modules/sentrie/semver) | compare, isValid, satisfies, major/minor/patch | +| [@sentrie/time](/reference/typescript_modules/sentrie/time) | now, parse, format, addDuration, isBefore, etc. | +| [@sentrie/url](/reference/typescript_modules/sentrie/url) | parse, join, getHost, getPath, getQuery, isValid | +| [@sentrie/uuid](/reference/typescript_modules/sentrie/uuid) | v4, v6, v7 | -4. **Type safety** - All modules provide full TypeScript type definitions for better error detection and IntelliSense support. +## Good to Know -5. **Error handling** - Most functions throw errors on invalid input. Always validate inputs before calling module functions. +Before you implement this, keep a few boundaries in mind: -6. **Module organization** - Use aliases when importing multiple functions from the same module to keep code readable. +- Only the listed functions can be imported. Paths are resolved at load time. Invalid function names or modules cause errors. -## See Also -- [Using TypeScript](/reference/using-typescript) - Complete guide to using TypeScript in Sentrie -- [Policy Pack Structure](/structure-of-a-policy-pack/overview) - Learn about organizing your policy pack -- [Policy Language Reference](/reference) - Complete reference for the Sentrie policy language +- Built-in `@sentrie/*` modules do not use quotes. Local files use quoted paths (e.g. `"./utils.ts"`). JWT module only decodes/verifies; it does not create tokens. Prefer SHA-256/SHA-512 over MD5/SHA-1 for security. diff --git a/src/content/docs/reference/typescript_modules/sentrie/collection.md b/src/content/docs/reference/typescript_modules/sentrie/collection.md index 8a86ed4..cbb1182 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/collection.md +++ b/src/content/docs/reference/typescript_modules/sentrie/collection.md @@ -333,3 +333,12 @@ policy mypolicy { export decision of myrule } ``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- List functions use `list_` prefix; map functions use `map_` prefix. Operations do not mutate the original collection. + + +- Throws if input is not an array (for list_*) or not an object (for map_*). Deep equality used where applicable. diff --git a/src/content/docs/reference/typescript_modules/sentrie/crypto.md b/src/content/docs/reference/typescript_modules/sentrie/crypto.md index 4225bac..81f15fe 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/crypto.md +++ b/src/content/docs/reference/typescript_modules/sentrie/crypto.md @@ -45,6 +45,11 @@ policy mypolicy { } ``` -## See Also +## Good to Know -- [@sentrie/hash](./hash) - For additional hash algorithms (MD5, SHA-1, SHA-512, HMAC) +Before you implement this, keep a few boundaries in mind: + +- Returns SHA-256 hash as hexadecimal string. For more algorithms use [@sentrie/hash](/reference/typescript_modules/sentrie/hash). + + +- Input must be string. Invalid or unsupported input may throw. diff --git a/src/content/docs/reference/typescript_modules/sentrie/encoding.md b/src/content/docs/reference/typescript_modules/sentrie/encoding.md index 0f9f96b..94a6d3c 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/encoding.md +++ b/src/content/docs/reference/typescript_modules/sentrie/encoding.md @@ -178,3 +178,12 @@ policy mypolicy { export decision of encodeData } ``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- Base64/hex/URL encode decode; input and output are strings. Invalid Base64/hex input may throw. + + +- base64Url omits padding; URL encoding is percent-encoding. Decode errors throw. diff --git a/src/content/docs/reference/typescript_modules/sentrie/hash.md b/src/content/docs/reference/typescript_modules/sentrie/hash.md index f1328ce..8a29299 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/hash.md +++ b/src/content/docs/reference/typescript_modules/sentrie/hash.md @@ -29,7 +29,7 @@ Computes the MD5 hash of a string. ```text use { md5 } from @sentrie/hash -let hash = hash.md5("Hello, World!") // "65a8e27d8879283831b664bd8b7f0ad4" +let hash = md5("Hello, World!") // "65a8e27d8879283831b664bd8b7f0ad4" ``` ### `sha1(str: string): string` @@ -105,7 +105,7 @@ Computes HMAC (Hash-based Message Authentication Code) for data using a secret k ```text use { hmac } from @sentrie/hash -let mac = hash.hmac("sha256", "Hello, World!", "secret-key") +let mac = hmac("sha256", "Hello, World!", "secret-key") ``` ## Complete Example @@ -137,8 +137,12 @@ policy mypolicy { } ``` -## Security Recommendations +## Good to Know -1. **Use SHA-256 or SHA-512** for secure hashing instead of MD5 or SHA-1 -2. **Use HMAC** for message authentication with a secret key -3. **Store hashed passwords** using SHA-256 or SHA-512 with proper salt (consider using HMAC with a secret key) +Before you implement this, keep a few boundaries in mind: + +- All hash functions return hexadecimal-encoded strings. Input is string; HMAC takes algorithm name, message, and secret key. +- Use SHA-256 or SHA-512 for security; MD5 and SHA-1 are cryptographically broken. + + +- Invalid input (e.g. non-hex for decode) may throw. HMAC algorithm must be supported (e.g. sha256). diff --git a/src/content/docs/reference/typescript_modules/sentrie/js.md b/src/content/docs/reference/typescript_modules/sentrie/js.md index bf59991..5ebeb19 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/js.md +++ b/src/content/docs/reference/typescript_modules/sentrie/js.md @@ -473,3 +473,12 @@ policy mypolicy { export decision of processData } ``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- Exposes Math, String, Number, Date, JSON, Array as individual functions. Behavior matches JavaScript; angles in radians for trig. + + +- Invalid input (e.g. NaN, invalid date) may produce NaN or throw per JS semantics. random() is not cryptographically secure. diff --git a/src/content/docs/reference/typescript_modules/sentrie/json.md b/src/content/docs/reference/typescript_modules/sentrie/json.md index 14c5855..871c0ee 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/json.md +++ b/src/content/docs/reference/typescript_modules/sentrie/json.md @@ -1,45 +1,40 @@ --- title: "@sentrie/json" -description: JSON validation utility +description: JSON validation. For parse/stringify use @sentrie/js. --- -The `@sentrie/json` module provides JSON validation utilities. For JSON parsing and stringification, use the `@sentrie/js` module. -## Usage +Provides `isValid` to check if a string is valid JSON. For parsing and stringifying use [@sentrie/js](/reference/typescript_modules/sentrie/js). + +## Syntax ```text -use { isValid } from @sentrie/json +use { isValid } from @sentrie/json [ as alias ] +alias.isValid(str) ``` -## Functions - -### `isValid(str: string): boolean` - -Validates whether a string is valid JSON. +## Configuration & Arguments -**Parameters:** +| Name | Type | Required | Description | +| :---- | :----- | :------- | :-------------------------- | +| `str` | string | Yes | String to validate as JSON. | -- `str` - The JSON string to validate +**Returns:** `boolean` - true if the string is valid JSON, false otherwise. -**Returns:** `true` if the string is valid JSON, `false` otherwise +## Examples in Action -**Example:** +### Typical use ```text -use { isValid } from @sentrie/json - -let jsonStr = '{"name": "John", "age": 30}' -let isValid = json.isValid(jsonStr) // true - -let invalidStr = '{"name": "John", "age":}' -let isInvalid = json.isValid(invalidStr) // false +use { isValid } from @sentrie/json as jsonUtil +let ok = jsonUtil.isValid('{"name": "John", "age": 30}') +let bad = jsonUtil.isValid('{"name": "John", "age":}') ``` -## JSON Parsing and Stringification - -For JSON parsing and stringification, use the `@sentrie/js` module: +### Going further ```text +use { isValid } from @sentrie/json as jsonUtil use { parse, stringify } from @sentrie/js as json let obj = json.parse('{"name": "John", "age": 30}') @@ -73,3 +68,12 @@ policy mypolicy { export decision of processData } ``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- Only validates syntax; does not parse. Use `@sentrie/js` for `parse` and `stringify`. + + +- Empty string is not valid JSON. Invalid UTF-8 or malformed structure returns false. diff --git a/src/content/docs/reference/typescript_modules/sentrie/jwt.md b/src/content/docs/reference/typescript_modules/sentrie/jwt.md index e93d707..df79072 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/jwt.md +++ b/src/content/docs/reference/typescript_modules/sentrie/jwt.md @@ -150,3 +150,12 @@ header.payload.signature - **Header** - Contains metadata about the token (algorithm, type) - **Payload** - Contains the claims (data) - **Signature** - Used to verify the token hasn't been tampered with + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- Decode and verify only; does not create tokens. Supported algorithms: HS256, HS384, HS512. Always verify signature before trusting payload. + + +- Invalid token format, bad signature, or unsupported algorithm throws. Use verify() or decode() with secret; do not rely on getPayload() alone. diff --git a/src/content/docs/reference/typescript_modules/sentrie/net.md b/src/content/docs/reference/typescript_modules/sentrie/net.md index b7f47c5..cfbfec3 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/net.md +++ b/src/content/docs/reference/typescript_modules/sentrie/net.md @@ -258,3 +258,12 @@ policy mypolicy { export decision of checkAccess } ``` + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- IPv4 and IPv6; CIDR and single IP. parseIP returns parsed representation or null. Invalid CIDR/IP throws. + + +- Invalid CIDR or IP throws. Use isPrivate, isPublic, isLoopback for classification. diff --git a/src/content/docs/reference/typescript_modules/sentrie/regex.md b/src/content/docs/reference/typescript_modules/sentrie/regex.md index 8a95f66..2ca35e7 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/regex.md +++ b/src/content/docs/reference/typescript_modules/sentrie/regex.md @@ -200,3 +200,12 @@ let alnumPattern = "^[a-zA-Z0-9_]+$" - All patterns are compiled and cached for performance - Repeated use of the same pattern in a single execution context will use the cached compiled pattern - Patterns are automatically cleaned up after execution + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- Patterns are strings; compiled and cached per execution. match/find/findAll/replace/replaceAll/split follow standard regex semantics. + + +- Invalid pattern throws. Replace/split return new string/list; original unchanged. diff --git a/src/content/docs/reference/typescript_modules/sentrie/semver.md b/src/content/docs/reference/typescript_modules/sentrie/semver.md index 006d7b2..8da9146 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/semver.md +++ b/src/content/docs/reference/typescript_modules/sentrie/semver.md @@ -234,3 +234,12 @@ Semantic versions follow the format: `MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]` - **PATCH** - Incremented for backwards-compatible bug fixes - **PRERELEASE** - Optional prerelease identifier (e.g., `alpha`, `beta`, `rc.1`) - **BUILD** - Optional build metadata (e.g., `+001`, `+exp.sha.5114f85`) + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- "v" prefix is stripped; compare returns -1, 0, 1. satisfies uses npm-style range. major/minor/patch/prerelease/metadata extract components. + + +- Invalid version string throws. stripPrefix removes leading "v". diff --git a/src/content/docs/reference/typescript_modules/sentrie/time.md b/src/content/docs/reference/typescript_modules/sentrie/time.md index 7b8d058..23a80e1 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/time.md +++ b/src/content/docs/reference/typescript_modules/sentrie/time.md @@ -243,3 +243,12 @@ Common format patterns: - `"15:04:05"` - Time: `15:04:05` - `"2006-01-02 15:04:05"` - Date and time: `2006-01-02 15:04:05` - `"Mon, 02 Jan 2006 15:04:05 MST"` - RFC1123 format + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- Timestamps are Unix seconds. now() is fixed within an execution. addDuration/subtractDuration use duration strings (e.g. "1h", "30m"). isBefore/isAfter/isBetween compare timestamps. + + +- Invalid format or duration may throw. Constants (RFC3339, etc.) are format layout strings. diff --git a/src/content/docs/reference/typescript_modules/sentrie/url.md b/src/content/docs/reference/typescript_modules/sentrie/url.md index df67e58..d9c1c48 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/url.md +++ b/src/content/docs/reference/typescript_modules/sentrie/url.md @@ -194,3 +194,12 @@ policy mypolicy { - Relative URLs (starting with `"/"`) are supported - The `parse` function provides comprehensive URL parsing with all components - The `join` function properly resolves relative paths against base URLs + +## Good to Know + +Before you implement this, keep a few boundaries in mind: + +- parse returns ParsedURL; getHost/getPath/getQuery work on string URLs. Encoding/decoding is in @sentrie/encoding. + + +- Invalid URL may throw or return invalid result. isValid checks format. Relative URLs (starting with /) supported. diff --git a/src/content/docs/reference/typescript_modules/sentrie/uuid.md b/src/content/docs/reference/typescript_modules/sentrie/uuid.md index cad5e3d..790e38c 100644 --- a/src/content/docs/reference/typescript_modules/sentrie/uuid.md +++ b/src/content/docs/reference/typescript_modules/sentrie/uuid.md @@ -1,40 +1,41 @@ --- title: "@sentrie/uuid" -description: UUID generation +description: UUID generation (v4, v6, v7). --- -The `@sentrie/uuid` module provides functions for generating UUIDs (Universally Unique Identifiers). -## Usage +Generates UUIDs. v4: random; v6/v7: time-ordered. Use when you need unique identifiers or time-ordered IDs for indexing. + +## Syntax ```text -use { v4, v6, v7 } from @sentrie/uuid +use { v4, v6, v7 } from @sentrie/uuid [ as alias ] +alias.v4() +alias.v6() +alias.v7() ``` -## Functions +## Configuration & Arguments -### `v4(): string` +| Function | Parameters | Required | Description | +| :------- | :--------- | :------- | :------------------------------------------------- | +| `v4()` | none | - | Random UUID (version 4). | +| `v6()` | none | - | Time-ordered UUID (version 6). | +| `v7()` | none | - | Time-ordered UUID with Unix timestamp (version 7). | -Generates a version 4 UUID (random UUID). Version 4 UUIDs are randomly generated and provide strong uniqueness guarantees. +**Returns:** `string` - UUID in form `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`. Throws on generation failure. -**Returns:** A UUID string in standard format (e.g., `"550e8400-e29b-41d4-a716-446655440000"`) +## Examples in Action -**Example:** +### Typical use ```text -use { v4 } from @sentrie/uuid -let uuid = uuid.v4() // "550e8400-e29b-41d4-a716-446655440000" +use { v4, v7 } from @sentrie/uuid +let id = uuid.v4() +let timeId = uuid.v7() ``` -### `v6(): string` - -Generates a version 6 UUID (time-ordered UUID). Version 6 UUIDs are time-ordered and provide better database indexing performance. - -**Returns:** A UUID string in standard format (e.g., `"1b21dd213814000-8000-6000-0000-000000000000"`) - -**Throws:** Error if UUID generation fails - -**Example:** +### Going further ```text use { v6 } from @sentrie/uuid @@ -96,40 +97,11 @@ policy mypolicy { } ``` -## When to Use Each Version - -### Use Version 4 (v4) when: +## Good to Know -- You need general-purpose unique identifiers -- Ordering doesn't matter -- You want maximum randomness - -### Use Version 6 (v6) when: - -- You need time-ordered UUIDs -- Database indexing performance is important -- You want UUIDs that are roughly ordered by creation time - -### Use Version 7 (v7) when: - -- You need time-ordered UUIDs with Unix timestamp -- Sorting by creation time is important -- You want the best database indexing performance -- You need to extract timestamp information from the UUID - -## UUID Format - -All UUID versions follow the standard format: - -``` -xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -``` +Before you implement this, keep a few boundaries in mind: -Where each `x` is a hexadecimal digit (0-9, a-f). +- v4: random; no ordering. v6/v7: time-ordered for better DB indexing/sorting. v7 includes Unix timestamp. -## Security Considerations -- UUIDs are not cryptographically secure random numbers -- For security-sensitive applications, use proper cryptographic random number generators -- UUIDs are designed for uniqueness, not security -- Version 4 UUIDs provide good randomness but are not suitable for cryptographic purposes +- UUIDs are not cryptographically secure; use proper CSPRNG for security-sensitive randomness. Generation failure throws. diff --git a/src/content/docs/running-sentrie/serving-policies.md b/src/content/docs/running-sentrie/serving-policies.md index de8eb2a..4912f9d 100644 --- a/src/content/docs/running-sentrie/serving-policies.md +++ b/src/content/docs/running-sentrie/serving-policies.md @@ -7,15 +7,20 @@ description: "How to serve policies with Sentrie via HTTP API." Policies can be served via an HTTP API using the `sentrie serve` command. This allows you to evaluate policies and rules over HTTP, making Sentrie policies accessible to other services and applications. -## Starting the Server +At a high level: -The `sentrie serve` command starts an HTTP server that exposes your policy pack for evaluation: +- **You run** `sentrie serve` to load a policy pack and start an HTTP server. +- **Callers send** JSON facts to the `/decision/{namespace}/{policy}[/{rule}]` endpoint. +- **Sentrie returns** decisions and attachments; your system enforces based on those decisions. -```bash -sentrie serve -``` +## Where to find full details + +This page is a conceptual overview. For the complete HTTP API reference—including flags, endpoints, request/response schema, and error formats—see: + +- [CLI Reference: `sentrie serve`](/cli-reference/serve) +- [Deployment & Operations: Running as a Service](/deployment-operations/running-as-service) -### Command Options +The following sections summarize default flags and typical API usage. - `--http-port` (default: `7529`): Port number to listen on - `--pack-location` (default: `./`): Directory containing the policy pack to serve diff --git a/src/content/docs/troubleshooting/common-errors.md b/src/content/docs/troubleshooting/common-errors.md new file mode 100644 index 0000000..9250b6a --- /dev/null +++ b/src/content/docs/troubleshooting/common-errors.md @@ -0,0 +1,44 @@ +--- +title: Common Errors +description: The most frequent Sentrie errors, why they happen, and how to fix them. +--- + +This page lists common errors you may encounter when authoring or running Sentrie policies, along with likely causes and suggested fixes. + +## Language and pack errors + +| Error / symptom | When it happens | How to fix | See also | +| :-------------- | :-------------- | :--------- | :------- | +| Parse error (unexpected token, invalid syntax) | While loading `*.sentrie` files (CLI or server startup) | Check for missing `namespace`, unmatched braces, or misplaced keywords; compare against the examples in the reference pages. | [Namespaces](/reference/namespaces), [Policies](/reference/policies), [Rules](/reference/rules) | +| Unknown namespace/policy/rule | `sentrie exec` / `sentrie validate` / HTTP `/decision/...` target does not match an exported rule | Verify the namespace, policy, and rule names in your files and commands; ensure the rule is exported with `export decision of ...`. | [CLI: exec](/cli-reference/exec), [Running as a Service](/deployment-operations/running-as-service), [Exporting & Importing Rules](/reference/exporting-and-importing-rules) | +| Pack file (`sentrie.pack.toml`) error | Pack cannot be loaded; invalid or missing schema/pack section | Ensure `[schema]` and `[pack]` sections exist, `schema.version = 1`, and `pack.name`/`pack.version` are valid. | [`sentrie.pack.toml`](/structure-of-a-policy-pack/packfile) | + +## Facts, types, and constraints + +| Error / symptom | When it happens | How to fix | See also | +| :-------------- | :-------------- | :--------- | :------- | +| Missing required fact / `ErrRequiredFact` | Evaluation runs without providing all required facts | Add the missing fact to `--facts`, `--fact-file`, or the HTTP request body; or make the fact optional (`fact name?`) if it is truly optional. | [Facts](/reference/facts), [CLI: exec](/cli-reference/exec) | +| Type mismatch for fact | Fact value does not match its declared type or shape | Check the JSON structure and field names; ensure it matches the shape and field modifiers (`!`, `?`) defined in the policy. | [Types and Values](/reference/types-and-values), [Shapes](/reference/shapes) | +| Constraint validation failed | A value violates a constraint like `@min(0)` or `@uuid()` | Adjust the input value or change the constraint to match what you actually allow; avoid casting into constrained types unless the value really satisfies them. | [Constraints](/reference/constraints) | + +## Execution and HTTP service + +| Error / symptom | When it happens | How to fix | See also | +| :-------------- | :-------------- | :--------- | :------- | +| HTTP 400 (Bad Request) | Malformed JSON, missing `facts` field, or invalid path | Validate the JSON body, ensure it has a top-level `"facts"` object, and double-check the `/decision/{namespace}/{policy}[/{rule}]` path. | [Running as a Service](/deployment-operations/running-as-service) | +| HTTP 404 (Not Found) | Namespace, policy, or rule path does not resolve | Confirm that the namespace/policy/rule exists and that the rule is exported; check for typos and casing differences. | [Running as a Service](/deployment-operations/running-as-service), [Exporting & Importing Rules](/reference/exporting-and-importing-rules) | +| HTTP 405 (Method Not Allowed) | Using GET/PUT/etc. against the decision endpoint | Use `POST` for `/decision/...` and `GET` for `/health`. | [Running as a Service](/deployment-operations/running-as-service) | +| HTTP 500 (Internal Server Error) | Runtime failure during evaluation (e.g. type or constraint error) | Inspect the error detail, then cross-check the relevant policy, facts, and constraints; run the same target via `sentrie exec` or `sentrie validate` for more context. | [CLI: exec](/cli-reference/exec), [CLI: validate](/cli-reference/validate) | + +## Permissions and environment + +If your pack uses TypeScript modules that read files, call the network, or access environment variables, you may see permission-related failures: + +- Files or directories not readable when using local I/O helpers. +- Network calls failing despite valid URLs. +- Environment variables always appearing unset. + +These are usually caused by restrictive pack permissions. + +See **[Security and Permissions](/reference/security-and-permissions)** for how to configure `fs_read`, `net`, and `env` in `sentrie.pack.toml` and apply the principle of least privilege. + diff --git a/src/content/docs/troubleshooting/index.md b/src/content/docs/troubleshooting/index.md new file mode 100644 index 0000000..04ec98c --- /dev/null +++ b/src/content/docs/troubleshooting/index.md @@ -0,0 +1,15 @@ +--- +title: Troubleshooting +description: Diagnose and fix common issues when developing or running Sentrie policies. +--- + +Sentrie is designed to be deterministic and predictable, but misconfigurations and type or pack issues can still cause errors. This section helps you quickly identify what went wrong and where to look next. + +## What you'll find here + +- **Common errors**: Typical parse, type, and runtime failures and how to fix them. +- **Pointers to reference docs**: Where to look in the language and CLI reference for deeper details. +- **Operational hints**: When a problem is in your pack, your CLI invocation, or the HTTP service. + +Start with **[Common Errors](/troubleshooting/common-errors)** for a quick lookup of error messages, causes, and fixes. + diff --git a/src/content/docs/typescript-modules.md b/src/content/docs/typescript-modules.md index b7067c8..8fe53ad 100644 --- a/src/content/docs/typescript-modules.md +++ b/src/content/docs/typescript-modules.md @@ -3,8 +3,6 @@ title: "JavaScript Modules" description: "Integration with JavaScript modules for complex business logic." --- -# JavaScript Modules - > **Note**: This feature is currently under development and will be available in a future release. ## Overview