diff --git a/README.md b/README.md
index 91db2f8..ca2f4d4 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Cedarling Tutorials
-
+
Start here ·
@@ -9,104 +9,51 @@
Cedarling docs
-Learn to turn business rules into authorization policies with **Cedarling**.
-Explore fifteen runnable Node.js applications, most with React interfaces:
-task boards, AI assistants, shared documents, file sharing, and more.
-
-Each project gives you a working application, sample users or workloads, an
-exercise, and tests. Start with P1 to learn the core pattern, or choose an
-application that matches what you build. The projects run independently;
-you do not need to complete them in order.
+Practice authorization with fifteen independent applications, from a task board
+to AI assistants and machine-to-machine transfers. Each project has sample users
+or workloads, a setup guide, exercises, and tests. Start with P1 or pick a problem
+that matches your own application.
> [!NOTE]
-> P1–P5 include Cedarling authorization. P6–P15 retain the tutorial starting
+> P1-P5 include Cedarling authorization. P6-P15 retain the tutorial starting
> applications, whose marked authorization checks return `FAKE ALLOW`.
-> Use these applications locally with sample data, not as production deployments.
## Start with a task manager
-[P1](./p1-task-manager/) is a small React task board backed by a Node.js API.
-It introduces a practical question: **who can read or change a task?**
-
-With Git and Docker Desktop, or Docker Engine with Compose, installed:
-
-```bash
-git clone https://github.com/GluuFederation/cedarling-tutorials.git
-cd cedarling-tutorials/p1-task-manager
-docker compose up --build
-```
-
-Open ****. The stack starts both the application and its
-identity provider; you do not need to configure hostnames or install Node.js
-on your computer for this Docker quick start.
-
-1. Sign in as **Mina**, the tenant owner, and explore the task board.
-2. Compare access with **Alex**, a contributor, and **Sam**, a user from another tenant.
-3. Follow P1's [Exercise](./p1-task-manager/README.md#exercise) to identify the
- rules that Cedarling will enforce.
-
-To stop, press `Ctrl+C`, then run `docker compose down` in the same directory.
-This keeps the project's stored data for your next session.
-
-Prefer working directly with Node.js? Follow P1's
-[native instructions](./p1-task-manager/README.md#run).
+[Run P1](./p1-task-manager/README.md) to try a React task board whose API checks
+task permissions with Cedarling. To build the integration yourself, follow the
+[tutorial](./p1-task-manager/docs/tutorials.md) from its permissive starting
+application through server enforcement and browser guidance.
## Find your next project
-Choose a familiar application, then explore the authorization problem behind it.
-Each project title opens its own setup guide, architecture, exercise, and commands.
-The publication column will link to the written tutorial when it is published
-on [Cedarling.dev](https://cedarling.dev/learn).
-
-| Project | Stack | What you'll learn | Publication |
-| -------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------- |
-| [P1 - Protecting a Node.js REST API with Cedarling](./p1-task-manager/) | Node.js, Fastify, React, SQLite | Keep tenants' tasks separate and control who can read or change them. | Not published |
-| [P2 - Preventing Cross-Tenant RAG Data Leaks with Cedarling](./p2-tenantrag/) | Node.js, Fastify, Orama, Voyage, OpenRouter | Check access to search results before documents reach AI generation. | Not published |
-| [P3 - Authorizing MCP Incident Operations with Cedarling](./p3-mcp-capability-governance/) | Node.js, MCP, Express, OpenRouter | Control an assistant's access to incident tools, runbooks, and triage prompts. | Not published |
-| [P4 - Securing Editorial Publishing with Cedarling](./p4-editorial-publishing/) | Node.js, Next.js App Router, React, SQLite | Tie publishing approval to the reviewed revision and current reviewer authority. | Not published |
-| [P5 - Protecting Sensitive Fields and Data Exports with Cedarling](./p5-dataguard/) | Node.js, Hono, React, SQLite | Protect individual records, sensitive fields, aggregates, and data exports. | Not published |
-| [P6 - Reauthorizing Offline Field Inspections with Cedarling](./p6-field-inspection/) | Node.js, Fastify, React, SQLite, IndexedDB | Check current assignments before accepting work saved while offline. | Not published |
-| [P7 - Securing Real-Time Collaborative Documents with Cedarling](./p7-collaborative-docs/) | Node.js, Fastify, React, SQLite, SSE | Apply changing permissions to document edits, comments, sharing, and live updates. | Not published |
-| [P8 - Securing File Sharing and Blocking Path Traversal with Cedarling](./p8-cedarfile/) | Node.js, Express, React, SQLite | Combine file-access decisions with application-owned filesystem safeguards. | Not published |
-| [P9 - Securing Realtime Chat Rooms and Events with Cedarling](./p9-cedarrealtime/) | Node.js, Express, Socket.IO, React, SQLite | Recheck access when people join, reconnect, receive messages, or moderate a room. | Not published |
-| [P10 - Authorizing Warehouse Workloads with Cedarling](./p10-warehouse-workloads/) | Node.js, Fastify, React, SQLite, OAuth Client Credentials | Authorize machine-to-machine transfers using warehouse relationships and current state. | Not published |
-| [P11 - Securing Active-Tenant Switching in a SaaS Workspace with Cedarling](./p11-saas-workspace/) | Node.js, React Router Framework Mode, Express, PostgreSQL | Reevaluate access as users switch tenants, accept invitations, or receive support access. | Not published |
-| [P12 - Governing Employee Record Access with Cedarling](./p12-hr-access-governance/) | Node.js, Express, React, SQLite | Grant and revoke employee-record access while separating requesters from approvers. | Not published |
-| [P13 - Protecting Grade Publication and Guardian Access with Cedarling](./p13-student-records/) | Node.js, Express, React, SQLite | Separate grade editing, publication, student access, and guardian access. | Not published |
-| [P14 - Governing an AI Scheduling Assistant with Cedarling](./p14-ai-scheduling-assistant/) | Node.js, Fastify, React, SQLite | Authorize each scheduling action an assistant proposes before it changes anything. | Not published |
-| [P15 - Authorizing a Multi-Party Marketplace Refund with Cedarling](./p15-marketplace/) | Node.js, Express, React, SQLite | Give buyers, sellers, support, and fraud reviewers the right views and refund actions. | Not published |
+Project links open the setup guides; article links open the published tutorials.
+
+| Project | Stack | What you'll learn | Articles |
+| -------------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
+| [P1 - Protecting a Node.js REST API with Cedarling](./p1-task-manager/) | Node.js, Fastify, React, SQLite | Keep tenants' tasks separate and control who can read or change them. | [Read tutorial](https://cedarling.dev/learn/protect-a-nodejs-rest-api-with-cedarling) |
+| [P2 - Preventing Cross-Tenant RAG Data Leaks with Cedarling](./p2-tenantrag/) | Node.js, Fastify, Orama, Voyage, OpenRouter | Check access to search results before documents reach AI generation. | [Read tutorial](https://cedarling.dev/learn/prevent-cross-tenant-rag-data-leaks-with-cedarling) |
+| [P3 - Authorizing MCP Incident Operations with Cedarling](./p3-mcp-capability-governance/) | Node.js, MCP, Express, OpenRouter | Control an assistant's access to incident tools, runbooks, and triage prompts. | [Read tutorial](https://cedarling.dev/learn/govern-mcp-capabilities) |
+| [P4 - Securing Editorial Publishing with Cedarling](./p4-editorial-publishing/) | Node.js, Next.js App Router, React, SQLite | Tie publishing approval to the reviewed revision and current reviewer authority. | [Read tutorial](https://cedarling.dev/learn/secure-editorial-publishing) |
+| [P5 - Protecting Sensitive Fields and Data Exports with Cedarling](./p5-dataguard/) | Node.js, Hono, React, SQLite | Protect individual records, sensitive fields, aggregates, and data exports. | [Read tutorial](https://cedarling.dev/learn/protect-sensitive-data-exports) |
+| [P6 - Reauthorizing Offline Field Inspections with Cedarling](./p6-field-inspection/) | Node.js, Fastify, React, SQLite, IndexedDB | Check current assignments before accepting work saved while offline. | - |
+| [P7 - Securing Real-Time Collaborative Documents with Cedarling](./p7-collaborative-docs/) | Node.js, Fastify, React, SQLite, SSE | Apply changing permissions to document edits, comments, sharing, and live updates. | - |
+| [P8 - Securing File Sharing and Blocking Path Traversal with Cedarling](./p8-cedarfile/) | Node.js, Express, React, SQLite | Combine file-access decisions with application-owned filesystem safeguards. | - |
+| [P9 - Securing Realtime Chat Rooms and Events with Cedarling](./p9-cedarrealtime/) | Node.js, Express, Socket.IO, React, SQLite | Recheck access when people join, reconnect, receive messages, or moderate a room. | - |
+| [P10 - Authorizing Warehouse Workloads with Cedarling](./p10-warehouse-workloads/) | Node.js, Fastify, React, SQLite, OAuth Client Credentials | Authorize machine-to-machine transfers using warehouse relationships and current state. | - |
+| [P11 - Securing Active-Tenant Switching in a SaaS Workspace with Cedarling](./p11-saas-workspace/) | Node.js, React Router Framework Mode, Express, PostgreSQL | Reevaluate access as users switch tenants, accept invitations, or receive support access. | - |
+| [P12 - Governing Employee Record Access with Cedarling](./p12-hr-access-governance/) | Node.js, Express, React, SQLite | Grant and revoke employee-record access while separating requesters from approvers. | - |
+| [P13 - Protecting Grade Publication and Guardian Access with Cedarling](./p13-student-records/) | Node.js, Express, React, SQLite | Separate grade editing, publication, student access, and guardian access. | - |
+| [P14 - Governing an AI Scheduling Assistant with Cedarling](./p14-ai-scheduling-assistant/) | Node.js, Fastify, React, SQLite | Authorize each scheduling action an assistant proposes before it changes anything. | - |
+| [P15 - Authorizing a Multi-Party Marketplace Refund with Cedarling](./p15-marketplace/) | Node.js, Express, React, SQLite | Give buyers, sellers, support, and fraud reviewers the right views and refund actions. | - |
## Run the projects your way
-### With Docker
+Follow the chosen project's README for prerequisites and startup commands.
+Most support Docker and native Node.js development. P3 requires Docker for its
+Cedarling sidecar and runs the chat client on the host.
-All fifteen projects include a Compose stack. Read the chosen project's
-**Prerequisites**, enter its directory, then run `docker compose up --build`.
-P2 needs Voyage and OpenRouter credentials. P3's interactive chat runs in a
-host terminal and needs Node.js, pnpm, and an OpenRouter key even when its
-services run in Docker.
-
-P3's Compose stack also runs its private Cedarling sidecar alongside the MCP
-server and identity provider.
-
-### With Node.js
-
-Use **Node.js 24.21 or newer within 24.x** and **pnpm 10**. Follow the project's
-**Run** section for dependency installation, setup, and startup. P11 also needs
-PostgreSQL: use the Compose-managed default or your own local database.
-
-On macOS or Linux with nvm, run `nvm install` and `nvm use` from this repository;
-`.nvmrc` selects Node.js 24.21.0. Other Node.js managers can select the same
-version. Check `node --version` in the terminal used to run pnpm.
-
-Each project generates its private application configuration in `.env` and its
-identity-provider configuration in `.local/idp/.env`. Some development commands
-start both processes; others use a separate identity-provider terminal.
-
-### Local addresses and isolation
-
-The same `localhost` addresses work in native and Docker mode. Project **PN**
-uses application port **17000 + N** and identity-provider port **18000 + N**:
+Project `PN` uses application port `17000 + N` and identity-provider port `18000 + N`:
P1 uses `17001` / `18001`, and P15 uses `17015` / `18015`.
Each project README gives its exact URL; P3 exposes an MCP service rather than a web interface.
@@ -116,7 +63,7 @@ from shared source code, with separate registrations and cookie names.
These stacks assume a trusted local machine. Ports do not isolate browser
cookies, and a project's Docker application services share its IdP's network
-namespace. Never commit generated credentials or local data.
+namespace.
## Explore and verify the code
@@ -134,14 +81,11 @@ pnpm audit --audit-level low
```
`pnpm check` runs the package's quality checks. Also run `pnpm test:e2e` when
-listed separately in the project's **Verify** section.
-
-## Keep learning
+listed separately in the project's Verify section.
-- [Cedarling Playground](https://cedarling.dev/playground) — experiment in your browser.
-- [Cedarling Learn](https://cedarling.dev/learn) — read the published learning material.
-- [Cedarling documentation](https://docs.jans.io/stable/cedarling/) — explore configuration and reference guides.
-- [Cedarling source](https://github.com/JanssenProject/jans/tree/main/jans-cedarling) — explore the engine behind the tutorials.
+For more examples, visit [Cedarling Learn](https://cedarling.dev/learn).
+The [engine source](https://github.com/JanssenProject/jans/tree/main/jans-cedarling)
+is maintained in the Janssen Project repository.
## License
diff --git a/p1-task-manager/README.md b/p1-task-manager/README.md
index 192bcfd..09930bb 100644
--- a/p1-task-manager/README.md
+++ b/p1-task-manager/README.md
@@ -2,32 +2,34 @@

-P1 is a multi-tenant task manager showing how Cedarling centralizes task
-authorization inside a trusted Node.js API. Authentication, sessions, request
-integrity, validation, tenant-scoped lists, and optimistic concurrency remain
-application responsibilities.
+P1 is a multi-tenant task manager. Its Node.js API asks Cedarling before reading
+or changing a task. The browser evaluates the same policy release to hide
+unavailable controls; the server checks every protected request.
-The server enforces every protected task read and effect with Cedarling. The
-browser evaluates the same policy release to hide controls conservatively,
-while the server always makes the final decision.
+The application handles authentication, sessions, request integrity, validation,
+tenant-scoped lists, and optimistic concurrency. Follow the
+[tutorial](docs/tutorials.md) to add authorization to the [starting application](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p1-task-manager).
## Architecture
-```text
-Alex / Mina / Sam ── sign in ──→ Tutorial IdP
- │
- └── task request ──→ React UI → Node.js API (PEP)
- │ principal + action + task + context
- ▼
- Cedarling PDP
- │ │
- DENY ALLOW → Task service → SQLite
+```mermaid
+flowchart TD
+ accTitle: Task authorization in the completed application
+ accDescr: The API asks its embedded Cedarling instance before accessing tasks. Browser evaluation guides controls but cannot grant server permission.
+ IdP["Tutorial IdP"] -->|"Signed token in server session"| API["Fastify API: current user and task facts"]
+ UI["React task board"] -->|"Task request"| API
+ API --> PDP["Embedded Cedarling instance"]
+ PDP --> Check["API enforces decision"]
+ Check -->|"ALLOW"| Data["Read or write SQLite tasks"]
+ Check -->|"DENY or error"| Stop["No protected effect"]
+ API -->|"Safe facts, ceiling and policy archive"| Browser["Browser Cedarling: unsigned evaluation"]
+ Browser -->|"Available controls"| UI
```
## Prerequisites
-- Docker Desktop or Docker Engine with Compose, or
-- Node.js 24.21 or newer within 24.x and pnpm 10.
+- To run with Docker: Docker Desktop or Docker Engine with Compose.
+- For native development and checks: Node.js 24.21 or newer within 24.x and pnpm 10.
The commands work from PowerShell, macOS terminals, and Ubuntu shells.
@@ -56,24 +58,22 @@ with the registered application URL without resetting data.
For compiled startup, run `pnpm run setup`, `pnpm --dir ../shared/identity-provider build`,
and `pnpm build`. Keep `node --env-file=.local/idp/.env ../shared/identity-provider/dist/main.js`
running in another terminal, then run `pnpm start`.
-Setup, build, and development startup validate the readable `policy-store/` source and create the ignored
-`.local/policy-store.cjar` archive used by the Cedarling integration.
+Setup, build, and development startup validate the readable `policy-store/` source and create the ignored `.local/policy-store.cjar` archive used by the Cedarling integration.
After editing policies, restart `pnpm dev` or rebuild before `pnpm start`.
The policy store trusts only issuer `http://localhost:18001` and audience
`http://localhost:17001/api`; setup and startup reject different values.
## Exercise
-The business workflow is a shared task board:
+Compare the task board using these accounts:
-- **Alex** — Tenant A contributor who works assigned tasks.
-- **Mina** — Tenant A owner who creates, assigns, edits, and deletes tasks.
-- **Sam** — Tenant B external user who must remain isolated from Tenant A.
+- Alex can view and edit his assigned Tenant A task, but cannot create one.
+- Mina can create tasks in Tenant A and manage tasks she owns.
+- Sam can view and edit his own Tenant B task, but cannot access Tenant A tasks.
-Compare their lists and mutations, including a direct task URL. The current
-policy permits Alex to view and edit assigned Tenant A work, gives Mina the
-owner actions in Tenant A, and isolates Sam's Tenant B work. Browser state
-cannot grant an operation that the server denies.
+Try the [direct API request](docs/tutorials.md#create-a-task-as-alex)
+as well as the visible controls. A hidden button alone does not prove that
+the server enforces permission.
## Commands
diff --git a/p1-task-manager/docs/tutorials.md b/p1-task-manager/docs/tutorials.md
index d3e9439..c335ac0 100644
--- a/p1-task-manager/docs/tutorials.md
+++ b/p1-task-manager/docs/tutorials.md
@@ -10,71 +10,91 @@ lastVerified: 2026-10-01T09:46:20Z
# Protect a Node.js REST API with Cedarling
+Hello! In this tutorial, we'll add Cedarling to a task manager built with React
+and Node.js. The app already handles sign-in. Our next step is to check what
+each user is allowed to do.
+
+Alex needs to edit his assigned tasks. Creating work for the team is Mina's job.
+Our starting task manager lets Alex do both, even though he has only a
+contributor role. We'll reproduce that creation request, then block it while
+keeping his assigned-task edits working.
+
+We'll also keep each tenant's tasks separate and check who owns or is assigned
+to a task before allowing reads and edits. Creation, assignment, completion, and
+deletion have additional role, ownership, or assurance requirements. A new
+assignee must belong to the same tenant. The API will check these rules using
+validated access tokens and current database facts. Browser decisions will guide
+the controls; direct API requests will face the same server checks.
+
+## Build the integration or try the finished app
+
+- To build the integration, start with [Start the baseline app](#start-the-baseline-app). We'll add policies and server enforcement, then browser controls.
+- To try the finished app, clone and run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p1-task-manager-v1.0.1/p1-task-manager) using its README, then go to [Check allowed and denied operations](#check-allowed-and-denied-operations). This version should already deny the operation we'll reproduce in the baseline.
+
-Project source and prerequisites
+What you'll need
-- [Complete P1 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p1-task-manager-v1.0.0/p1-task-manager) and [starting checkpoint](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p1-task-manager).
-- Install Docker with Compose, or Node.js 24.21+ within 24.x and pnpm 10.17.1. The project supplies its own tutorial identity provider.
-- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta.
-- I prepared these steps on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
+- For the coding steps: Git, Node.js 24.21+ within 24.x, and pnpm 10.17.1. The project supplies its own tutorial identity provider.
+- Docker with Compose is optional for running the baseline or finished example. Use native Node.js while working through the coding steps.
+- Familiarity with basic TypeScript, HTTP requests, sessions, and access tokens.
- New to Cedarling? [Read the short introduction](https://cedarling.dev/learn/what-is-cedarling) when you need it.
-- Keep the official [Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and [Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html) references handy for the policy-store steps.
-Paths to application code below are relative to `p1-task-manager/`.
+Copy whole files from GitHub's raw-file view; the short examples aren't complete
+replacements. Create missing parent directories. Code paths and commands are
+relative to `p1-task-manager/`; repository-level `shared/` files go one directory
+above it.
-## What are we going to secure?
+## Meet the task manager and its users
-Signing in tells a task manager who you are. It does not answer whether you may
-create work, edit another person's task, or read another organization's data.
-I'll show how to describe those decisions in Cedar policies and enforce them
-with Cedarling.
+The application uses React, a Fastify Node.js API, and SQLite. Its bundled IdP
+uses the Node.js `oidc-provider` package for sign-in and issuing tokens. After
+verifying the OIDC login, the API uses the token issuer and user identifier
+(subject) to find a local user. SQLite supplies that user's current role,
+tenant, and assurance level; those facts do not come from browser input or
+token claims.[^1]
-The application uses React, a Fastify Node.js API, and SQLite:
+Assurance expresses confidence in how a user authenticated. In this lab, levels
+1 and 2 are preset database values so we can try an assurance rule.
+Signing in does not perform multi-factor authentication or prove that an
+assurance standard has been met. We'll use three sample accounts:
- **Alex** is a Tenant A contributor with assurance level 1. He works on assigned tasks.
-- **Mina** is a Tenant A owner with assurance level 2. She manages her team's work.
+- **Mina** has the Tenant A owner role and assurance level 2. She manages her team's work.
- **Sam** is a Tenant B external user with assurance level 1. His work must stay separate from Tenant A.

-_Meet the three users. A role or tenant label introduces a person; the current
-action and resource determine each authorization result._
-
-Assurance is a database fact in this tutorial. Selecting Mina demonstrates the
-rule; it is not a real multi-factor authentication ceremony.
-
-Our first problem: Alex can create a task even though creation belongs to an
-assured owner. We will stop that operation while preserving his ability to edit
-an assigned task.
-
-```text
-Tutorial IdP -- signed access token --> Node.js session
- |
-React -- request --> Fastify API (PEP) <-- current user/task -- SQLite
- |
- Cedarling instance (PDP) <-- policy store
- |
- ALLOW --> protected read/write
- DENY --> no protected effect
-
-API -- safe facts + decision ceiling + policy archive --> React
- Cedarling browser PDP --> visible controls
+_Each decision depends on the user's facts and the action and resource involved.
+A role or tenant label alone doesn't settle it._
+
+```mermaid
+flowchart TD
+ accTitle: Task authorization in the completed application
+ accDescr: The API asks its embedded Cedarling instance before accessing tasks. Browser evaluation guides controls but cannot grant server permission.
+ IdP["Tutorial IdP"] -->|"Signed token in server session"| API["Fastify API: current user and task facts"]
+ UI["React task board"] -->|"Task request"| API
+ API --> PDP["Embedded Cedarling instance"]
+ PDP --> Check["API enforces decision"]
+ Check -->|"ALLOW"| Data["Read or write SQLite tasks"]
+ Check -->|"DENY or error"| Stop["No protected effect"]
+ API -->|"Safe facts, ceiling and policy archive"| Browser["Browser Cedarling: unsigned evaluation"]
+ Browser -->|"Available controls"| UI
```
-The **PDP** decides; the **PEP** enforces. Cedarling is the policy decision point.
-The API handlers enforce its decisions. The browser evaluates presentation rules,
-but the server decides again before releasing data or changing a task.
+Cedarling is the policy decision point (PDP). The API handlers are the policy
+enforcement points (PEPs): they act on Cedarling's decisions. The browser also
+evaluates rules to show the right controls, but the server checks permission
+again before releasing data or changing a task.
-## See what happens without authorization
+## Try the app before adding Cedarling

-_The illustration shows the starting application's gap. The request in this
-section provides the actual baseline evidence._
+_Alex's creation request succeeds in the starting app. We'll reproduce it below._
-### Run the starting application
+### Start the baseline app
Use a separate checkout so the exercise does not change an existing database:
@@ -91,8 +111,7 @@ With Docker, start the application and its own identity provider:
docker compose up --build
```
-Alternatively, use Node.js 24.21 or newer within 24.x and pnpm 10.17.1.
-At this starting commit, native startup uses two terminals. In the first:
+For Node.js startup at this commit, use two terminals. In the first:
```bash
pnpm --dir ../shared/identity-provider install --frozen-lockfile
@@ -113,7 +132,7 @@ Use one startup method at a time. Sign in as **Alex**. The development IdP
usually prefills `alex`; enter it if the field is empty. Use any non-empty
password, such as `cedarling-is-awesome`, then approve access.
-### Show the missing authorization check
+### Create a task as Alex
Open developer tools on the task manager page and run this in the console:
@@ -134,63 +153,63 @@ console.log(response.status, await response.json());
```
The application returns **201** and saves the task. Reload to see it.
-Authentication and CSRF protection worked: this really is Alex's session. The
-missing decision is whether Alex may create work.
+Authentication and CSRF checks passed for Alex's session, but nothing checked
+whether he may create a task.
Keep a capture of the response and saved task. Stop the application before
-changing code. For Docker, use `Ctrl+C`, then `docker compose down`; this keeps
-its data volume.
+changing code. For native execution, stop its second terminal but keep the IdP
+running. For Docker, use `Ctrl+C`, then `docker compose down`; this keeps
+its data volume. The coding steps below use native startup;
+if you started with Docker, install dependencies and start the native IdP using
+the first-terminal commands above before continuing.
-## Prepare the existing application for authorization
+## Where should we check permission?
-No separate feature needs adding before Cedarling. The starting application
-already authenticates the session, checks CSRF and input, and uses task versions
-to reject stale writes. Locate the point where its Create route goes straight
-from those checks to a database write:[^6]
+Open the baseline's
+[`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p1-task-manager/src/server/app.ts)
+and find `POST /api/tasks`. It authenticates the session, checks request
+integrity, validates the input, then calls `database.createTask()`. None of
+those checks asks whether Alex is allowed to create a task.
-```ts
-// src/server/app.ts (starting checkpoint)
-const task = database.createTask(
- session.user,
- parsed.data.title,
- parsed.data.description,
-);
-```
+We'll ask Cedarling before that database write. It will evaluate
+`Create` for the current user and tenant; the route must wait for `ALLOW`
+before saving the task. The existing `task.create` label names the operation;
+it doesn't make a permission decision.
-The route's capability metadata names `task.create`; it does not authorize the
-write. The baseline's `FAKE ALLOW` list trace is diagnostic, not a decision for
-Create. Make no source change yet: keep the existing safeguards, then add a
-fresh Cedarling decision before each protected read or effect in the steps
-below.
+Keep authentication, CSRF, input validation, and stale-write checks alongside
+authorization. Before editing the route, we'll define the
+permissions for each operation.
-## Who should be allowed to do what?
+## Decide who can do what

-_Create targets the existing tenant; Edit targets a current task.[^1] The policy
-store defines the vocabulary and rules used for both decisions._
+_`Create` targets the existing tenant; `Edit` targets a current task.[^2] The policy
+store defines the request types and rules used for both decisions._
-### Turn the business rules into capabilities
+### List the rules for each task action
Every operation requires the user's current tenant to match the resource tenant.
-For existing tasks, “related” means the user owns the task or is its assignee.
+For existing tasks, "related" means the user owns the task or is its assignee.
-| Capability | Action | Resource | Additional conditions |
-| --------------- | ---------- | -------- | ------------------------------------------------------------------------- |
-| `task.view` | `View` | Task | Related user |
-| `task.create` | `Create` | Tenant | Owner role, assurance at least 2 |
-| `task.edit` | `Edit` | Task | Related user |
-| `task.assign` | `Assign` | Task | Task owner, owner role, assurance at least 2, assignee in the same tenant |
-| `task.complete` | `Complete` | Task | Related user, owner role, assurance at least 2 |
-| `task.delete` | `Delete` | Task | Task owner, owner role, assurance at least 2 |
+| Capability | Action | Resource | Additional conditions |
+| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------- |
+| `task.view` | [`View`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L8 "server-view-related-task") | `Task` | Related user |
+| `task.create` | [`Create`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L33 "server-owner-create-task") | `Tenant` | Owner role, assurance at least 2 |
+| `task.edit` | [`Edit`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L58 "server-edit-related-task") | `Task` | Related user |
+| `task.assign` | [`Assign`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L83 "server-owner-assign-task") | `Task` | Task owner, owner role, assurance at least 2, assignee in the same tenant |
+| `task.complete` | [`Complete`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L111 "server-owner-complete-related-task") | `Task` | Related user, owner role, assurance at least 2 |
+| `task.delete` | [`Delete`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar#L138 "server-owner-delete-task") | `Task` | Task owner, owner role, assurance at least 2 |
The complete action identifier is, for example, `Task::Action::"Create"`.
-Creation targets `Task::Tenant`: the tenant exists before the new task does.[^1]
-The rule combines the user's role with current tenant and task relationships.[^2]
+Creation targets `Task::Tenant`: the tenant exists before the new task does.[^2]
+The rule combines role, tenant, and task relationships.[^3]
+An owner role is a tenant role; a task owner is the user in the task's
+`ownerId`. These are different facts, and some operations require both.
-### Design the policy store
+### Create the policy-store files
-Create the readable source using Cedarling's
+Create the policy-store source using Cedarling's
[directory-based format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format):
```text
@@ -204,20 +223,24 @@ policy-store/
tutorial-idp.json
```
-Create the five files shown above.[^7] The Create example below is complete;
-the linked policy files contain the other five actions and the browser rules.
-
-`metadata.json` gives this store its stable ID, name, Cedar version, and policy
-version `1.0.0`. The version identifies a reviewed set of rules; the generated
-archive's SHA-256 identifies its exact bytes. `schema.cedarschema` declares the
-types of requests the policies may evaluate. The two policy files separate
-server enforcement from browser guidance within the same store. Current facts
-arrive in requests, so P1 needs no default entities, templates, or custom
-issuers.[^3]
+Create the five files above using the complete linked versions:
+[`metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/metadata.json),
+[`schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/schema.cedarschema),
+[`tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/trusted-issuers/tutorial-idp.json),
+[`server-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/server-access.cedar), and
+[`browser-shadow-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/policy-store/policies/browser-shadow-access.cedar).
+The examples below explain these files; do not append a second `Create` policy.
+
+`metadata.json` records the store's stable ID, name, Cedar version, and policy
+version `1.0.0`. The version identifies the reviewed rules; the generated
+archive's SHA-256 hash identifies the exact file. `schema.cedarschema` defines the
+request types the policies accept. The two policy files hold server and browser
+rules in the same store. P1 supplies current facts with each request, so it needs
+no default entities, templates, or custom issuers.[^4]
| Design question | P1 answer |
| ----------------------------------- | -------------------------------------------------------------------------------------- |
-| Where does identity come from? | Server-held signed access token; server-projected user in the browser |
+| Where does identity come from? | Signed access token on server; safe user fields sent to browser |
| Which token mapping is trusted? | `P1TaskManager::Access_token` from P1's IdP |
| Who is the browser principal? | `Task::User`: ID, tenant, role, assurance |
| Which task relationships matter? | `Task::Task`: `tenant_id`, `owner_id`, optional `assignee_id` |
@@ -226,14 +249,13 @@ issuers.[^3]
| What else must the schema describe? | Access-token attributes/tags, trusted-issuer URL, generated token context, six actions |
Server multi-issuer requests supply tokens rather than a principal entity.
-`Task::Any` is an empty schema type satisfying Cedar's action principal
-declaration. It is not another user record or an authorization role. Browser
-requests use `Task::User`. In the `Task` namespace, `Task` carries the current
-task's tenant, owner, and optional assignee; `Tenant` is the existing Create
-target. `RequestContext` declares the `boundary`, current user, optional
-proposed-assignee tenant, and Cedarling token context. The six `action`
-declarations pair each business operation with its valid resource type. For
-example, the schema makes Create a decision about a `Tenant`:
+`Task::Any` is an empty type for Cedar's action principal declaration, not another
+user record or role. Browser requests use `Task::User`.
+
+In the `Task` namespace, `RequestContext` declares the `boundary`, current user,
+optional proposed-assignee tenant, and Cedarling token context. Each of the six
+`action` declarations specifies its resource type. For example, `Create` uses
+a `Tenant`:
```cedar
// policy-store/schema.cedarschema
@@ -244,11 +266,11 @@ action "Create" appliesTo {
};
```
-The `P1TaskManager` namespace describes the trusted issuer and access-token
-entity that Cedarling builds from signed JWT evidence. The policy can then
-read the token's validated claims in `context.tokens`.
+The `P1TaskManager` namespace describes the trusted issuer and the access-token
+entity Cedarling builds from the signed JWT. Policies read its validated claims
+through `context.tokens`.
-Configure `trusted-issuers/tutorial-idp.json`:
+`trusted-issuers/tutorial-idp.json` describes the tokens Cedarling will trust:
```json
{
@@ -266,16 +288,19 @@ Configure `trusted-issuers/tutorial-idp.json`:
}
```
-This file names the IdP discovery endpoint, marks the access token trusted,
-maps it to `P1TaskManager::Access_token`, and requires the listed claims. OIDC
-already validates the ID token during authentication. Authorization uses
+This file sets the IdP discovery endpoint, trusts access tokens mapped to
+`P1TaskManager::Access_token`, and requires the listed claims. OIDC already
+validates the ID token during authentication. For authorization, we use
the access token intended for `http://localhost:17001/api`. Each server policy
-binds its `sub` to the current database user and requires the action's OAuth
-scope. Requesting scopes during login does not itself grant a business operation.
+matches its `sub` to the current database user and requires the action's OAuth
+scope. Requesting a scope during login doesn't grant permission to perform the
+operation; the policy must allow it too.
-### Write the Create rule
+### Set the conditions for `Create`
-In `policies/server-access.cedar`, the complete Create policy is:
+In `policies/server-access.cedar`, the `Create` policy matches the token's subject
+to the current database user and checks its audience and scope. It also requires
+a matching tenant, the owner role, and the required assurance level:
```cedar
// policy-store/policies/server-access.cedar
@@ -304,65 +329,68 @@ permit(
};
```
+For Alex's Tenant A creation request, the tenant check passes. His
+role is `contributor` and his assurance level is `1`, so both the owner and
+assurance conditions fail. Mina belongs to the same tenant with role `owner`
+and assurance level `2`, so she meets those conditions. Her request must also
+pass the token subject, audience, and scope checks above.
+
`p1taskmanager_access_token` is Cedarling's generated token-context key, not
-another namespace. Dynamic `sub` and `aud` claims are accessed as tags. Scope
+another namespace. Policies read dynamic `sub` and `aud` claims as tags. Scope
is a space-separated list of permissions. This rule accepts `task.create` as
-one complete entry—whether it is alone, first, middle, or last—and does not
+one complete entry, wherever it appears in that list. It doesn't
mistake `task.create.extra` for the same permission.
-`policies/server-access.cedar` contains one permit per protected API action.
-All six bind the signed token subject and API audience to trusted server facts,
-require that action's exact scope, and require a tenant match. Their remaining
-conditions follow the capability table: View and Edit allow the owner or
-assignee; Assign requires the assured owner and a same-tenant assignee;
-Complete requires an assured related owner; Delete requires the assured task
-owner. Each rule has a distinct `@id` so its contribution is visible in logs.
+`policies/server-access.cedar` contains one `permit` per protected API action.
+All six check the signed token's subject and API audience against trusted server
+facts, require the action's exact scope, and check the tenant match. The remaining
+conditions follow the capability table. Each rule has a distinct `@id` that
+identifies it in decision logs.
-How the browser policy file differs
+What changes in the browser policies?
`policies/browser-shadow-access.cedar` has the same six actions and resource
types, but its rules use `context.boundary == "browser"` and the safe
-`Task::User` principal projected by the server. They check tenant, role,
+`Task::User` principal built from user fields sent by the server. They check tenant, role,
assurance, and task relationships without reading JWTs. For example,
-`browser-owner-create-task` shows Create only to an assured owner in the
-selected tenant. The server-provided decision ceiling and a fresh server
-decision still control every protected API effect.
+`browser-owner-create-task` shows `Create` only to a user with the owner role
+and assurance of at least 2 in the selected tenant.
-No matching permit means DENY. See the
+No matching `permit` means `DENY`. See the
[Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html).
-### Design the requests and their enforcement points
+### Map each operation to a permission check
Each server request uses the authenticated session token and freshly loaded
database facts. The route selects the action; the browser cannot supply trusted
role, owner, or tenant values.
-| Boundary | Resource and context | Protected effect |
-| ---------------- | ------------------------------------------------------- | -------------------------------------------- |
-| List / detail | Current task and user | Return only allowed task data |
-| Create | Current user's tenant and user | Insert with server-selected owner and tenant |
-| Edit | Current task and user | Update fields at the expected version |
-| Assign | Task/user plus proposed assignee's database tenant | Change assignee at the expected version |
-| Complete | Current task and user | Complete at the expected version |
-| Delete | Current task and user | Delete at the expected version |
-| Browser controls | Projected user, versioned task/tenant, browser boundary | Show controls within the server ceiling |
+| Boundary | Resource and context | Protected effect |
+| -------------------- | ------------------------------------------------------------ | -------------------------------------------- |
+| `View` (list/detail) | Current task and user | Return only allowed task data |
+| `Create` | Current user's tenant and user | Insert with server-selected owner and tenant |
+| `Edit` | Current task and user | Update fields at the expected version |
+| `Assign` | Task/user plus proposed assignee's database tenant | Change assignee at the expected version |
+| `Complete` | Current task and user | Complete at the expected version |
+| `Delete` | Current task and user | Delete at the expected version |
+| Browser controls | Server-supplied user, task/tenant versions, browser boundary | Show controls within the server ceiling |
Denied task operations return the same **404** as an absent task. Denied creation
-returns **403**. Unavailable required authorization returns **503**, not ALLOW.
+returns **403**. If required authorization is unavailable, return **503**, never `ALLOW`.
CSRF, input validation, session refresh, and stale-write **409** checks stay in
the application.
-## Enforce the rules with Cedarling
+## Add Cedarling to the app

-_Browser guidance shapes the interface. The API keeps the signed token and
-current facts, asks Cedarling, and gates the protected effect._
+_Cedarling checks the signed token and current database facts before the API
+returns or changes task data._
-### Install and package the store
+### Install Cedarling and build the policy archive
From `p1-task-manager/`:
@@ -375,22 +403,34 @@ The Cedar package checks source syntax; the Cedarling SDK makes application
decisions. The examples follow the
[pinned SDK README](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468).
-Add the integration's repository-level `shared/policy-store.mjs` builder and
-`shared/policy-store.d.mts` declaration.[^8] These are not in the starting commit.
+Add the complete repository-level
+[`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/policy-store.mjs) builder and
+[`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/policy-store.d.mts) declaration.
The builder validates source paths, JSON, schema/policy syntax, and policy IDs,
-then creates the ignored ZIP-format `.local/policy-store.cjar`:
+then creates `.local/policy-store.cjar`, a ZIP-format archive ignored by Git:
```bash
node ../shared/policy-store.mjs
```
-Keep `policy-store/` readable in Git. Rebuild and restart after policy edits.
+The builder should create `.local/policy-store.cjar` and print
+`Built policy store 1.0.0 | sha256 ...` without validation errors.
+Keep `policy-store/` in Git. Rebuild and restart after policy edits.
-### Initialize once on the server
+### Create one Cedarling instance for the server
-In `src/server/authorization-trace.ts`, initialize Cedarling from the archive.
-This excerpt omits the surrounding artifact registration; the completed
-function resolves the archive relative to `options.projectRoot`:
+Replace these files with the complete linked versions:
+[`src/server/authorization-trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/authorization-trace.ts),
+[`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/app.ts),
+[`src/server/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/main.ts),
+[`src/server/config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/config.ts), and
+[`tsconfig.server.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/tsconfig.server.json).
+Add [`src/shared/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/shared/authorization.ts)
+and remove the replaced `src/server/capabilities.ts`. These files wire up all six
+server operations, including the imports, function calls, and response types.
+
+`createServerAuthorization()` loads the archive relative to `options.projectRoot`.
+It registers the archive and initializes Cedarling:
```ts
// src/server/authorization-trace.ts
@@ -416,13 +456,17 @@ const cedarling = await initFromArchiveBytes(
);
```
-The IdP must run first. Require `loadedTrustedIssuersCount()` to be at least one before serving requests. `src/server/main.ts` owns this instance, passes the authorization functions to `buildApp()`, and calls `shutDown()` on application closure. Strict schema validation makes an invalid loaded model a startup error.[^9]
+Start the IdP first. The initialization code requires `loadedTrustedIssuersCount()`
+to be at least one before serving requests. `src/server/main.ts` owns the instance,
+passes its authorization functions to `buildApp()`, and arranges for `shutDown()`
+when the app closes. With strict schema validation, an invalid model prevents startup.
-### Ask Cedarling for a signed decision before the write
+### Check permission before saving a task
-Inside the server authorization function, `session` is trusted server state.
-The following expanded Create request illustrates the shape produced by
-`tokenSet(session)` and `requestItem(session, target)` in the completed file:
+Inside `createServerAuthorization()`'s returned
+`authorize(requestId, session, target)` function, `session` is trusted server
+state and `target` describes the action and resource. This expanded `Create`
+request shows what `tokenSet(session)` and `requestItem(session, target)` produce:
```ts
// src/server/authorization-trace.ts
@@ -463,32 +507,96 @@ if (result.response.diagnostics.errors.length > 0) {
return result.decision;
```
-`authorizeMultiIssuer()` takes a JSON string, so `JSON.stringify()` serializes
-the request object into the format this Cedarling JavaScript method expects.[^4]
-The `JSON.stringify(log, null, 2)` call is different: it prints nested decision
-evidence legibly in this local lab. In production, send reviewed, access-
-controlled decision logs to centralized audit storage such as Jans Lock Server
-instead of relying on a console.
+`authorizeMultiIssuer()` expects a JSON string; `JSON.stringify()` converts the
+request object to that format.[^5] We use `JSON.stringify(log, null, 2)` for a
+different reason: it makes nested decision logs readable in the console. For
+production, review those logs and send them to access-controlled audit storage,
+such as Jans Lock Server, rather than relying on console output.
+
+The module also logs `authorization.context`, linking the Fastify request ID,
+user, capability, and Cedarling request ID. It leaves Cedarling's own records
+unchanged. Unexpected errors produce one `authorization.failed` record with
+limited fields, without raw error messages or tokens.
+
+In the updated `POST /api/tasks` handler, authentication, CSRF, and input checks
+come first. These lines in `src/server/app.ts` then wait for authorization
+before writing:
+
+```ts
+// src/server/app.ts
+if (
+ !(await authorize(
+ session,
+ { capability: capabilities.create, tenantId: session.user.tenantId },
+ reply,
+ 403,
+ ))
+)
+ return;
+const task = database.createTask(
+ session.user,
+ parsed.data.title,
+ parsed.data.description,
+);
+return reply.code(201).send({
+ task,
+});
+```
-The module also emits `authorization.context`, linking the Fastify request ID,
-actor, capability, and Cedarling request ID. Native Cedarling records remain
-unchanged. Unexpected errors produce one bounded `authorization.failed` record;
-arbitrary exception messages and raw tokens are not printed.
+This route's `authorize()` helper calls the Cedarling-backed authorization
+function above. It sends 403 for a denied decision or 503 for an exception,
+returning `undefined` in either case. The handler then exits before
+`database.createTask()`; only an allowed request reaches the write and its 201
+response. Existing-task routes reload the task and check their action before
+returning data or changing it.
-In `src/server/app.ts`, run this after authentication and validation, but before `database.createTask()`. The route helper maps false to 403 and an exception to 503. Existing-task routes reload their resource and use their own action before reading or mutating it. Replace permissive traces rather than leaving unguarded effects alongside Cedarling calls.
+List and control-preview checks use `authorizeMultiIssuerBatch()` with shared
+`tokens` and an `items` array of action/resource/context requests. The batch
+handler checks `item.is_ok` before `item.unwrap()` and rejects diagnostic errors.
+A successful item with `decision: false` is a valid denial, not a runtime error.
+The API matches results to submitted items in order and returns only allowed
+list rows.
-For lists and control previews, call `authorizeMultiIssuerBatch()` with shared
-`tokens` and an `items` array of action/resource/context requests. Check
-`item.is_ok` before `item.unwrap()` and reject diagnostic errors. A successful
-item with `decision: false` is a valid denial, not a runtime error. Associate
-results with submitted items in order and return only allowed list rows.
+### Test task creation as Alex and Mina
-### Add browser guidance without transferring authority
+Keep the native IdP running. In the application terminal, run:
-Return an authorization envelope beside task data: safe user projection, server decision ceiling, policy ID/version/SHA-256/archive URL, subject epoch, resource versions, evaluation time, and expiry. Serve the exact archive at `/policy-store/.cjar`. OAuth tokens and session IDs stay on the server.
+```bash
+pnpm exec vite build
+pnpm exec tsc -p tsconfig.server.json
+pnpm start
+```
-In `src/web/authorization-trace.ts`, fetch that same-origin archive and verify
-its digest before initialization:
+On startup, you should see the policy version and SHA-256. `/health` should
+return `{ "status": "ok" }`. Sign in as Alex, reload, and repeat the original
+`Create` request: expect **403**, with no new task. In a separate Mina session,
+the same request with a valid title returns **201**. The baseline-created
+task may still exist. We haven't changed the browser buttons yet, so they still
+offer operations the API now denies. We'll fix that next.
+
+### Show the actions each user can take
+
+Replace these browser files with the complete linked versions:
+[`src/web/authorization-trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/web/authorization-trace.ts),
+[`src/web/api.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/web/api.ts),
+[`src/web/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/web/types.ts),
+[`src/web/App.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/web/App.tsx), and
+[`tsconfig.web.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/tsconfig.web.json).
+The server returns a list of actions it currently allows: its **decision ceiling**.
+For Alex, that includes editing his assigned brief, but not creating a task.
+Browser Cedarling can remove actions from this list, but cannot add permission to create.
+
+An **authorization envelope** carries that list, task data, safe user fields,
+policy ID/version/SHA-256/archive URL, subject epoch, resource
+versions, evaluation time, and expiry. The exact archive is served at
+`/policy-store/.cjar`. OAuth tokens and session IDs stay on the server.
+
+The subject epoch is a value used to detect changes in the user or their
+permissions. A change in user or role requires the browser to refresh its controls.
+
+In `src/web/authorization-trace.ts`, the loader fetches the archive from the
+app's origin and checks its SHA-256 hash. It then passes the verified `bytes` to
+Cedarling:
```ts
// src/web/authorization-trace.ts
@@ -508,10 +616,10 @@ const cedarling = await initFromArchiveBytes(
Both JWT checks are disabled only in this unsigned browser instance. It receives
no JWTs and needs no IdP discovery; the server's validation remains enabled.
-Build `principal` as `Task::User` from `envelope.uiPrincipal`. Each item uses the
-shared action mapping, the projected resource, and
-`context: { boundary: "browser" }`. Assignment also supplies the projected
-assignee tenant. Evaluate them together:
+The browser builds `principal` as `Task::User` from user facts checked against
+`envelope.uiPrincipal`. Each item uses the shared action mapping, the resource
+fields sent by the server, and `context: { boundary: "browser" }`. Assignment
+also supplies the assignee's tenant from the server. The browser evaluates these requests together:
```ts
// src/web/authorization-trace.ts
@@ -520,82 +628,126 @@ const batch = await cedarling.authorizeUnsignedBatch(
);
```
-Check item success and diagnostics. A control is enabled only when **server
-ceiling AND browser decision** allow it. `App.tsx` consumes these results, not a
-parallel role-to-permission table.
+The browser checks each item's success and diagnostics. It enables a control
+only when both the server ceiling and browser decision allow it. `App.tsx` uses
+these results instead of maintaining a separate role-to-permission table.
-An expired envelope, changed subject, or mismatched version clears controls and
-causes a refresh. When browser evaluation fails with a current envelope, retain
-the server ceiling and warn. This is not a fabricated DENY; every attempted
-effect still requires a fresh server decision.
+The UI clears controls and refreshes when the envelope expires, the subject
+changes, or a version no longer matches. If browser evaluation fails while the
+envelope is current, it uses the server ceiling and logs a warning. An evaluation
+failure isn't a `DENY` decision. The server still checks every attempted
+operation again.
-The shared action names and envelope types belong in
-`src/shared/authorization.ts`; move their callers from the baseline's
-`src/server/capabilities.ts` and remove that old file. Connect the browser
-authorization function to the API client, response types, and React controls
-without adding another permission table.[^10]
+Stop only the API, then run:
-## Finish the runnable integration
+```bash
+pnpm exec tsc --noEmit -p tsconfig.web.json
+pnpm exec vite build
+pnpm start
+```
+
+After restarting, Alex should no longer see the **New task** button but should
+still be able to edit his assigned brief. Mina can create, and browser logs
+show unsigned Cedarling decisions. Alex's direct
+`Create` request still returns **403**.
-The decisions now guard the task manager. Complete its startup paths so each
-one builds and loads the same policy archive: call the shared builder during
-`scripts/setup.mjs` and before the application build, then include the archive
-in the Docker image.[^11] The build script in `package.json` is:
+## Update startup and Docker builds
+
+We still need startup and Docker builds to prepare the policy archive. Replace
+these files with the complete linked versions:
+[`scripts/setup.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/scripts/setup.mjs),
+[`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/scripts/dev.mjs), and
+[`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/Dockerfile), and update
+[`shared/dev-supervisor.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/dev-supervisor.mjs)
+at repository root. Both startup paths will use the same policy archive.
+Update only these two entries in
+[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/package.json)'s existing `scripts` object;
+retain the other scripts and installed dependencies:
```json
{
"scripts": {
- "build": "node ../shared/policy-store.mjs && vite build && tsc -p tsconfig.server.json"
+ "build": "node ../shared/policy-store.mjs && vite build && tsc -p tsconfig.server.json",
+ "dev": "node scripts/dev.mjs"
}
}
```
-Keep the configured issuer and API audience aligned with the policy store.
+Run `pnpm build` to produce the archive, browser bundle, and compiled server.
+Stop both native terminals before switching to the supervisor or Docker.
+Keep the configured issuer and API audience consistent with the policy store.
The completed `scripts/dev.mjs` prepares the project and starts its IdP,
browser watcher, and API together. With dependencies installed, run `pnpm dev`;
-Docker still uses `docker compose up --build`. The baseline's two-terminal
-instructions no longer apply to this completed development script.
+Docker still uses `docker compose up --build`. We no longer need the baseline's
+two-terminal startup.
+
+
+Troubleshooting startup and request errors
+
+- Occupied ports: stop the other P1 checkout or startup method; native and Docker use the same ports.
+- Issuer loading: start the IdP first at `http://localhost:18001`; keep the API audience at `http://localhost:17001/api`.
+- Policy validation: check the named source file, complete contents, and unique policy IDs before rebuilding.
+- Stale write (409): fetch the current task version instead of resending an old version.
+- Browser WASM or discovery errors: use the copied browser authorization and server files; only the unsigned browser instance disables JWT checks.
+
+
-## Verify that the right operations succeed
+## Check allowed and denied operations

-_These are the expected outcomes. Repeat the requests and inspect the real
-responses and Cedarling logs below to prove them._
+_Alex's creation request should fail. His assigned-task edit and Mina's creation
+should succeed. We'll check responses and logs._
-### Repeat the original request and a legitimate operation
+### Retry Alex's request, then edit his assigned task
+
+
+Reset the sample data in this checkout
+
+Stop the exercise services first. For native execution, run `pnpm reset`, then
+`pnpm dev` from this checkout's `p1-task-manager/`. Reset removes `.data`,
+including SQLite tasks, sessions, and stored policy artifacts; it preserves
+`.env` and `.local/idp/.env`. Startup recreates fixtures. Sign in again.
+
+For this checkout's Docker stack, run `docker compose down --volumes`, then
+`docker compose up --build`. This deletes its data and generated app/IdP
+configuration volumes. Do not use either reset against data you want to keep.
+
+
Use fresh exercise data for the integrated run. Sign in as Alex and repeat the
same console request used before integration. It returns **403** with
-`{"error":"forbidden"}`; reloading shows no new task. A hidden Create button alone
+`{"error":"forbidden"}`; reloading shows no new task. A hidden **New task** button alone
would not prove this: the direct request bypasses the interface.
Open **Prepare launch brief**, edit its title, and select **Save changes**. It
-succeeds because Alex is assigned this Tenant A task. Authorization must preserve
-legitimate work.
+succeeds because Alex is assigned this Tenant A task.
-On the original seed data, check the complete matrix:
+Can Alex also `Complete` that task? Compare its policy with `Edit` before
+checking the matrix.
-| Operation | Alex, Tenant A | Mina, Tenant A | Sam, Tenant B |
-| --------- | -------------------------------------------- | ------------------------------------------- | ------------------------------ |
-| View | ALLOW assigned brief; DENY unassigned review | ALLOW owned A tasks | ALLOW own B task; DENY A tasks |
-| Create | DENY | ALLOW in A | DENY |
-| Edit | ALLOW assigned brief | ALLOW owned A tasks | ALLOW own B task; DENY A tasks |
-| Assign | DENY | ALLOW owned task to A user; DENY B assignee | DENY |
-| Complete | DENY | ALLOW related A task | DENY |
-| Delete | DENY | ALLOW owned A task | DENY |
+With the original sample data, check every row in this table:
-Sign in separately as each person. State changes affect later observations:
-deleting a task makes it absent, while submitting an old version causes 409.
-Test domain conditions separately from policy denials.
+| Operation | Alex, Tenant A | Mina, Tenant A | Sam, Tenant B |
+| ---------- | ------------------------------------------------ | ----------------------------------------------- | ---------------------------------- |
+| `View` | `ALLOW` assigned brief; `DENY` unassigned review | `ALLOW` owned A tasks | `ALLOW` own B task; `DENY` A tasks |
+| `Create` | `DENY` | `ALLOW` in A | `DENY` |
+| `Edit` | `ALLOW` assigned brief | `ALLOW` owned A tasks | `ALLOW` own B task; `DENY` A tasks |
+| `Assign` | `DENY` | `ALLOW` owned task to A user; `DENY` B assignee | `DENY` |
+| `Complete` | `DENY` | `ALLOW` related A task | `DENY` |
+| `Delete` | `DENY` | `ALLOW` owned A task | `DENY` |
-### Explain the decision evidence
+Sign in separately as each person. Changes affect later requests: deleting a
+task makes it absent, and submitting an old version causes 409. Keep these
+workflow checks separate from permission denials.
-Server logs use formatted JSON so nested reasons are visible. The small
-application record's `cedarlingRequestId` matches a native `request_id`. One HTTP
+### Read the decision logs
+
+Formatted JSON lets us read nested reasons in the server logs. Match the
+application record's `cedarlingRequestId` to Cedarling's `request_id`. One HTTP
request can produce several decisions, each with its own Cedarling ID.
-The denied Alex creation contains these native fields:
+Alex's denied creation request produces these Cedarling log fields:
```json
{
@@ -608,31 +760,38 @@ The denied Alex creation contains these native fields:
}
```
-This is a selected-field excerpt, not a replacement log format. IDs and timestamps
-vary. `principal: []` is expected for this multi-issuer request: token evidence
-represents identity. This DENY has no determining policy: no permit matched and
-no forbid applied. The empty `reason` does not identify which condition failed.[^5]
-Compare the Create rule with trusted facts to explain Alex's missing owner role
-and assurance.
+We've shown only selected fields; keep the full log format. IDs and timestamps
+will vary. `principal: []` is expected because this multi-issuer request uses tokens
+as identity evidence. This `DENY` has no determining policy: no `permit` matched
+and no `forbid` applied. An empty `reason` doesn't tell us which condition
+failed.[^6]
-For the legitimate edit, expand `diagnostics.reason` and find the
+For the allowed edit, expand `diagnostics.reason` and find the
`server-edit-related-task` policy annotation. Policy-store ID/version identify
the rules; startup SHA-256 identifies their archive bytes. `batch_id` groups
decisions evaluated together.
Browser logs show a label such as
-`P1 browser | ALLOW | Task::Action::"Edit"` and an expandable native object.
-They prove browser evaluation, not server permission. A fallback warning means
+`P1 browser | ALLOW | Task::Action::"Edit"` and an expandable Cedarling log object.
+These record browser evaluation, not server permission. A fallback warning means
the UI used the current server ceiling instead.
-Record only synthetic evidence. Do not publish tokens, cookies, CSRF values, or
-session objects. Native logs contain identifiers, so review captures before use.
+Capture only sample exercise data. Don't publish tokens, cookies, CSRF values,
+or session objects. Cedarling logs contain identifiers too, so review captures
+before sharing them.
-### Prove stale and unavailable decisions stay safe
+### Test stale permissions and authorization failures
-Stop P1 services to free ports 17001 and 18001, then run:
+Run tests from the finished checkout: the baseline's tests expect operations to
+be allowed and aren't updated by copying runtime files. No new tests are needed
+for this exercise. Stop P1 services to free ports 17001 and
+18001, then use a separate directory for the finished example:
```bash
+git clone --branch p1-task-manager-v1.0.1 https://github.com/GluuFederation/cedarling-tutorials.git cedarling-p1-finished
+cd cedarling-p1-finished/p1-task-manager
+pnpm --dir ../shared/identity-provider install --frozen-lockfile
+pnpm install --frozen-lockfile
pnpm exec playwright install chromium
pnpm check
```
@@ -641,66 +800,71 @@ On Linux, missing browser libraries may require
`pnpm exec playwright install --with-deps chromium`. The check includes formatting,
lint, types, unit tests, build, and real browser/IdP verification with disposable data.
-- `test/policy-store.test.ts` evaluates actual Cedarling rules, including token,
- scope, tenant, and relationship failures. It checks readable, correlated logs
- without token payloads.
-- `test/app.test.ts` injects unavailable authorization and proves 503 responses
- preserve protected state. `test/browser-authorization.test.ts` checks stale
- envelopes and the current-server-ceiling fallback.
-- `e2e/task-authorization.e2e.ts` uses the real IdP and browser Cedarling
- instance, then repeats the unauthorized direct Create request.[^12]
+These tests exercise actual Cedarling policies for token, scope, tenant, and
+relationship failures. They check that logs stay readable and linked by request ID without
+token payloads, and that unavailable authorization returns 503 without changing
+protected data.
+
+Browser checks cover stale envelopes and fallback to a current server ceiling.
+They also repeat the unauthorized direct `Create` request with the real IdP and
+browser Cedarling instance.[^7]
-A runtime failure is not a policy denial. Keep its separate error event and
-fail closed on the server. Stopping only the IdP is not a reliable PDP-failure
-simulation: signing keys and valid tokens may already be cached.
+A runtime failure needs its own error event, distinct from a policy denial, and
+must stop the protected operation. Stopping the IdP alone won't reliably simulate
+a PDP failure because signing keys and valid tokens may already be cached.
-## Apply the pattern to your own application
+## Use the same checks in your own app

-_Use the same sequence around any effect your application must protect._
+_Use this sequence around reads and writes that need permission._
-Reuse this sequence: authenticate, load current facts, select a fixed business
-action, ask Cedarling, then gate the effect. Browser decisions improve the
-experience without becoming the authority over server data.
+Choose one operation in your own app, such as creating a task. Authenticate the
+user and load current facts, then ask Cedarling whether the operation may
+proceed. Put that check in the server route that returns or changes the data.
Follow `policy-store/`, the two `authorization-trace.ts` files, and the tests
-in the [completed P1 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p1-task-manager-v1.0.0/p1-task-manager)
-to trace each rule to a protected effect.
+in the [completed P1 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p1-task-manager-v1.0.1/p1-task-manager)
+to see where each decision controls a read or write.
+
+
+Warning: This setup is for local practice
+Local HTTP and the bundled IdP's sample passwords are for learning only.
Production needs real authentication and assurance, HTTPS, protected secrets,
-reviewed policy distribution, and appropriate log access/retention. This local
+reviewed policy delivery, and rules for who can read logs and how long to keep them. This local
development IdP and console output are not a production identity or audit system.
-For a production stack, consider Agama Lab Policy Designer for policy authoring,
-Jans Auth for token issuance, and Lock Server for centralized decision logs.
-See [Cedarling production solutions](https://cedarling.dev/solutions).
-
-Next, P2 applies the same approach to retrieval: authorize the corpus and
-candidate documents before loading protected text or sending it to an AI model.
-
----
+Use a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/),
+Gluu, Auth0, or Okta, rather than deploying the tutorial IdP.
-[^1]: Cedar recommends authorizing creation against an existing resource container because the new resource does not yet exist. Here, the tenant is that container. See [Cedar's resource-container guidance](https://docs.cedarpolicy.com/bestpractices/bp-resources-containers.html).
+These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI
+on macOS and Windows. If a platform-specific step fails,
+[open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
-[^2]: RBAC names role-based access control; ReBAC names relationship-based access control. P1's concrete rule also checks tenant and assurance attributes, so neither label alone describes its decision.
+For further reference, use the official
+[Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and
+[Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html).
-[^3]: [Default entities](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#default-entities) are shared across requests. P1 loads its changing users and tasks for each decision; it has no reusable policy slots or non-JWT issuer to configure.
+
-[^4]: The pinned [`cedarling_wasm` JavaScript API](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468) accepts a JSON-string request for token-based and unsigned authorization. Serialization does not make browser-supplied facts trustworthy.
+For a production stack, consider Agama Lab Policy Designer for policy authoring,
+Jans Auth for issuing tokens, and Lock Server for centralized decision logs.
+See [Cedarling production solutions](https://cedarling.dev/solutions).
-[^5]: Cedar returns an empty list of determining policies when no policy permits or forbids the request. A matched `forbid` would instead appear as a determining policy. See [How Cedar authorization works](https://docs.cedarpolicy.com/auth/authorization.html).
+Next, P2 applies the same approach to retrieval: authorize the corpus and
+candidate documents before loading protected text or sending it to an AI model.
-[^6]: Starting-checkpoint source: [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p1-task-manager/src/server/app.ts) contains the direct Create write; [`src/server/authorization-trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p1-task-manager/src/server/authorization-trace.ts) contains the fake list trace.
+[^1]: The bundled IdP's [`provider.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/identity-provider/src/provider.ts) configures sign-in and tokens; [`accounts.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/shared/identity-provider/src/accounts.ts) supplies identity claims. P1's login callback in [`app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/app.ts) maps the verified issuer and subject to a local user. [`database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/src/server/database.ts) seeds and loads the role, tenant, and assurance values used here.
-[^7]: Complete tagged policy-store source: [`metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/policy-store/metadata.json), [`schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/policy-store/schema.cedarschema), [`tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/policy-store/trusted-issuers/tutorial-idp.json), [`server-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/policy-store/policies/server-access.cedar), and [`browser-shadow-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/policy-store/policies/browser-shadow-access.cedar).
+[^2]: Cedar recommends authorizing creation against an existing resource container because the new resource does not yet exist. Here, the tenant is that container. See [Cedar's resource-container guidance](https://docs.cedarpolicy.com/bestpractices/bp-resources-containers.html).
-[^8]: Add the repository-level [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/shared/policy-store.mjs) and [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/shared/policy-store.d.mts), one level above `p1-task-manager/`.
+[^3]: RBAC means role-based access control; ReBAC means relationship-based access control. P1 also checks tenant and assurance attributes, so neither label alone describes its decision.
-[^9]: Complete server integration: [`src/server/authorization-trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/src/server/authorization-trace.ts), [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/src/server/app.ts), and [`src/server/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/src/server/main.ts).
+[^4]: [Default entities](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#default-entities) are shared across requests. P1 loads its changing users and tasks for each decision; it has no reusable policy slots or non-JWT issuer to configure.
-[^10]: Complete shared and browser integration: [`src/shared/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/src/shared/authorization.ts), [`src/web/authorization-trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/src/web/authorization-trace.ts), [`src/web/api.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/src/web/api.ts), [`src/web/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/src/web/types.ts), and [`src/web/App.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/src/web/App.tsx).
+[^5]: The pinned [`cedarling_wasm` JavaScript API](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468) accepts a JSON-string request for token-based and unsigned authorization. Converting to JSON does not make browser-supplied facts trustworthy.
-[^11]: Completed startup and packaging files: [`scripts/setup.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/scripts/setup.mjs), [`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/package.json), [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/Dockerfile), [`src/server/config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/src/server/config.ts), [`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/scripts/dev.mjs), and [`shared/dev-supervisor.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/shared/dev-supervisor.mjs).
+[^6]: Cedar returns an empty list of determining policies when no policy permits or forbids the request. A matched `forbid` would instead appear as a determining policy. See [How Cedar authorization works](https://docs.cedarpolicy.com/auth/authorization.html).
-[^12]: Full verification examples: [`test/policy-store.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/test/policy-store.test.ts), [`test/app.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/test/app.test.ts), [`test/browser-authorization.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/test/browser-authorization.test.ts), and [`e2e/task-authorization.e2e.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.0/p1-task-manager/e2e/task-authorization.e2e.ts).
+[^7]: Full verification examples: [`test/policy-store.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/test/policy-store.test.ts), [`test/app.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/test/app.test.ts), [`test/browser-authorization.test.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/test/browser-authorization.test.ts), and [`e2e/task-authorization.e2e.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p1-task-manager-v1.0.1/p1-task-manager/e2e/task-authorization.e2e.ts).
diff --git a/p1-task-manager/pnpm-lock.yaml b/p1-task-manager/pnpm-lock.yaml
index 22820e5..c5b61ce 100644
--- a/p1-task-manager/pnpm-lock.yaml
+++ b/p1-task-manager/pnpm-lock.yaml
@@ -1324,8 +1324,8 @@ packages:
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
@@ -2123,7 +2123,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
csstype@3.2.3: {}
@@ -2610,7 +2610,7 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
prelude-ls@1.2.1: {}
@@ -2700,7 +2700,7 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
split2@4.2.0: {}
diff --git a/p10-warehouse-workloads/pnpm-lock.yaml b/p10-warehouse-workloads/pnpm-lock.yaml
index a1a5fad..3c935c8 100644
--- a/p10-warehouse-workloads/pnpm-lock.yaml
+++ b/p10-warehouse-workloads/pnpm-lock.yaml
@@ -822,8 +822,8 @@ packages:
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
@@ -1333,7 +1333,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
optional: true
csstype@3.2.3: {}
@@ -1626,7 +1626,7 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
process-warning@4.0.1: {}
@@ -1704,7 +1704,7 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
split2@4.2.0: {}
diff --git a/p11-saas-workspace/pnpm-lock.yaml b/p11-saas-workspace/pnpm-lock.yaml
index a4bbb4e..7457ec2 100644
--- a/p11-saas-workspace/pnpm-lock.yaml
+++ b/p11-saas-workspace/pnpm-lock.yaml
@@ -1018,8 +1018,8 @@ packages:
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
engines: {node: '>=0.10.0'}
- proxy-addr@2.0.7:
- resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ proxy-addr@2.0.8:
+ resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
punycode@2.3.1:
@@ -1127,8 +1127,8 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
@@ -1853,7 +1853,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
optional: true
csstype@3.2.3: {}
@@ -1939,7 +1939,7 @@ snapshots:
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
- proxy-addr: 2.0.7
+ proxy-addr: 2.0.8
qs: 6.16.0
range-parser: 1.3.0
router: 2.2.0
@@ -2244,7 +2244,7 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
postgres-array@2.0.0: {}
@@ -2256,7 +2256,7 @@ snapshots:
dependencies:
xtend: 4.0.2
- proxy-addr@2.0.7:
+ proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
@@ -2401,7 +2401,7 @@ snapshots:
siginfo@2.0.0: {}
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
split2@4.2.0: {}
diff --git a/p12-hr-access-governance/pnpm-lock.yaml b/p12-hr-access-governance/pnpm-lock.yaml
index 771c5ca..3d3c249 100644
--- a/p12-hr-access-governance/pnpm-lock.yaml
+++ b/p12-hr-access-governance/pnpm-lock.yaml
@@ -742,8 +742,8 @@ packages:
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
engines: {node: ^10 || ^12 || >=14}
- proxy-addr@2.0.7:
- resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ proxy-addr@2.0.8:
+ resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
punycode@2.3.1:
@@ -824,8 +824,8 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
@@ -1313,7 +1313,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
csstype@3.2.3: {}
@@ -1387,7 +1387,7 @@ snapshots:
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
- proxy-addr: 2.0.7
+ proxy-addr: 2.0.8
qs: 6.16.0
range-parser: 1.3.0
router: 2.2.0
@@ -1618,9 +1618,9 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
- proxy-addr@2.0.7:
+ proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
@@ -1746,7 +1746,7 @@ snapshots:
siginfo@2.0.0: {}
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
stackback@0.0.2: {}
diff --git a/p13-student-records/pnpm-lock.yaml b/p13-student-records/pnpm-lock.yaml
index 05c06e3..350e4f6 100644
--- a/p13-student-records/pnpm-lock.yaml
+++ b/p13-student-records/pnpm-lock.yaml
@@ -739,8 +739,8 @@ packages:
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
engines: {node: ^10 || ^12 || >=14}
- proxy-addr@2.0.7:
- resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ proxy-addr@2.0.8:
+ resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
punycode@2.3.1:
@@ -821,8 +821,8 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
@@ -1310,7 +1310,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
csstype@3.2.3: {}
@@ -1384,7 +1384,7 @@ snapshots:
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
- proxy-addr: 2.0.7
+ proxy-addr: 2.0.8
qs: 6.16.0
range-parser: 1.3.0
router: 2.2.0
@@ -1615,9 +1615,9 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
- proxy-addr@2.0.7:
+ proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
@@ -1743,7 +1743,7 @@ snapshots:
siginfo@2.0.0: {}
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
stackback@0.0.2: {}
diff --git a/p14-ai-scheduling-assistant/pnpm-lock.yaml b/p14-ai-scheduling-assistant/pnpm-lock.yaml
index f2b6c73..1397cc7 100644
--- a/p14-ai-scheduling-assistant/pnpm-lock.yaml
+++ b/p14-ai-scheduling-assistant/pnpm-lock.yaml
@@ -721,8 +721,8 @@ packages:
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
@@ -1356,7 +1356,7 @@ snapshots:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
process-warning@4.0.1: {}
@@ -1426,7 +1426,7 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
split2@4.2.0: {}
diff --git a/p15-marketplace/pnpm-lock.yaml b/p15-marketplace/pnpm-lock.yaml
index d69a34e..ed1db05 100644
--- a/p15-marketplace/pnpm-lock.yaml
+++ b/p15-marketplace/pnpm-lock.yaml
@@ -821,8 +821,8 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
@@ -1310,7 +1310,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
csstype@3.2.3: {}
@@ -1615,7 +1615,7 @@ snapshots:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
proxy-addr@2.0.8:
dependencies:
@@ -1743,7 +1743,7 @@ snapshots:
siginfo@2.0.0: {}
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
stackback@0.0.2: {}
diff --git a/p2-tenantrag/README.md b/p2-tenantrag/README.md
index ccc6d58..8031d01 100644
--- a/p2-tenantrag/README.md
+++ b/p2-tenantrag/README.md
@@ -2,42 +2,40 @@

-P2 is a headless retrieval service that turns synthetic PDFs into answers. It
-shows how Cedarling centralizes authorization at corpus search and protected
-chunk access without mixing policy decisions with retrieval or generation.
+P2 is a headless retrieval service that answers questions using synthetic PDFs.
+Before searching, the server asks Cedarling whether the caller may use the
+selected corpus. It then checks each candidate document before loading its text
+for the model context.
-The server authorizes the selected corpus before remote retrieval work and
-authorizes every unique candidate document before loading protected text.
+Follow the [tutorial](docs/tutorials.md) to add these checks to the [starting
+service](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p2-tenantrag).
## Architecture
-```text
-Ada / Leo / Mallory ── Device Flow ──→ Tutorial IdP
- │
- └── retrieval request ──→ Node.js API (PEP)
- │
- ▼
- Cedarling: authorize corpus
- │ DENY → stop
- └ ALLOW → Voyage query → Orama candidates
- │
- ▼
- Cedarling: authorize documents
- │ DENY → filter
- └ ALLOW → load text → OpenRouter
-
- PDFs → PDF.js → Voyage embeddings → Orama index
+```mermaid
+flowchart TD
+ accTitle: Permission checks in the completed retrieval service
+ accDescr: The API enforces a corpus decision before search and document decisions before loading text for generation.
+ Caller["Caller: access token and question"] --> Corpus["API enforces Cedarling SearchCorpus"]
+ Corpus -->|"DENY"| Stop["Stop before search"]
+ Corpus -->|"ALLOW"| Search["Voyage query embedding and Orama search"]
+ PDFs["PDFs: PDF.js extraction and Voyage embedding"] --> Index["Orama index"]
+ Index --> Search
+ Search --> Documents["API enforces Cedarling RetrieveDocument batch"]
+ Documents -->|"DENY"| Filter["Exclude document text"]
+ Documents -->|"ALLOW"| Text["Load selected authorized text"]
+ Text --> Model["OpenRouter: generate answer with citations"]
```
## Prerequisites
-- Docker Desktop or Docker Engine with Compose, or Node.js 24.21 or newer within 24.x and pnpm 10.
+- Node.js 24.21 or newer within 24.x and pnpm 10, including for the sign-in CLI when running Docker.
+- Docker Desktop or Docker Engine with Compose if using the Docker startup path.
- Voyage AI and OpenRouter API keys.
## Run
Set `P2_VOYAGE_API_KEY` and `P2_OPENROUTER_API_KEY` in `.env` before preparing the corpus or starting Docker.
-Setup embeds synthetic PDF chunks with Voyage and checks OpenRouter generation; requests also consume provider quota. Do not submit private queries.
`P2_OPENROUTER_MODEL` defaults to `openrouter/free`. To select another free
model, set its OpenRouter model ID in `.env`. A model that may incur charges
also requires `P2_OPENROUTER_ALLOW_PAID=true`; otherwise requests retain a
@@ -46,8 +44,7 @@ the OpenRouter answer model does not require rebuilding the corpus.
Setup sends synthetic PDF chunks to Voyage for embedding and makes a small
OpenRouter generation check. During retrieval, Voyage receives your query;
-OpenRouter receives the query and up to three authorized chunks. Use synthetic
-queries, not private data. Provider calls consume account quota.
+OpenRouter receives the query and up to three authorized chunks. Provider calls consume account quota.
Start the application and its own IdP:
@@ -66,8 +63,8 @@ pnpm run setup
pnpm dev
```
-Run setup once to prepare the corpus, or explicitly repeat it when you want to
-rebuild it. `pnpm dev` refreshes local configuration and policies, starts this
+Run setup once to prepare the corpus; repeat it only to rebuild the corpus.
+`pnpm dev` refreshes local configuration and policies, starts this
project's IdP, and watches the API. Startup itself makes no AI-provider calls;
retrieval requests do. A missing corpus or provider key stops startup with an error.
@@ -90,11 +87,11 @@ running in another terminal, then run `pnpm start`.
## Exercise
-The business workflow answers support questions from tenant-owned documents:
+Use these accounts to compare access to the same support documents:
-- **Ada (`ada`)** — Tenant A analyst with confidential-document access.
-- **Leo (`leo`)** — Partner reviewer limited to Tenant A public evidence.
-- **Mallory (`mallory`)** — Tenant B analyst with no Tenant A access.
+- Ada (`ada`) is a Tenant A analyst with confidential-document access.
+- Leo (`leo`) is a partner reviewer limited to Tenant A public evidence.
+- Mallory (`mallory`) is a Tenant B analyst with no Tenant A access.
Run `pnpm auth ada`, complete sign-in, and copy the printed access token into
your HTTP client's bearer-token field. Send this request (replace ``):
@@ -111,16 +108,15 @@ Content-Type: application/json
}
```
-Repeat with `pnpm auth leo` and `pnpm auth mallory`. Ada can use Tenant A
+Repeat with `pnpm auth leo` or `pnpm auth mallory`. Ada can use Tenant A
public and confidential evidence, Leo receives only public Tenant A evidence,
and Mallory receives the same 404 status and error code for Tenant A as for an
unknown corpus. Response timing is not guaranteed to be identical.
The PDFs describe two fictional businesses: Aster (Tenant A) and Beacon
(Tenant B). Public documents are shared within their tenant, not across tenants.
-Inspect the response's `citations` and match its `requestId` to the server's
-JSON authorization context and retrieval summary; Cedarling's decision records
-include the matching policy reasons and evaluation errors.
+Inspect `citations` to see which evidence reached the answer. Match `requestId`
+to the server's JSON logs to follow the decisions and retrieval steps.
To test a whole-request denial, use Leo's token with the same POST URL and:
@@ -133,30 +129,26 @@ To test a whole-request denial, use Leo's token with the same POST URL and:
Expect **404 `corpus_not_found`**, before embedding, search, or generation.
Changing only `corpusId` to `tenant-a-support` permits Leo's search but excludes
-confidential documents. Naming Beacon in the question does not change the selected
-corpus or grant access to its documents. The model is instructed to acknowledge
+confidential documents. The model is instructed to acknowledge
insufficient evidence; its wording is not an authorization decision.
If no authorized candidate evidence remains, the response is **200** with
`answer: null` and `citations: []`. A **503 `retrieval_unavailable`** instead
-indicates a runtime or provider failure. Match its `requestId` to
-`retrieval.failed`: `stage` identifies the failed step; provider failures also
-include `provider`, `reason`, and any available `httpStatus` or numeric
-`providerCode`. Reasons distinguish HTTP rejection (`http_error`), a provider
-error envelope (`provider_error`), timeout, network error, invalid response,
-and empty answer. Keys, questions, evidence, and raw provider errors stay out of
-these logs. In successful summaries, `documentAuthorizationCount` means
-documents evaluated, not documents allowed.
+indicates a runtime or provider failure. Free-model availability varies; retry
+the request or check `retrieval.failed` for the failing stage and provider reason.
+The [log guide](docs/tutorials.md#read-the-retrieval-and-decision-logs) explains
+correlation and error categories. `documentAuthorizationCount` counts evaluated
+documents, including denials.
Document instructions cannot change the server's authorization inputs or corpus
filter. P2 controls evidence access; it does not filter generated answers or
guarantee that the model ignores instructions inside authorized evidence.
-The readable policy store is in `policy-store/`. Setup, build, and development startup validate it and build
-the ignored `.local/policy-store.cjar` loaded by Cedarling. P2 uses the
-`corpus.search` and `document.retrieve` scopes and direct multi-issuer SDK
-calls against the fixed tutorial issuer and P2 API audience; policy or runtime
-failures return `retrieval_unavailable` without releasing protected text.
+The policies are in `policy-store/`. Setup, build, and development startup
+validate them and build the ignored `.local/policy-store.cjar` loaded by
+Cedarling. Policy or runtime failures return `retrieval_unavailable` without
+releasing protected text. See the [integration steps](docs/tutorials.md#put-cedarling-in-the-retrieval-path)
+for token validation and request construction.
Restart `pnpm dev` or rebuild before `pnpm start` after changing policy source.
## Commands
diff --git a/p2-tenantrag/docs/tutorials.md b/p2-tenantrag/docs/tutorials.md
index d94e04c..5fb25ee 100644
--- a/p2-tenantrag/docs/tutorials.md
+++ b/p2-tenantrag/docs/tutorials.md
@@ -10,72 +10,80 @@ lastVerified: 2026-10-01T09:46:20Z
# Prevent Cross-Tenant RAG Leaks with Cedarling
+Hi there! We've got a service that searches documents and uses an AI model to
+answer questions. It serves people from different tenants, each with their own
+documents and access rules. We'll use Cedarling to check which documents each
+person may read.
+
+In the starting service, Mallory can ask about Tenant A's support records despite
+belonging to Tenant B. Even within Tenant A, Leo can retrieve confidential
+documents reserved for Ada.
+
+We'll close both gaps with Cedarling: check the caller's access to the selected
+document collection (corpus) before embedding or searching. Then we'll check each
+document's tenant, corpus, and permitted readers before loading text. We'll follow Mallory's
+request through that change and compare Ada's and Leo's results. By the end,
+denied text will stay out of model input and citations, while permitted
+documents remain available. The model must never decide access.
+
+## Build the integration or try the finished app
+
+- To build the integration, start with [Run the starting application](#run-the-starting-application), then add policies and retrieval checks.
+- To try the finished app, run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p2-tenantrag-v1.0.1/p2-tenantrag) using its README, then go to [Check which documents each user can retrieve](#check-which-documents-each-user-can-retrieve). This version already uses Cedarling.
+
-Project source and prerequisites
+What you'll need
-- [Complete P2 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p2-tenantrag-v1.0.0/p2-tenantrag) and [starting checkpoint](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p2-tenantrag).
-- Install Docker with Compose, or Node.js 24.21+ within 24.x and pnpm 10.17.1. Live retrieval also needs Voyage AI and OpenRouter API keys and consumes provider quota; use synthetic queries only.
-- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta.
-- I prepared these steps on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
+- Git, Node.js 24.21+ within 24.x, and pnpm 10.17.1 for the coding steps.
+- Docker with Compose is optional for the baseline or finished example; the intermediate coding steps use native Node.js.
+- Voyage AI and OpenRouter API keys for live retrieval. Indexing and requests consume provider quota; use fictional data only.
+- Familiarity with TypeScript, HTTP APIs, access tokens, and the basics of retrieval-augmented generation.
- New to Cedarling? [Read the short introduction](https://cedarling.dev/learn/what-is-cedarling) when you need it.
-- Keep the official [Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and [Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html) references handy for the policy-store steps.
+- Keep the [Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and [Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html) references handy while editing policies.
-Paths below are relative to `p2-tenantrag/` unless stated otherwise. Generated
-answers and retrieval rankings can vary; the authorization decisions and cited
-evidence are what we will verify.
+Copy whole files from GitHub's raw-file view into your baseline checkout; don't
+switch to the finished tag. The short examples aren't complete replacements.
+Create missing parent directories. Paths and commands are relative to
+`p2-tenantrag/`; repository-level `shared/` files go one directory above it.
-## What are we going to protect?
+## Meet the service and its users

-_Ada has a confidential-document grant, Leo can use public Tenant A evidence, and Mallory belongs to Tenant B._
-
-A support assistant can give a useful answer and still disclose information to
-the wrong person. The problem starts before the answer: which documents were
-sent to the model?
-
-I'll trace that evidence path with you, then put a decision before each place
-where protected text can enter it.
+_Ada can read a confidential document, Leo can use public Tenant A documents, and Mallory belongs to Tenant B._
-P2 is a Node.js API that searches synthetic PDFs and generates answers.
-Voyage creates embeddings, Orama searches the vector index, and OpenRouter
-provides generation. There is no browser application. We will protect two
-operations: searching a tenant's corpus and loading each candidate document.
+P2 is a Node.js API that searches fictional PDFs and generates answers.
+Voyage turns text into numeric vectors called embeddings. Orama searches those
+vectors, and OpenRouter provides the answer model. There is no browser application.
- **Ada** belongs to Tenant A and may read its public documents and the
confidential document explicitly shared with her.[^1]
- **Leo** belongs to Tenant A but may read only its public documents.
-- **Mallory** belongs to Tenant B and must not retrieve Tenant A's evidence.
-
-“Public” means public within that tenant, not available to every caller.
-
-```text
-Caller -- access token + question --> Node.js API (PEP)
- |
- current corpus facts --> Cedarling: SearchCorpus
- |
- ALLOW
- v
- Voyage query embedding
- |
- Orama candidate metadata
- |
- current document facts --> Cedarling: RetrieveDocument batch
- |
- keep allowed documents
- v
- load selected text
- |
- OpenRouter
- |
- answer + citations
+- **Mallory** belongs to Tenant B and must not retrieve Tenant A's documents.
+
+"Public" means public within that tenant, not available to every caller.
+The bundled Node.js `oidc-provider` handles sign-in and issues access tokens.
+The API verifies a token, then looks up the caller's tenant in its repository;
+the document metadata lists who may read confidential content.
+
+```mermaid
+flowchart TD
+ accTitle: Permission checks in the completed retrieval service
+ accDescr: The API enforces a corpus decision before search and document decisions before loading text for generation.
+ Caller["Caller: access token and question"] --> Corpus["API enforces Cedarling SearchCorpus"]
+ Corpus -->|"DENY"| Stop["Stop before search"]
+ Corpus -->|"ALLOW"| Search["Voyage query embedding and Orama search"]
+ Search --> Documents["API enforces Cedarling RetrieveDocument batch"]
+ Documents -->|"DENY"| Filter["Exclude document text"]
+ Documents -->|"ALLOW"| Text["Load selected authorized text"]
+ Text --> Model["OpenRouter: generate answer with citations"]
```
-Cedarling is the policy decision point, or PDP. The retrieval service is the
-policy enforcement point, or PEP: it controls whether execution reaches protected
-text and generation. The model does not—and must never—decide access.
+Cedarling is the policy decision point (PDP). The retrieval service enforces
+its decisions as the policy enforcement point (PEP). It checks both corpus and
+document access with the same embedded Cedarling instance.
## Reproduce the leak before adding Cedarling
@@ -83,7 +91,7 @@ text and generation. The model does not—and must never—decide access.
_In the baseline, authentication alone does not stop Mallory's cross-tenant retrieval._
-### Start an isolated baseline
+### Run the starting application
Use a separate checkout so the exercise does not change your existing data:
@@ -94,7 +102,6 @@ git switch --detach 21b0832be4b31271320df992d04e9d97667d0e38
cd p2-tenantrag
```
-Use Node.js 24.21 or newer within 24.x and pnpm 10.17.1 for host commands.
Put `P2_VOYAGE_API_KEY` and `P2_OPENROUTER_API_KEY` in the ignored project `.env`.
Keep their values out of source control and recordings.
@@ -119,12 +126,12 @@ time. The API is `http://localhost:17002`; the IdP is `http://localhost:18002`.
With Docker, also install the project's host dependencies for the authentication
CLI using `pnpm install --frozen-lockfile`.
-`pnpm run setup` builds the Orama corpus index from the synthetic PDFs as part
+`pnpm run setup` builds the Orama corpus index from the fictional PDFs as part
of native setup; the first Docker startup builds it automatically. No separate
`corpus:reset` is needed for a fresh checkout. Preparing the corpus sends PDF
chunks to Voyage, and native setup also checks generation. Retrieval calls
-consume provider quota. Use fictional
-questions: the embedding provider receives your question, and generation receives
+consume provider quota. Use fictional questions: the embedding provider receives
+your question, and generation receives
the question and selected evidence. The baseline does not yet filter that evidence
by the caller's permissions.
@@ -139,8 +146,8 @@ pnpm auth mallory
Open the displayed verification URL. The development IdP usually prefills
`mallory`; enter it if the field is empty. Use any non-empty password, such as
`cedarling-is-awesome`, then approve access. Copy the printed token into your
-HTTP client's bearer-token field. I use [Postman](https://learning.postman.com/docs/use/send-requests/create-requests/request-basics),
-but any client that can send HTTP requests works:
+HTTP client's bearer-token field. We'll use [Postman](https://learning.postman.com/docs/use/send-requests/create-requests/request-basics);
+any client that sends HTTP requests works:
```http
POST http://localhost:17002/v1/retrievals
@@ -154,42 +161,53 @@ Content-Type: application/json
}
```
-The baseline authenticates Mallory but permits the two retrieval boundaries. It
-can load Tenant A text and send it to the model for her. Inspect returned citations,
-not just the answer. The baseline `test/retrieval.test.ts` demonstrates the leak
-with fixed candidates and a test generation client, independently of providers.
+The baseline authenticates Mallory but doesn't check whether she may search
+Tenant A or read its documents. It can load their text and send it to the model
+for her. Inspect the returned citations as well as the answer.
+
+Capture the request and any Tenant A citations. A provider error proves neither
+a leak nor a denial; retry a failed generation request. Don't rebuild the corpus
+to retry a response: rebuilding consumes Voyage quota. Stop the baseline before
+editing. For Docker, use `Ctrl+C`, then `docker compose down` without removing
+its volume, and install the native dependencies above for the coding steps.
-Capture the request and any returned Tenant A citations. A provider error proves
-neither a leak nor a denial. Stop the baseline before applying the integration.
-For Docker, use `Ctrl+C`, then `docker compose down`; keep its data volume.
+## Give the server the document permissions it needs
-## Prepare trusted retrieval facts
+Open the baseline's
+[`src/rag/retrieval.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p2-tenantrag/src/rag/retrieval.ts).
+It looks up the corpus before embedding the question and loads document metadata
+before reading text. Those are the two places we'll ask Cedarling for a decision.
+The confidential-document rule also needs a list of permitted readers.
-The starting service already authenticates callers, resolves corpus metadata,
-searches candidate IDs, and loads document text. Its two marked authorization
-seams are before query embedding and before loading candidate text, but both
-currently omit per-caller authorization.[^3]
+Save these complete fact-source files before defining the rules:
-Before writing a rule about confidential readers, add the missing server-owned
-facts. Map Ada and Leo to Tenant A and Mallory to Tenant B; give only Ada a
-grant for the confidential Aster document. Carry that grant through the fixture
-type, PDF metadata, and repository without taking it from the HTTP request:[^4]
+- [`src/rag/fixtures.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/fixtures.ts)
+- [`src/rag/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/types.ts)
+- [`src/rag/pdf.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/pdf.ts)
+- [`src/rag/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/repository.ts)
+- [`src/rag/corpus.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/corpus.ts)
+- [`src/rag/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/setup.ts)
+
+These files add reader permissions to the index without changing the fictional
+PDF content. They map Ada and Leo to Tenant A and Mallory to Tenant B. Only Ada
+may read the confidential Aster document:
```ts
// src/rag/fixtures.ts (confidential Aster document)
confidentialReaderSubjects: ["ada"],
```
-Keep the existing corpus-indexing and provider setup. No document text should
-be loaded to determine a grant; the next steps authorize from metadata first.
+That permission comes from the server's sample data, never the HTTP request.
+It is available before loading document text. We'll rebuild the index once
+the integration files are in place so the new metadata reaches retrieval.
## Decide which evidence each caller may use

-_The corpus rule gates search; the document rule gates which candidate text can be loaded._
+_The corpus rule controls search; the document rule controls which text can be loaded._
-### Design a readable policy store
+### Create the policy store
Use the [directory-based policy-store format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format):
@@ -203,11 +221,17 @@ policy-store/
tutorial-idp.json
```
-Create these four source files from the completed policy store.[^5]
+Create the complete policy-store files from the pinned version:
+
+- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/metadata.json)
+- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/schema.cedarschema)
+- [`policy-store/policies/server-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/policies/server-access.cedar)
+- [`policy-store/trusted-issuers/tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/trusted-issuers/tutorial-idp.json)
+
The metadata identifies this store and version `1.0.0`. The schema defines the
-request vocabulary. Both rules live in one policy file with distinct `@id`
-annotations. Current document facts arrive in requests, so default entities and
-templates are unnecessary.
+request types. Both rules live in one policy file with distinct `@id`
+annotations. Current document facts arrive in requests, so we need no default
+entities or templates.
| Design question | P2 answer |
| -------------------------------------- | ----------------------------------------------------------------------------------------------- |
@@ -217,23 +241,23 @@ templates are unnecessary.
| What does search target? | `RAG::Corpus` with corpus and tenant IDs |
| What does retrieval target? | `RAG::Document` with corpus, tenant, classification, and confidential readers |
| Which context is required? | Server boundary, authenticated subject, current user tenant, selected corpus ID |
-| What permits access? | Required token binding and scope, matching tenant/corpus, and document access |
-| What must deny? | Another tenant, missing scope, wrong identity binding, or confidential evidence without a grant |
+| What permits access? | Matching token identity and scope, tenant/corpus match, and document permission |
+| What must deny? | Another tenant, missing scope, wrong token identity, or confidential content without permission |
The schema includes the token and issuer types Cedarling builds during JWT
processing. `RAG::Any` satisfies the action's principal-type declaration; it is
not another application user. Multi-issuer requests carry identity in `tokens`.
-In `trusted-issuers/tutorial-idp.json`, set `openid_configuration_endpoint` to
+The copied `trusted-issuers/tutorial-idp.json` sets `openid_configuration_endpoint` to
`http://localhost:18002/.well-known/openid-configuration`. The trusted
`access_token` mapping uses `entity_type_name: "P2TenantRAG::Access_token"`,
`token_id: "jti"`, and required claims `iss`, `sub`, `aud`, `jti`, `exp`, and
`scope`.
-### Write the document rule
+### Check the tenant and document's readers
-This complete policy from `policies/server-access.cedar` binds verified token
-claims to current application facts:
+This complete policy from `policies/server-access.cedar` checks verified token
+claims against current application facts:
```cedar
// policy-store/policies/server-access.cedar
@@ -263,30 +287,31 @@ permit(
};
```
-The context key `p2tenantrag_access_token` is generated by Cedarling. Dynamic
-`sub` and `aud` claims use tags; `scope` stays a space-delimited string. Whole-name
-matching prevents `document.retrieve.extra` from satisfying `document.retrieve`.
+Ada and Leo have valid P2 tokens and belong to Tenant A. For `a-confidential`,
+the tenant matches both users, but only Ada's subject appears in
+`confidential_reader_subjects`. Leo fails that last condition. Mallory's Tenant
+B account fails the tenant comparison even with a valid token.
-The companion `server-search-tenant-corpus` policy requires `corpus.search` with
-the same token binding and matching corpus/tenant. It does not grant every
+Cedarling generates the context key `p2tenantrag_access_token`. Dynamic `sub`
+and `aud` claims use tags; `scope` is a list of names separated by spaces. The rule looks
+for the complete scope name `document.retrieve`, so a different name such as
+`document.retrieve.extra` does not grant retrieval.
+
+The other policy, `server-search-tenant-corpus`, requires `corpus.search` with
+the same token checks and matching corpus/tenant. It does not grant access to every
document in that corpus. No matching permit gives DENY; evaluation errors are
handled as failures.
-This model combines a tenant boundary with a document-specific reader
-relationship. Membership in a tenant alone does not confer access to every
-confidential document.[^1]
-
-### Place each decision before its effect
+### Choose an action and resource for each check
-| Capability | Identity | Action | Resource | Context | Effect waiting for ALLOW |
-| ------------------- | ------------------- | ------------------ | ------------------------------ | ---------------------------------------------- | -------------------------------------- |
-| `corpus.search` | Caller access token | `SearchCorpus` | Resolved corpus | Current user, selected corpus, server boundary | Query embedding and vector search |
-| `document.retrieve` | Same token | `RetrieveDocument` | Each unique candidate document | Same trusted context | Load text for generation and citations |
+| Capability | Identity | Action | Resource | Context | Effect waiting for ALLOW |
+| ------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ---------------------------------------------- | -------------------------------------- |
+| `corpus.search` | Caller access token | [`SearchCorpus`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/policies/server-access.cedar#L2 "server-search-tenant-corpus") | Resolved corpus | Current user, selected corpus, server boundary | Query embedding and vector search |
+| `document.retrieve` | Same token | [`RetrieveDocument`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/policy-store/policies/server-access.cedar#L25 "server-retrieve-authorized-document") | Each unique candidate document | Same trusted context | Load text for generation and citations |
The route chooses actions; the repository supplies tenants, classification, and
-reader grants. Neither the question nor model output supplies trusted facts.
-Resolve candidate IDs and their relationships against repository metadata before
-building document requests.
+reader permissions. Neither the question nor model output supplies trusted facts.
+Look up search results by document ID in the repository before building requests.
## Put Cedarling in the retrieval path
@@ -294,7 +319,7 @@ building document requests.
_A corpus DENY stops retrieval; a document DENY filters that document from the batch._
-### Package and load the rules
+### Build the archive and load Cedarling
Install the pinned dependencies from the project directory:
@@ -303,22 +328,23 @@ pnpm add --save-exact @janssenproject/cedarling_wasm@0.0.468 fflate@0.8.3
pnpm add --save-dev --save-exact @cedar-policy/cedar-wasm@4.12.0
```
-Add the integration's repository-level `shared/policy-store.mjs` and
-`shared/policy-store.d.mts`, which are absent from the starting commit. Run the
-shared builder to create the archive:
+Save the repository-level archive builder and its declaration:
+
+- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/shared/policy-store.mjs)
+- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/shared/policy-store.d.mts)
+
+Run the shared builder to create the archive:
```bash
node ../shared/policy-store.mjs
```
-It validates source and generates ignored `.local/policy-store.cjar`. Docker
-uses the same archive source. This helper packages policies; authorization
-remains direct Cedarling calls.
+The command must finish without validation errors and create `.local/policy-store.cjar`.
+Docker builds an archive from the same source files.
-In `src/authorization.ts`, initialize one server instance following the
-[pinned SDK README](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468).
-Here `policyStorePath` comes from server configuration; this excerpt omits the
-surrounding lifecycle code:
+The complete `src/authorization.ts` in the next step initializes one
+[Cedarling](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468)
+instance. Here, `policyStorePath` comes from server configuration:
```ts
// src/authorization.ts
@@ -344,14 +370,27 @@ if (cedarling.loadedTrustedIssuersCount() < 1) {
}
```
-Start the IdP first. Log the archive version and SHA-256 at startup and call
-`shutDown()` on application closure. `src/runtime.ts` wires the authorization
-functions into the retrieval service.
+The IdP must be running before initialization. The complete module logs the
+archive version and SHA-256 at startup and calls `shutDown()` on application
+shutdown. `src/runtime.ts` supplies these authorization functions to retrieval.
-### Authorize the corpus
+### Check the corpus before searching
+
+Save these complete files together to connect the permission checks:
+
+- [`src/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/authorization.ts)
+- [`src/runtime.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/runtime.ts)
+- [`src/rag/retrieval.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/retrieval.ts)
+- [`src/rag/trace.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/rag/trace.ts)
+- [`src/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/app.ts)
+- [`src/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/main.ts)
+- [`src/config/project-config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/config/project-config.ts)
+- [`src/openapi.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/openapi.ts)
+- [`src/auth/cli.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/src/auth/cli.ts)
+- [`tsconfig.build.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/tsconfig.build.json)
Inside the corpus authorization function, `principal` is the authenticated
-caller, `profile` its repository access profile, and `corpus` the resolved record.
+caller, `profile` holds its stored permissions, and `corpus` is the stored record.
This expanded request shows what the completed `tokenSet()` and
`requestContext()` helpers provide:
@@ -394,15 +433,36 @@ the request object into the format expected by the Cedarling JavaScript API.[^2]
The later `JSON.stringify(log, null, 2)` only formats nested log fields for this
local exercise.
-In `src/rag/retrieval.ts`, call this before `voyage.embed()` and
-`corpusSearch.search()`. False becomes `corpus_not_found`, like an unknown
-corpus. An unavailable decision stops work with `retrieval_unavailable`.
+In `src/rag/retrieval.ts`, the corpus check runs before query embedding:
-### Batch document decisions before loading text
+```ts
+// src/rag/retrieval.ts
+if (
+ !(await dependencies.authorization.authorizeCorpus(
+ requestId,
+ principal,
+ profile,
+ corpus,
+ ))
+) {
+ throw corpusNotFound();
+}
+stage = "query.embedding";
+const [queryEmbedding] = await dependencies.voyage.embed(
+ [request.query],
+ "query",
+);
+```
+
+A false decision throws `corpus_not_found`, the same error as an unknown corpus.
+If authorization throws an error, the surrounding handler returns `retrieval_unavailable`.
+Neither case reaches `voyage.embed()` or the following `corpusSearch.search()`.
+
+### Check each document before loading its text
-Deduplicate resolved candidate documents. In the document authorization function,
-reuse the same token set and context and submit one item per document. This
-excerpt expands the request assembled in `src/authorization.ts`:
+The retrieval service removes duplicate documents from the search results. Its document check
+reuses the token set and context, submitting one item per document. In
+`src/authorization.ts`, the expanded batch request looks like this:
```ts
// src/authorization.ts
@@ -427,28 +487,57 @@ const batch = await cedarling.authorizeMultiIssuerBatch(
);
```
-Require `item.is_ok` before `item.unwrap()`, reject diagnostics errors, and collect
-actual decisions in submitted order. Print native logs by each result's request
-ID. A successful item can contain DENY. An incomplete batch is a failure, not
-permission for unchecked documents.
+The module checks `item.is_ok` before `item.unwrap()`, rejects diagnostic errors,
+and collects decisions in submitted order. It prints Cedarling's logs using each
+result's request ID. A successfully evaluated item can still contain DENY.
-The retrieval service forms an allowed-document set, filters candidates, and
-selects at most the requested three chunks. Only then does it call
-`repository.loadChunkText()` and generation. Build citations from those same
-chunks, not all search candidates.[^6]
+The service checks that every document received a decision, then loads text only
+from the allowed set:
-Keep the query bounds, server-selected corpus filter, input validation, and
-provider timeouts. The integration also refreshes the synthetic PDFs; after
-changing PDFs, rebuild the index with `pnpm corpus:reset` and restart. Rebuilding
-consumes Voyage quota; do not rerun setup or reset just to retry a model response.
+```ts
+// src/rag/retrieval.ts
+if (decisions.length !== uniqueDocuments.length)
+ throw new Error("Cedarling returned an incomplete document batch");
+const allowedDocuments = new Set(
+ uniqueDocuments
+ .filter((_document, index) => decisions[index])
+ .map((document) => document.documentId),
+);
-Restart after policy edits.
+const selected = candidates
+ .filter((candidate) => allowedDocuments.has(candidate.documentId))
+ .slice(0, Math.min(request.limit, 3));
+stage = "content.load";
+const chunks = selected.map((candidate) =>
+ dependencies.repository.loadChunkText(candidate.chunkId),
+);
+```
+
+Generation and citations use those same chunks. Denied documents cannot reach
+either; an incomplete batch stops retrieval before any text loads.
+
+Query bounds, the server-selected corpus filter, input validation, and provider
+timeouts remain in place. Restart after policy edits; metadata or PDF changes
+also require rebuilding the index, as in the next step.
+
+## Rebuild the index and restart the app
-## Finish the runnable retrieval stack
+Save the runtime preparation and packaging files:
-With both authorization gates in place, connect archive creation to every
-startup path. The completed build runs the shared builder before TypeScript;
-Docker copies the generated archive into the API image.[^7]
+- [`scripts/prepare.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/scripts/prepare.ts)
+- [`scripts/preflight.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/scripts/preflight.ts)
+- [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/scripts/setup.ts)
+- [`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/scripts/dev.mjs)
+- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/Dockerfile)
+- [`compose.yaml`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/compose.yaml)
+- [`shared/dev-supervisor.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/shared/dev-supervisor.mjs)
+
+Apply the `build` entry shown below to your existing
+[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.1/p2-tenantrag/package.json), keeping
+the other dependencies and scripts. Also set `dev` to `node scripts/dev.mjs`.
+
+The build runs the shared archive builder before TypeScript; the copied
+Dockerfile includes that archive in the API image:
```json
{
@@ -458,25 +547,44 @@ Docker copies the generated archive into the API image.[^7]
}
```
-Separate cheap preparation from provider-backed corpus setup: `scripts/prepare.ts`
-synchronizes the local IdP/configuration and builds policies without using AI
-quota. `scripts/preflight.ts` refuses startup if the corpus index is missing;
-`scripts/dev.mjs` prepares configuration, then starts the IdP and API. It does
-not rebuild the index. Run `pnpm run setup` once to build the corpus; it also
-makes a provider smoke request. Then run `pnpm dev`. Repeat setup or
-`pnpm corpus:reset` only when the PDF/index needs rebuilding; both consume
-Voyage quota.[^8]
+`scripts/prepare.ts` synchronizes the local IdP configuration and builds policies
+without using AI quota. `scripts/preflight.ts` refuses startup if the index is
+missing. The new `scripts/dev.mjs` starts both the IdP and API without rebuilding
+the index, replacing the baseline's two-terminal native startup.
-The baseline's two-terminal native startup no longer applies to the completed
-development script.
+For this baseline checkout, rebuild the index once to include the reader grants:
-## Prove the evidence boundary, not the model's wording
+```bash
+pnpm corpus:reset
+pnpm build
+pnpm dev
+```
+
+`corpus:reset` consumes Voyage quota; the baseline index cannot be reused
+unchanged. Stop the separate IdP terminal before `pnpm dev`, which now owns both
+services. Wait for the API at `http://localhost:17002` before testing.
+
+In a fresh finished checkout, use `pnpm run setup` instead: it builds the corpus
+and sends a small request to check the provider. Repeat setup or `pnpm corpus:reset` only
+when the PDF/index needs rebuilding.
+
+## Check which documents each user can retrieve

-_An allowed corpus search does not grant every document inside that corpus._
+_Permission to search a corpus does not grant access to every document in it._
+
+### Retry Mallory's request, then compare Ada and Leo
-### Repeat the denied request and legitimate work
+Authenticate as Mallory again to get a fresh token:
+
+```bash
+pnpm auth mallory
+```
+
+Complete sign-in and replace the bearer token in your HTTP client. P2 tokens
+expire after 30 minutes, so the one from the baseline exercise may now return
+`401 authentication_required` before Cedarling evaluates the request.
Repeat Mallory's original request for `tenant-a-support`. Expect **404** with
`error: "corpus_not_found"`, before embedding, search, or generation. An unknown
@@ -496,10 +604,13 @@ Content-Type: application/json
}
```
-The integrated PDFs describe Aster in Tenant A and Beacon in Tenant B. Ada may
-use Aster's confidential evidence; Leo may not. Ranking determines candidates,
-so inspect actual document decisions and citations rather than assuming one
-fixed answer or candidate order.
+The PDFs describe Aster in Tenant A and Beacon in Tenant B. Ada may read Aster's
+confidential documents; Leo may not. Search results vary, so inspect document
+decisions and citations rather than expecting a fixed answer or result order.
+
+If Cedarling denies Leo access to a confidential document, must the whole
+request fail? Check where the retrieval code filters documents, then compare
+the outcomes below.
| Attempt | Expected outcome |
| ---------------------------------------- | ------------------------------------------------- |
@@ -509,11 +620,11 @@ fixed answer or candidate order.
| Leo retrieves Tenant A public candidates | ALLOW; generation may use those chunks |
| Leo selects `tenant-b-support` | Corpus DENY, regardless of the question's wording |
-A denied document does not deny the entire question. With no authorized candidate
-chunks, expect **200**, `answer: null`, and `citations: []`, without generation.
+A denied document does not deny the entire question. With no allowed chunks,
+expect **200**, `answer: null`, and `citations: []`, without generation.
A **503 `retrieval_unavailable`** is instead a runtime or provider failure.
With the free OpenRouter model router, availability and answer quality vary;
-retry a 503 before treating it as an application defect. A terse answer such as
+retry a 503 before treating it as an application bug. A short answer such as
`"User Safety: safe"` is not the expected business answer and is not evidence
of authorization. Judge access by Cedarling decisions and citations, not the
model's wording. To try another model, set `P2_OPENROUTER_MODEL` in `.env`.
@@ -521,15 +632,14 @@ Paid routing requires both a paid model ID and
`P2_OPENROUTER_ALLOW_PAID=true`; use it only if you intend to spend your
OpenRouter credit. Adding credit alone leaves the configured
`openrouter/free` router unchanged.
-Do not rerun `pnpm run setup` merely to retry generation: setup also rebuilds
-the corpus using Voyage quota.
+Don't rerun `pnpm run setup` to retry generation: it rebuilds the corpus using Voyage quota.
-### Explain the logs
+### Read the retrieval and decision logs
The response's `requestId` appears in `authorization.context`. Its
-`cedarlingRequestId` matches a native `request_id`; document decisions also share
-`batch_id`. For Leo's confidential-document attempt, the important native fields
-look like this illustrative excerpt:
+`cedarlingRequestId` matches Cedarling's `request_id`; document decisions also share
+`batch_id`. For Leo's confidential-document attempt, the main Cedarling log fields
+look like this:
```json
{
@@ -542,79 +652,70 @@ look like this illustrative excerpt:
}
```
-Token evidence explains the empty principal array. An empty reason with no errors
-means no permit matched, not a list of failed conditions. Compare Leo's subject
-with the policy's reader grant. An allowed document names
+Identity comes from tokens in these requests, so the principal array
+is empty. An empty reason with no errors means no permit matched; it doesn't
+list failed conditions. An allowed document names
`server-retrieve-authorized-document` in its reasons.
`retrieval.completed` reports candidates, documents evaluated, chunks loaded,
and model. `documentAuthorizationCount` is not a count of allowed documents.
-`retrieval.failed` names the failed stage; provider failures include bounded
+`retrieval.failed` names the failed stage; provider failures include limited
provider details. Failure at `answer.generate` is not a Cedarling denial.
-Capture only synthetic evidence and remove tokens and secrets from recordings.
+Capture only fictional data and remove tokens and secrets from recordings.
-### Check ordering and failures
+### Check denied text stays out of the result
-From the completed project:
+For automated checks, use a separate checkout of the
+[finished tag](https://github.com/GluuFederation/cedarling-tutorials/tree/p2-tenantrag-v1.0.1/p2-tenantrag), install its locked project and shared IdP dependencies, then run:
```bash
pnpm check
```
This runs formatting, lint, types, tests, and build without provider keys.
-`test/policy-store.test.ts` evaluates real Cedarling rules with signed test
-evidence. `test/retrieval.test.ts` proves ordering, denied-text exclusion, empty
-results, and unavailable or incomplete decisions with deterministic provider
-doubles. Those doubles are not an offline application mode.
-`test/app.test.ts` checks request failures, including unsupported content types
-returning `400 invalid_retrieval` before retrieval runs.
-
-Start the integrated app and run `pnpm test:e2e`.
+The checks evaluate real Cedarling policies with signed test evidence. They
+verify decision order, removal of denied text, empty results, and failed
+or incomplete decisions using fixed provider responses. These are only for
+tests; the application still needs live providers. Unsupported content
+types also return `400 invalid_retrieval` before retrieval runs.
+
+In that finished checkout, run setup and start the integrated app before
+running `pnpm test:e2e`.
Approve the Ada, Leo, and Mallory sign-ins. This consumes provider quota and
-checks evidence access, not identical generated prose. Record the same denied
-attempt before and after integration, plus one successful authorized search.
+checks document access, not identical model answers. Record Mallory's request
+before and after integration, plus one successful authorized search.
-## Reuse the pattern in your own service
+## Protect retrieved evidence in your own service

_A denied corpus stops the question; a denied document is filtered while other allowed documents may continue._
-Authorize the collection before expensive work, then authorize evidence before
-loading content. Preserve this ordering wherever search results feed a model,
+Check access to the collection before expensive work, then check each document before
+loading its content. Keep this order wherever search results feed a model,
report, or another service.
Follow `src/authorization.ts`, `src/rag/retrieval.ts`, `src/rag/repository.ts`,
`policy-store/`, and the tests in the
-[completed P2 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p2-tenantrag-v1.0.0/p2-tenantrag).
-
-Production needs real identity and entitlement sources, secure transport,
-reviewed provider data handling, and protected logs. Setup embeds the synthetic
-corpus as an ingestion operation, not a caller-authorized retrieval. P2 controls
-evidence access, not model truthfulness or prompt-injection immunity: an
-instruction-like public document remains ordinary authorized evidence.
+[completed P2 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p2-tenantrag-v1.0.1/p2-tenantrag).
For a production stack, consider Agama Lab Policy Designer for policy authoring,
-Jans Auth for token issuance, and Lock Server for centralized decision logs.
+Jans Auth for issuing tokens, and Lock Server for centralized decision logs.
See [Cedarling production solutions](https://cedarling.dev/solutions).
Next, P3 puts authorization at an MCP server, where a model can request a change
to incident state as well as information.
----
-
-[^1]: P2's confidential-reader rule uses the current document's `confidential_reader_subjects` set. Ada's subject is listed; Leo's is not. The application supplies this relationship for each authorization request.
-
-[^2]: The pinned [`cedarling_wasm` JavaScript API](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468) accepts a JSON-string request. Serialization changes the data format.
-
-[^3]: Starting-checkpoint source: [`src/rag/retrieval.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p2-tenantrag/src/rag/retrieval.ts) marks both permissive seams before embedding and text loading.
-
-[^4]: Completed fact sources: [`src/rag/fixtures.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/src/rag/fixtures.ts), [`src/rag/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/src/rag/types.ts), [`src/rag/pdf.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/src/rag/pdf.ts), and [`src/rag/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/src/rag/repository.ts).
+
+Warning: This setup is for local practice
-[^5]: Complete tagged store: [`metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/policy-store/metadata.json), [`schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/policy-store/schema.cedarschema), [`server-access.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/policy-store/policies/server-access.cedar), and [`tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/policy-store/trusted-issuers/tutorial-idp.json).
+- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta.
+- Use trusted user and permission records, review how providers handle data, and protect logs. Setup sends the fictional corpus for embedding to build the index; it does not check a caller's access to those documents.
+- Cedarling controls access to documents. It does not guarantee correct model answers or prevent prompt injection; a public document containing instructions is still allowed content.
+- These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
-[^6]: Complete enforcement source: [`src/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/src/authorization.ts), [`src/rag/retrieval.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/src/rag/retrieval.ts), and [`src/runtime.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/src/runtime.ts).
+
-[^7]: Archive build and Docker source: [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/shared/policy-store.mjs), [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/shared/policy-store.d.mts), [`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/package.json), and [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/Dockerfile).
+[^1]: P2's confidential-reader rule uses the current document's `confidential_reader_subjects` set. Ada's subject is listed; Leo's is not. The application supplies this relationship for each authorization request.
-[^8]: Completed startup files: [`scripts/prepare.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/scripts/prepare.ts), [`scripts/preflight.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/scripts/preflight.ts), [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/scripts/setup.ts), and [`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p2-tenantrag-v1.0.0/p2-tenantrag/scripts/dev.mjs).
+[^2]: The pinned [`cedarling_wasm` JavaScript API](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468) accepts a JSON-string request. `JSON.stringify()` converts the request to JSON.
diff --git a/p2-tenantrag/pnpm-lock.yaml b/p2-tenantrag/pnpm-lock.yaml
index 6fc75d7..b4983a7 100644
--- a/p2-tenantrag/pnpm-lock.yaml
+++ b/p2-tenantrag/pnpm-lock.yaml
@@ -1109,8 +1109,8 @@ packages:
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
@@ -2149,7 +2149,7 @@ snapshots:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
prelude-ls@1.2.1: {}
@@ -2224,7 +2224,7 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
split2@4.2.0: {}
diff --git a/p3-mcp-capability-governance/README.md b/p3-mcp-capability-governance/README.md
index 2212b3c..ee445f5 100644
--- a/p3-mcp-capability-governance/README.md
+++ b/p3-mcp-capability-governance/README.md
@@ -3,38 +3,37 @@

P3 is a terminal assistant for searching incidents, reading a runbook, preparing
-triage, and updating incident status through MCP. Cedarling centralizes decisions
-for tools, resources, and prompts; the MCP server enforces them before returning
-content or changing an incident.
+triage, and updating incident status through MCP. The MCP server asks Cedarling
+for permission before a tool, resource, or prompt returns content or changes
+an incident.
+
+Follow the [tutorial](docs/tutorials.md) to secure the [starting application](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p3-mcp-capability-governance) with a [private Cedarling sidecar](https://docs.jans.io/stable/cedarling/developer/sidecar/cedarling-sidecar-overview/).
## Architecture
-```text
-Dana / Amir / Eve ── Device Flow ──→ Tutorial IdP (localhost:18003)
- │ │ signed access token
- └── terminal chat → OpenRouter → MCP client
- │
- Compose trust boundary ▼
- ┌─────────────────────────────────────────┐
- │ MCP server (PEP) → Cedarling sidecar PDP │
- │ │ private loopback │
- │ ├── DENY → no content / no change │
- │ └── ALLOW → incidents / runbook │
- │ / triage │
- │ Tutorial IdP shares this namespace │
- └─────────────────────────────────────────┘
+```mermaid
+flowchart TD
+ accTitle: MCP enforcement with a private Cedarling sidecar
+ accDescr: The terminal uses OpenRouter to select work. The MCP server consults a separate Cedarling sidecar and enforces its response before accessing incidents, runbooks or prompts.
+ Host["Terminal host and MCP client"] <-->|"Select an operation"| Model["OpenRouter"]
+ Host -->|"MCP request and access token"| Server
+ subgraph Local["Local services"]
+ Server["MCP server: current caller and incident facts"] -->|"AuthZen request"| PDP["Cedarling sidecar and policy archive"]
+ PDP -->|"Validate signed token"| IdP["Tutorial IdP discovery and keys"]
+ PDP -->|"Decision"| Check["MCP server enforces result"]
+ Check -->|"ALLOW"| Effect["Incident, runbook or triage operation"]
+ Check -->|"DENY or failure"| Stop["No protected effect"]
+ end
```
-The MCP server verifies the token and `mcp.access` scope. It sends signed token
-evidence and current account/incident facts directly to the sidecar's AuthZen
-endpoint. Cedarling checks the caller, client, audience, role, and assignment.
-Discovery is filtered; direct calls still pass the operation's authorization
-boundary. The model never receives the access token or chooses trusted facts.
+The server verifies the token and `mcp.access` scope, then sends signed identity
+evidence and current account/incident facts to the sidecar. The model receives
+neither the token nor control over those facts. Discovery and direct calls each
+require permission; see the [enforcement walkthrough](docs/tutorials.md#check-permission-before-each-mcp-operation).
-The readable `policy-store/` is packaged by the shared builder into ignored
-`.local/policy-store.cjar`. Compose loads it read-only into the pinned sidecar.
-Only the MCP and IdP ports are published on host loopback. The IdP, MCP server,
-and sidecar share one local trust boundary; this is a local learning deployment.
+Compose builds `policy-store/` into `.local/policy-store.cjar` and mounts it
+read-only in the sidecar. Only the MCP and IdP ports are published on host
+loopback. These services share a network namespace for local learning.
## Prerequisites
@@ -44,7 +43,7 @@ and sidecar share one local trust boundary; this is a local learning deployment.
- An OpenRouter API key in `P3_OPENROUTER_API_KEY` for interactive chat.
P3 starts its own tutorial IdP at `http://localhost:18003` and MCP service at
-`http://localhost:17003/mcp`. Other projects use separate ports and IdP instances.
+`http://localhost:17003/mcp`.
## Run
@@ -81,9 +80,9 @@ terminal open while using chat.
## Exercise
-- **Dana (`dana`)** — Supervisor allowed to operate on every incident.
-- **Amir (`amir`)** — Analyst allowed to operate on assigned incidents.
-- **Eve (`eve`)** — Authenticated caller with no incident operations.
+- Dana (`dana`) is a supervisor who can operate on every incident.
+- Amir (`amir`) is an analyst who can operate on assigned incidents.
+- Eve (`eve`) can sign in but has no incident operations.
In `pnpm chat amir`, enter these prompts separately:
@@ -102,23 +101,16 @@ P3 advances one incident per request, not a bulk "resolve all" operation.
`Prepare triage for INC-2001.` returns `authorization_denied`.
- **Eve:** `Find incident INC-1001.` reports no available operations without calling the model.
-These are chat requests through MCP, not REST POST examples. The free model must
-return a valid tool call before any operation runs. Greetings and unrelated
-messages show help without invoking MCP. Chat reports provider, quota,
-timeout, and invalid-response failures separately from authorization denials;
-free-provider availability is not guaranteed.
+The model must return a valid operation selection before MCP can execute it.
+Chat reports provider, quota, timeout, and invalid-response failures separately
+from authorization denials.
The MCP server prints JSON `authorization.decision` or `authorization.failed`
-records with actor, action, resource, and application request ID. The sidecar
-prints Cedarling's native decision records, including policy reasons. Its native
-request IDs are separate from the application's IDs. The token-cache limit is
-1,800 seconds, matching the tutorial tokens' 30-minute lifetime. Signature,
-issuer, audience, and expiration validation remain enabled.
-
-Cedarling closes both discovery and direct-operation access gaps. Confirmation
-prevents accidental changes; it is not authorization. Statuses advance through
-`open` → `investigating` → `mitigated` → `resolved`. A sidecar failure denies
-access without changing incidents.
+records. The sidecar prints native Cedarling decisions with policy reasons and
+separate request IDs. The [log guide](docs/tutorials.md#read-the-server-and-sidecar-logs)
+explains how to read both streams.
+
+Incident statuses advance through `open` → `investigating` → `mitigated` → `resolved`. A sidecar failure denies access without changing incidents.
Stop and restart the stack with `docker compose down`, then
`docker compose up --build`, to restore incident fixtures. This also rebuilds
diff --git a/p3-mcp-capability-governance/docs/tutorials.md b/p3-mcp-capability-governance/docs/tutorials.md
index 5006308..50ad2de 100644
--- a/p3-mcp-capability-governance/docs/tutorials.md
+++ b/p3-mcp-capability-governance/docs/tutorials.md
@@ -10,73 +10,84 @@ lastVerified: 2026-10-01T09:46:20Z
# Govern MCP Capabilities with Cedarling
+Thanks for joining us! We'll use an incident assistant to see how Cedarling
+checks what a team member may do. Dana supervises the team's incidents, while
+Amir works on those assigned to him.
+
+Amir types `y` to confirm an incident update. The assistant carries it out,
+although nobody assigned that incident to him. Confirmation tells us what Amir
+wants to do; the starting MCP server still needs to check whether he may do it.
+
+We'll add server checks using a private Cedarling sidecar. Supervisors and
+analysts may discover operations, search, and read the runbook. Reading,
+updating, or preparing triage for an incident also requires a supervisor role
+or the analyst's current assignment. Eve has no incident permissions and must
+not reach these operations. We'll retry Amir's update, then check his assigned
+work still succeeds, including calls without the chat interface.
+
+## Build the integration or try the finished app
+
+- To build the integration, start with [Run the starting application](#run-the-starting-application), then add the policies and sidecar calls.
+- To try the finished app, run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance) using its README, then go to [Check assigned and unassigned incidents](#check-assigned-and-unassigned-incidents). This version already uses Cedarling.
+
-Project source and prerequisites
+What you'll need
-- [Complete P3 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance) and [starting checkpoint](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p3-mcp-capability-governance).
-- Install Node.js 24.21+ within 24.x, pnpm 10.17.1, and Docker with Compose. Docker is required for the completed P3 stack because Cedarling runs in a containerized Flask sidecar.[^3] Live chat needs an OpenRouter API key. The pinned sidecar is `linux/amd64`, so ARM Docker Desktop needs amd64 emulation.
-- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta.
-- I prepared these steps on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
+- Git, Node.js 24.21+ within 24.x, and pnpm 10.17.1.
+- Docker with Compose is required: the completed PDP runs in a containerized Flask sidecar.[^1] The pinned image is `linux/amd64`; ARM Docker Desktop needs amd64 emulation.
+- An OpenRouter API key for live chat. Keep paid routing disabled unless you choose to pay for requests.
+- Familiarity with TypeScript, HTTP APIs, and access tokens.
- New to Cedarling? [Read the short introduction](https://cedarling.dev/learn/what-is-cedarling) when you need it.
-- Keep the official [Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and [Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html) references handy for the policy-store steps.
+- Keep the [Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and [Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html) references handy while editing policies.
-Paths are relative to `p3-mcp-capability-governance/` unless stated otherwise.
-Interactive model availability, tool selection, and wording can vary. A free
-provider may be temporarily unavailable; retry the request or choose another
-free tool-calling model. If you already have paid OpenRouter credit, you may
-explicitly opt in to a paid model. Neither choice changes the MCP server's
-authorization rules or makes model output evidence of permission.
+Copy whole files from GitHub's raw-file view into your baseline checkout; don't
+switch to the finished tag. The short examples aren't complete replacements.
+Create missing parent directories. Paths and commands are relative to
+`p3-mcp-capability-governance/`; repository-level `shared/` files go one directory above it.
-## What may an incident assistant do for its caller?
+## Meet the incident assistant and its users

-_Dana, Amir, and Eve illustrate how role and current assignment affect each MCP operation._
-
-An assistant can choose a valid tool and still request an operation its caller
-must not perform. Asking “Are you sure?” does not solve that problem. Confirmation
-expresses intent; authorization establishes permission.
+_Role and current assignment determine which MCP operations Dana, Amir, and Eve may use._
-I'll use an incident assistant to show the gap, then protect the operation at
-the MCP server where it actually takes effect.
+P3 is a terminal assistant backed by a Node.js MCP server.[^2] It searches incidents,
+reads a runbook, prepares triage prompts, and advances incident status. We'll
+use Cedarling to protect MCP tools, resources, and prompts.
-P3 is a terminal assistant backed by a Node.js MCP server.[^1] It searches incidents,
-reads a runbook, prepares triage prompts, and advances incident status. We will
-use Cedarling to protect all three MCP surfaces: tools, resources, and prompts.
-
-- **Dana** is a supervisor who may operate on every incident.
-- **Amir** is an analyst who may operate only on assigned incidents.
-- **Eve** can authenticate but has no incident-operation authority.
+- **Dana** is a supervisor who may work on every incident.
+- **Amir** is an analyst who may work only on assigned incidents.
+- **Eve** can sign in but has no permission to work on incidents.
`INC-1001` is an open payment incident assigned to Amir. `INC-2001` is an
unassigned audit incident in the mitigated state. Amir should not resolve the
-second incident merely because the model selected its update tool.
-
-```text
-Operator --> terminal host --> OpenRouter selects an operation
- |
- MCP client + access token
- |
- local Compose trust boundary
- +---------------------------------------------------------+
- | Node.js MCP server (PEP) --> Cedarling sidecar (PDP) |
- | | ^ |
- | | current caller, | policy archive |
- | | action, resource | signed token validation |
- | | |
- | +-- ALLOW --> incident / runbook / triage effect |
- | +-- DENY or failure --> no protected effect |
- | |
- | Tutorial IdP shares this network namespace |
- +---------------------------------------------------------+
+second incident just because the model selected its update tool.
+
+The bundled Node.js `oidc-provider` authenticates these users and issues their
+access tokens. The MCP server looks up roles in its account map and assignments
+in the incident repository; neither comes from the model.
+
+```mermaid
+flowchart TD
+ accTitle: MCP enforcement with a private Cedarling sidecar
+ accDescr: The terminal uses OpenRouter to select work. The MCP server consults a separate Cedarling sidecar and enforces its response before accessing incidents, runbooks or prompts.
+ Host["Terminal host and MCP client"] <-->|"Select an operation"| Model["OpenRouter"]
+ Host -->|"MCP request and access token"| Server
+ subgraph Local["Local services"]
+ Server["MCP server: current caller and incident facts"] -->|"AuthZen request"| PDP["Cedarling sidecar and policy archive"]
+ PDP -->|"Validate signed token"| IdP["Tutorial IdP discovery and keys"]
+ PDP -->|"Decision"| Check["MCP server enforces result"]
+ Check -->|"ALLOW"| Effect["Incident, runbook or triage operation"]
+ Check -->|"DENY or failure"| Stop["No protected effect"]
+ end
```
-The host and model request work. The MCP server controls the data and effects.
-Cedarling supplies decisions; it does not invoke tools or update incidents.
+The host and model request work. The MCP server controls data access and changes.
+Cedarling supplies decisions; it does not call tools or update incidents.
-## Show why authentication and confirmation are insufficient
+## Try updating an unassigned incident

@@ -84,7 +95,7 @@ _The baseline validates the request but does not yet enforce the assignment rule
### Run the starting application
-Use a separate checkout with disposable incident state:
+Use a separate checkout with its own sample incidents:
```bash
git clone https://github.com/GluuFederation/cedarling-tutorials.git cedarling-p3
@@ -95,7 +106,6 @@ pnpm install --frozen-lockfile
pnpm run setup
```
-Host commands require Node.js 24.21 or newer within 24.x and pnpm 10.17.1.
Start the baseline server and its IdP with Docker Compose:
```bash
@@ -120,24 +130,21 @@ Find incident INC-2001.
Advance INC-2001 from mitigated to resolved.
```
-Confirm with `y` when asked. The permissive baseline lets Amir find and change
+Confirm with `y` when asked. The baseline lets Amir find and change
this unassigned incident. It validates identity, input, confirmation, and the
state transition, but does not enforce the assignment rule.
-A free provider may fail before producing an MCP call. Retry or choose another
-model; that failure does not show authorization protection. Capture the
-unauthorized incident change and note its business impact: an analyst changed
-an incident he does not own. Stop
-the baseline before running the integrated stack. `docker compose down` followed
-by a fresh start restores P3's in-memory incident fixtures.
+A free provider may fail before making an MCP call. Retry or choose another
+model; a provider failure does not prove authorization works. Answers and tool
+choices vary, so check the incident state. Capture Amir's unauthorized update,
+then stop the baseline with `docker compose down`. Restarting restores the sample incidents.
-## Prepare the MCP server for a decision
+## Where should we check permission?
-No separate incident feature needs adding before Cedarling. The baseline already
-authenticates the MCP caller, validates tool input, asks the terminal user to
-confirm a status change, and enforces the incident's state transition and
-idempotency key. In the update handler, a permissive trace sits immediately
-before the existing mutation:[^6]
+Open the baseline's
+[`src/mcp/server.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p3-mcp-capability-governance/src/mcp/server.ts)
+and find the `update_incident_status` handler. After authentication and input
+validation, it logs an ALLOW without checking permission, then calls the repository:
```ts
// src/mcp/server.ts (starting checkpoint)
@@ -149,44 +156,38 @@ const incident = services.incidents.updateStatus({
});
```
-Keep those application checks. Do not put permission in the chat model or its
-confirmation prompt. The MCP server will load the current incident and ask the
-sidecar before this call; discovery, search results, runbook text, and triage
-prompts need their own gates too.
-
-## Define authority for each MCP boundary
+We'll ask the sidecar for permission before this call. Keep the terminal's
+confirmation prompt. Also keep the status-change checks and retry protection
+(idempotency) in
+[`src/incidents/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p3-mcp-capability-governance/src/incidents/repository.ts).
+Idempotency prevents the same change being applied twice. The server also needs
+decisions before returning discovery, search results, runbook text, and triage prompts.
-We have now seen the authenticated but permissive starting point. From here,
-we will add Cedarling: first the authority model, then a decision at every MCP
-boundary, and finally the same unassigned-incident attempt as a comparison.
+## Decide which MCP operations each user may perform

_The decision combines trusted identity with facts about the current incident._
-### Model real resources, not model intentions
+### Choose an action and resource for each operation
-The business rule is small: operations staff can discover the service, search,
-and read guidance; supervisors cover all incidents, while analysts cover only
-their assignments. Eve receives no operations.
+Each action links to its policy. Requests use the signed caller token; roles and
+assignments come from the server.
-| Capability | Identity | Action | Resource | Trusted context | Protected effect |
-| -------------- | ------------------- | -------------- | ------------------------------- | ----------------------------------- | ---------------------------------- |
-| Discover | Signed caller token | `Discover` | `Service::"incident-assistant"` | Current caller subject and role | Register the caller's MCP surface |
-| Search | Same token | `Search` | Same service | Same caller | Begin incident search |
-| Read result | Same token | `Read` | Each candidate `Incident` | Same caller; assignment on resource | Return a matching incident summary |
-| Read runbook | Same token | `ReadRunbook` | `Runbook::"core"` | Same caller | Return runbook content |
-| Prepare triage | Same token | `Triage` | Current `Incident` | Same caller; assignment on resource | Return an incident-specific prompt |
-| Update status | Same token | `UpdateStatus` | Current `Incident` | Same caller; assignment on resource | Commit one valid transition |
+| Capability | Identity | Action | Resource | Trusted context | Protected effect |
+| -------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | ----------------------------------- | ------------------------------------------- |
+| Discover | Signed caller token | [`Discover`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L16 "operations-surface") | `Service::"incident-assistant"` | Current caller subject and role | Make MCP operations available to the caller |
+| Search | Same token | [`Search`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L16 "operations-surface") | Same service | Same caller | Begin incident search |
+| Read result | Same token | [`Read`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L26 "incident-assignment") | Each candidate `Incident` | Same caller; assignment on resource | Return a matching incident summary |
+| Read runbook | Same token | [`ReadRunbook`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L16 "operations-surface") | `Runbook::"core"` | Same caller | Return runbook content |
+| Prepare triage | Same token | [`Triage`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L26 "incident-assignment") | Current `Incident` | Same caller; assignment on resource | Return an incident-specific prompt |
+| Update status | Same token | [`UpdateStatus`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar#L26 "incident-assignment") | Current `Incident` | Same caller; assignment on resource | Commit one valid transition |
-All types and actions use namespace `P3IncidentAssistant`. The model can nominate
-an incident ID; the server loads the incident and its assignment. Role comes from
-the server's account profile, not the model or a caller-provided JSON field.
-State-transition and idempotency checks remain application responsibilities.
-The policy combines a role check with the current incident-to-analyst assignment;
-the model's choice of tool or incident is never itself a grant.
+All types and actions use namespace `P3IncidentAssistant`. The model can suggest
+an incident ID; the server loads its assignment and the caller's role. Neither
+comes from the model or caller JSON. The app still checks status changes and retries.
-### Build the policy store
+### Create the policy store
Create the [directory-based policy store](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format):
@@ -200,13 +201,19 @@ policy-store/
tutorial-idp.json
```
-Create these four files from the completed policy store.[^7]
+Create the complete policy-store files from the pinned version:
+
+- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/metadata.json)
+- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/schema.cedarschema)
+- [`policy-store/policies/incident-operations.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar)
+- [`policy-store/trusted-issuers/tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/policy-store/trusted-issuers/tutorial-idp.json)
+
Use the integration's metadata with policy version `1.0.0`. The schema defines
`Service`, `Runbook`, and `Incident`; only the incident needs an optional
`assigned_to` attribute. It also defines `Access_token`, `TrustedIssuer`, the
issuer URL shape, and context containing `caller` and optional generated tokens.
-The actions declare the token type in their principal vocabulary; the sidecar
-request supplies signed evidence rather than constructing another user entity.
+The actions use the token type as their principal type; the sidecar
+request supplies a signed token rather than another user entity.
No default entities, templates, or custom issuers are needed. Current account and
incident facts are supplied by the MCP server.
@@ -238,15 +245,14 @@ this exact shape:
}
```
-Keep `configuration_endpoint` as used by this pinned sidecar, rather than copying
-another runtime's issuer property name. The MCP authentication middleware verifies
-JWT evidence and the coarse `mcp.access` scope. Policies additionally bind token
-subject, API audience, and client ID to this caller and application.
+Keep `configuration_endpoint` for discovery with this pinned sidecar. The MCP middleware verifies
+the JWT and general `mcp.access` scope. Policies also check that the token's
+subject, API audience, and client ID match this caller and application.
-### Require identity evidence as well as a business permit
+### Check the token, then the role and assignment
In `policies/incident-operations.cedar`, a forbid protects every operation if
-required signed evidence is missing or mismatched:
+required token evidence is missing or does not match:
```cedar
// policy-store/policies/incident-operations.cedar
@@ -263,7 +269,7 @@ forbid(principal, action, resource) unless {
};
```
-Then grant incident operations only to the appropriate caller:
+Then allow incident operations based on role and assignment:
```cedar
// policy-store/policies/incident-operations.cedar
@@ -279,74 +285,96 @@ permit(principal, action in [
};
```
+For Amir reading `INC-1001`, `context.caller.role` is `analyst` and
+`resource.assigned_to` matches his subject, `amir`. The permit matches.
+`INC-2001` has no assignment, so the analyst condition cannot permit access to it.
+Both decisions still require the token's subject, audience, and client ID to
+pass the identity rule above.
+
The remaining `operations-surface` permit covers `Discover`, `Search`, and
-`ReadRunbook` for supervisor or analyst roles. A permit cannot override a matching
-forbid, and no matching permit gives DENY. These are
-[Cedar policy semantics](https://docs.cedarpolicy.com/policies/syntax-policy.html),
-not role checks in the chat model.
+`ReadRunbook` for supervisor or analyst roles. Eve's `observer` role matches no
+permit. Under [Cedar's policy rules](https://docs.cedarpolicy.com/policies/syntax-policy.html),
+a matching forbid overrides any permit, and no matching permit gives DENY.
-## Enforce the rules at the MCP server
+## Connect the MCP server to Cedarling

_The MCP server is the enforcement point; the sidecar returns authorization decisions._
-### Run the private sidecar
+### Prepare the private sidecar
-The Node.js application calls the sidecar's AuthZEN-style HTTP endpoint.[^4]
-The policy decision point runs in the Dockerized Flask sidecar, not inside the
-MCP server; the MCP server remains the enforcement point. Pin the Docker image:
+The Node.js app calls the sidecar's AuthZEN-style HTTP endpoint.[^3]
+Cedarling decides in Flask; the MCP server enforces the result.
+Compose pins this Docker image:
```text
ghcr.io/janssenproject/jans/cedarling-flask-sidecar:2.4.1-1@sha256:501d5bc88e8a0b67cbab314b31c787f94b6c4ad9f16a77ec81d0e183b2645f0b
```
-This image is `linux/amd64`; ARM Docker Desktop requires amd64 emulation. Add the
-integration's shared `policy-store.mjs` builder and declaration at repository
-level, and install its build dependencies from P3:
+Save the repository-level archive builder and its declaration:
+
+- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/shared/policy-store.mjs)
+- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/shared/policy-store.d.mts)
+
+Install its build dependencies from P3:
```bash
pnpm add --save-dev --save-exact fflate@0.8.3 @cedar-policy/cedar-wasm@4.12.0
node ../shared/policy-store.mjs
```
-Run the builder before TypeScript compilation. In `Dockerfile`, use the build
-artifact in the policy-store stage; Compose's one-shot `policy-store` service
-places it in a volume mounted read-only by Cedarling. Keep the readable source
-in Git and the generated local `.cjar` ignored.
+The builder and Dockerfile create the same `.local/policy-store.cjar` archive.
+Compose's `policy-store` service runs once to put it in a
+volume mounted read-only by Cedarling. Keep the readable source in Git and the
+generated local `.cjar` ignored.
`sidecar-bootstrap.json` selects `/policy-store/policy-store.cjar`, enables JWT
-signature and strict schema validation, accepts RS256, and loads issuers
-synchronously. Native logs go to standard output. Set
+signature and strict schema validation, accepts RS256, and loads trusted issuers
+before accepting requests. Cedarling's logs go to standard output. The bootstrap sets
`CEDARLING_TOKEN_CACHE_MAX_TTL` to `1800`, matching the thirty-minute tutorial
-tokens. Set `SIDECAR_DEBUG_RESPONSE=False` in Compose.
+tokens. Compose sets `SIDECAR_DEBUG_RESPONSE=False`.
-Use the integrated `compose.yaml` topology: the MCP server and Cedarling join the
+In the integrated `compose.yaml`, the MCP server and Cedarling join the
IdP's network namespace. The sidecar binds `127.0.0.1:5000` there. Only application
port 17003 and IdP port 18003 are published on host loopback. The sidecar is not
-published. Wait for the IdP and archive before starting Cedarling, then wait for
-Cedarling readiness before starting the MCP server.
+published. Compose waits for the IdP and archive before starting Cedarling,
+then waits for Cedarling readiness before starting the MCP server.
JWT validation establishes the request subject; it does not authenticate every
service that can reach the sidecar. This local trust boundary includes the IdP.
-A multi-host production deployment needs authenticated service transport, such as
-mTLS through a service mesh[^5] or reverse proxy, as well as network restrictions.
-### Send current facts directly to the Cedarling sidecar instance
+### Send current facts to the Cedarling sidecar
+
+Save these complete files together to connect the permission checks:
+
+- [`src/mcp/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/mcp/authorization.ts)
+- [`src/mcp/server.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/mcp/server.ts)
+- [`src/mcp/availability.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/mcp/availability.ts)
+- [`src/auth/token-verifier.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/auth/token-verifier.ts)
+- [`src/incidents/types.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/incidents/types.ts)
+- [`src/incidents/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/incidents/repository.ts)
+- [`src/mcp/schemas.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/mcp/schemas.ts)
+- [`src/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/app.ts)
+- [`src/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/main.ts)
+- [`src/config/project-config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/config/project-config.ts)
+- [`tsconfig.build.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/tsconfig.build.json)
+
+Remove `src/mcp/trace.ts`; the completed handlers no longer use it.
An AuthZEN-style decision request has a subject, action, resource, and context,
-and the response contains a boolean decision.[^4] In this Cedarling sidecar
+and the response contains a boolean decision.[^3] In this Cedarling sidecar
integration, the subject also carries a signed access token with a named token
mapping, and the resource carries `cedar_entity_mapping` plus current incident
facts. Those mapping properties and the `/cedarling/evaluation` path are
Cedarling-specific; do not treat them as generic AuthZEN fields. A valid true
decision allows the MCP server to continue; DENY or an unavailable response
-must leave the protected effect untouched.
+must stop the operation before it returns data or changes an incident.
-Place the HTTP call in `src/mcp/authorization.ts`. The following excerpt belongs
-inside the authorization function: `auth` is verified MCP authentication,
-`subject` is the mapped persona, `roles` is the server account map, and `resource`
-was loaded by the operation. `action` is a server-selected action name.
+In the copied `src/mcp/authorization.ts`, `auth` is verified MCP authentication,
+`subject` identifies the signed-in user, `roles` is the server account map, and `resource`
+was loaded by the operation. `action` is a server-selected action name. The
+expanded HTTP call is:
```ts
// src/mcp/authorization.ts
@@ -384,28 +412,28 @@ const response = await fetch("http://127.0.0.1:5000/cedarling/evaluation", {
});
```
-`JSON.stringify()` supplies the HTTP request body as JSON here.[^2]
+`JSON.stringify()` supplies the HTTP request body as JSON here.[^4]
-Reject unsuccessful HTTP responses, malformed bodies, and timeouts. Validate a
-boolean `decision` and object `context`; this pinned sidecar can report runtime
-failure in an HTTP 200 body with `context.id === "-1"`. Treat that as
-`authorization_unavailable`, not an ordinary policy denial. Only a valid true
-decision permits the effect. Omit an absent optional assignment rather than
-sending `null` to a string field.
+The module rejects HTTP errors and timeouts, and requires a boolean `decision`
+and object `context` in the response. This pinned sidecar can
+report runtime failure in an HTTP 200 body with `context.id === "-1"`; the module
+treats that as `authorization_unavailable`, not a policy denial. Only a valid
+true decision permits the operation. For an unassigned incident, the request omits
+the optional assignment instead of sending `null` to a string field.
-### Gate discovery and every operation
+### Check permission before each MCP operation
-In `src/mcp/server.ts`, evaluate `Discover` before registering the caller's
-operations. A denied caller receives valid empty discovery. Registering a tool
+`src/mcp/server.ts` evaluates `Discover` before registering the caller's
+operations. A denied caller receives an empty list of operations. Registering a tool
is not permission to use it against every resource.
- Search requires `Search`, then checks `Read` for each candidate incident before
- returning it. Apply the requested result limit after authorization filtering.
+ returning it. The result limit applies after authorization filtering.
- Runbook reads require `ReadRunbook` before returning text.
-- Triage requires `Triage` on the resolved incident before producing its prompt.
-- Status updates require `UpdateStatus` before the repository mutation.
+- Triage requires `Triage` on the incident loaded by the server before producing its prompt.
+- Status updates require `UpdateStatus` before changing the stored incident.
-The update boundary looks like this inside its existing handler:
+The update handler waits for permission before calling the repository:
```ts
// src/mcp/server.ts
@@ -424,20 +452,29 @@ const incident = services.incidents.updateStatus({
```
`requireAllowed` throws on DENY; unavailable decisions also stop execution.
-The repository still enforces one-step transitions, current status, and
-idempotency synchronously at the effect. A stale state is not made valid by
-an earlier ALLOW.
+Before changing an incident, the repository still checks its current status,
+one-step transitions, and repeated requests. An earlier ALLOW cannot make an
+outdated state valid.
The terminal host owns confirmation and its idempotency key. A model-provided
`confirmed` value cannot replace the user's confirmation. OAuth credentials never
-enter model messages. Model interpretation is not an authorization guarantee.[^8]
+enter model messages.
+
+## Finish setup and start the integrated stack
+
+Save the runtime preparation and packaging files:
-## Finish the runnable MCP stack
+- [`src/chat/host.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/src/chat/host.ts)
+- [`scripts/setup.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/scripts/setup.mjs)
+- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/Dockerfile)
+- [`compose.yaml`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/compose.yaml)
+- [`sidecar-bootstrap.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/sidecar-bootstrap.json)
-Package the policy archive before TypeScript compilation and make the Docker
-policy-store stage supply that archive to the private sidecar. The Compose
-services and readiness order above are part of the completed runtime, not a
-second authorization path.[^9]
+Remove `scripts/dev.mjs`. In `package.json`, set `dev` to
+`docker compose up --build` and `start` to `docker compose up`, then apply the
+`build` entry below. Keep the other dependencies and scripts; the
+[tagged manifest](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance/package.json)
+shows the completed configuration:
```json
{
@@ -448,33 +485,31 @@ second authorization path.[^9]
```
The host `scripts/setup.mjs` prepares chat configuration; Compose's configure
-service prepares the container IdP configuration. Do not make host setup rewrite
-that container's issuer. `pnpm dev` now runs `docker compose up --build`, while
-`pnpm chat amir` remains a host command. For unrelated greetings, the completed
-host asks the model for no operation and returns bounded help without calling
-the MCP server. This improves the exercise but is not an authorization rule.[^10]
+service prepares the container IdP configuration. Host setup must not rewrite
+that container's issuer. `pnpm dev` runs Compose; `pnpm chat amir` remains a host
+command. Greetings can return a short help response without an MCP operation.
-## Prove allowed work and blocked bypasses
+Run `pnpm build`, then `pnpm run setup` and `pnpm dev` from the host.
+Wait for the IdP, archive service, sidecar, and MCP server to become ready.
+Then run `pnpm chat amir` to check assigned and unassigned incidents.
+
+## Check assigned and unassigned incidents

_An assigned incident and an unassigned incident produce different decisions for Amir._
-### Exercise the completed stack
-
-Start the integrated services with `docker compose up --build`. On the host,
-run `pnpm install --frozen-lockfile` and `pnpm run setup`, then set
-`P3_OPENROUTER_API_KEY` in `.env` and run `pnpm chat amir`.
+### Complete Amir's assigned work
-The completed client defaults to `liquid/lfm-2.5-2.6b:free` with zero-price
-routing. To try another free tool-calling model, set `P3_OPENROUTER_MODEL` in
+The client defaults to `liquid/lfm-2.5-2.6b:free` with free routing.
+To try another free tool-calling model, set `P3_OPENROUTER_MODEL` in
`.env`. A paid model requires both a paid model ID and
`P3_OPENROUTER_ALLOW_PAID=true`; do this only if you intend to use your paid
credit. Model answers and tool choices can differ even when the authorization
result is the same. The provider may retain prompts for training; use only
fictional incident data.
-On fresh fixtures, enter separately:
+With fresh sample incidents, enter separately:
```text
Find incident INC-1001.
@@ -488,7 +523,9 @@ new status. Declining confirmation must leave it unchanged.
Now repeat Amir's baseline attempt on `INC-2001`. Search returns no matching
incident. Triage or update attempts are denied; its mitigated state is unchanged.
-Use the following controls:
+
+Dana has no assignment to `INC-2001` either. Can she advance it? Check the
+supervisor condition in the policy before trying the cases below:
| Caller and attempt | Expected outcome |
| --------------------------------------------- | ----------------------------------------------- |
@@ -496,33 +533,30 @@ Use the following controls:
| Amir reads or advances assigned `INC-1001` | ALLOW, subject to confirmation and state checks |
| Amir triages or updates unassigned `INC-2001` | DENY |
| Eve discovers operations | Empty surface; host does not call the model |
-| Direct unauthorized MCP invocation | No protected content or mutation |
-| Sidecar timeout or malformed decision | Unavailable; no protected effect |
-
-A free-model error before tool selection is not a policy denial. Retry or use
-another model; never change access policy to compensate for a generation failure.
+| Direct unauthorized MCP invocation | No incident data returned or changed |
+| Sidecar timeout or malformed decision | Unavailable; operation stopped |
-### Explain the two kinds of decision logs
+### Read the server and sidecar logs
The MCP server prints formatted `authorization.decision` records with
`requestId`, `actorId`, action, resource, and ALLOW or DENY. One application
request can perform both discovery and an operation, producing several decisions.
-Repeated discovery is expected for this stateless MCP surface.
+Discovery repeats because the server handles each request independently.
-Cedarling's native sidecar records contain policy-store identity and
+Cedarling's own sidecar records contain the policy-store ID and
`diagnostics.reason`. An allowed assigned-incident action cites
-`incident-assignment`. An unauthorized assignment can produce DENY with no
-matching permit and an empty reason; an identity-binding forbid can instead
-appear as a determining policy.
+`incident-assignment`. An unassigned analyst can receive DENY with no
+matching permit and an empty reason. If token identity checks fail, the forbid
+can appear as the policy that determined the denial.
-Application IDs and native sidecar request IDs are separate in this integration.
-Do not present them as an exact join. For a teaching capture, run one operation
-at a time and compare actor/action/resource, sequence, and policy reasons.
-Verify the returned incident state as well: an ALLOW is not proof of a mutation.
-`authorization.failed` distinguishes transport/runtime failure from policy denial
-without printing tokens or raw upstream error bodies.
+Application and sidecar request IDs are separate in this integration.
+Do not match their logs by ID. For a recording, run one operation at a time and
+compare the user, action, resource, request order, and policy reasons.
+Check the returned incident state too: an ALLOW does not prove it changed.
+`authorization.failed` separates connection or runtime failures from policy denials
+without printing tokens or raw sidecar errors.
-### Check the protected effect, not the assistant's wording
+### Try direct calls and a sidecar outage
Keep a before-and-after capture of Amir's attempt to resolve unassigned
`INC-2001`, plus a successful update to assigned `INC-1001`. A denial matters
@@ -532,58 +566,50 @@ with `incidentId: "INC-2001"`, `expectedStatus: "mitigated"`, and
`nextStatus: "resolved"`; the same server-side gate handles direct and
model-selected calls.
-For an unavailable-decision exercise, stop only the tutorial sidecar with
+To try a sidecar outage, stop only the sidecar with
`docker compose stop cedarling`, then attempt an otherwise permitted operation.
The server must report authorization unavailable and leave the incident
-unchanged. Restore it with `docker compose start cedarling`. Restart the learner
-stack to restore fixtures between runs; do not treat a resolved incident as a
-fresh mitigated fixture.
+unchanged. Restore it with `docker compose start cedarling`. Restart the stack
+between exercises to restore sample incidents; a resolved incident cannot be
+resolved again.
-## Apply the same rule to other agent tools
+## Protect other agent tools at the server

_Protect MCP discovery and execution at the server boundary, not in the model._
-The component controlling the protected effect must authorize it, even when an
+The component returning data or making a change must check permission, even when an
agent selected the operation and the user confirmed it. Cover discovery,
-resources, and prompts as well as mutation tools.
+resources, and prompts as well as tools that change data.
Trace `src/mcp/authorization.ts`, `src/mcp/server.ts`,
`src/incidents/repository.ts`, `policy-store/`, and `compose.yaml` in the
-[completed P3 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance).
-
-Production needs persistent incident storage, real account authority, secure
-service transport, protected credentials, and appropriate log retention. This
-lab has a development IdP, static account assignments, in-memory incidents, and
-an optional free-provider chat experience; none is a production availability
-promise.
+[completed P3 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p3-mcp-capability-governance-v1.0.1/p3-mcp-capability-governance).
For a production stack, consider Agama Lab Policy Designer for policy authoring,
-Jans Auth for token issuance, and Lock Server for centralized decision logs.
+Jans Auth for issuing tokens, and Lock Server for centralized decision logs.
See [Cedarling production solutions](https://cedarling.dev/solutions).
Next, P4 asks a related question about human workflows: may a publisher still
rely on an approval after the content or reviewer's authority has changed?
----
-
-[^1]: [MCP (Model Context Protocol)](https://modelcontextprotocol.io/specification/2026-07-28/) lets a client discover and invoke server tools and access resources or prompts. P3 authorizes the server-controlled operation, not the model's wording.
-
-[^2]: Here `JSON.stringify()` creates the JSON body for an HTTP POST to the Cedarling sidecar. P3 uses the sidecar's AuthZen endpoint, not the `cedarling_wasm` JavaScript authorization method.
-
-[^3]: A [Cedarling sidecar](https://docs.jans.io/stable/cedarling/developer/sidecar/cedarling-sidecar-overview/) is a separate service that exposes Cedarling decisions to the application over HTTP. In P3, Docker Compose runs that service beside the MCP server.
+
+Warning: This setup is for local practice
-[^4]: The [OpenID AuthZEN Authorization API](https://openid.net/specs/authorization-api-1_0-final.html) defines the decision request's subject, action, resource, and context and the response's decision. P3 uses the Cedarling sidecar's own endpoint and token/entity mapping fields to implement this pattern.
+- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta.
+- Production needs a database that keeps incidents after restarts, managed user permissions, protected credentials, and stored audit logs. The lab keeps a fixed account map and incidents in memory; free-model chat may be unavailable.
+- Across hosts, restrict the sidecar network and authenticate service connections with mTLS through a service mesh[^5] or reverse proxy. JWT validation alone does not establish which service is calling the sidecar.
+- These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
-[^5]: [Mutual TLS (mTLS)](https://istio.io/latest/docs/concepts/security/#mutual-tls-authentication) authenticates both ends of a service connection. A service mesh can manage that transport between application and policy decision service; the local tutorial uses a private shared network namespace instead.
+
-[^6]: Starting-checkpoint source: [`src/mcp/server.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p3-mcp-capability-governance/src/mcp/server.ts) reloads the incident, prints a permissive trace, then calls `updateStatus()`; [`src/incidents/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p3-mcp-capability-governance/src/incidents/repository.ts) owns the transition and idempotency checks.
+[^1]: A [Cedarling sidecar](https://docs.jans.io/stable/cedarling/developer/sidecar/cedarling-sidecar-overview/) is a separate service that exposes Cedarling decisions to the application over HTTP. In P3, Docker Compose runs that service beside the MCP server.
-[^7]: Complete tagged store: [`metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/policy-store/metadata.json), [`schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/policy-store/schema.cedarschema), [`incident-operations.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/policy-store/policies/incident-operations.cedar), and [`tutorial-idp.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/policy-store/trusted-issuers/tutorial-idp.json).
+[^2]: [MCP (Model Context Protocol)](https://modelcontextprotocol.io/specification/2026-07-28/) lets a client find and call server tools and access resources or prompts. P3 authorizes the server-controlled operation, not the model's wording.
-[^8]: Complete MCP enforcement: [`src/mcp/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/src/mcp/authorization.ts), [`src/mcp/server.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/src/mcp/server.ts), and [`src/incidents/repository.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/src/incidents/repository.ts).
+[^3]: The [OpenID AuthZEN Authorization API](https://openid.net/specs/authorization-api-1_0-final.html) defines the decision request's subject, action, resource, and context and the response's decision. P3 uses the Cedarling sidecar's own endpoint and token/entity mapping fields to implement this pattern.
-[^9]: Archive and private sidecar packaging: [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/shared/policy-store.mjs), [`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/package.json), [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/Dockerfile), [`compose.yaml`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/compose.yaml), and [`sidecar-bootstrap.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/sidecar-bootstrap.json).
+[^4]: Here `JSON.stringify()` creates the JSON body for an HTTP POST to the Cedarling sidecar. P3 uses the sidecar's AuthZen endpoint, not the `cedarling_wasm` JavaScript authorization method.
-[^10]: Host-only setup and no-operation handling: [`scripts/setup.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/scripts/setup.mjs), [`src/chat/host.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/src/chat/host.ts), and [`src/config/project-config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p3-mcp-capability-governance-v1.0.0/p3-mcp-capability-governance/src/config/project-config.ts).
+[^5]: [Mutual TLS (mTLS)](https://istio.io/latest/docs/concepts/security/#mutual-tls-authentication) authenticates both ends of a service connection. A service mesh can manage that transport between application and policy decision service; the local tutorial uses a private shared network namespace instead.
diff --git a/p3-mcp-capability-governance/package.json b/p3-mcp-capability-governance/package.json
index 417ffed..9ec4f96 100644
--- a/p3-mcp-capability-governance/package.json
+++ b/p3-mcp-capability-governance/package.json
@@ -22,7 +22,7 @@
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
+ "@modelcontextprotocol/client": "2.2.0",
"@modelcontextprotocol/express": "2.0.0",
"@modelcontextprotocol/node": "2.0.0",
"@modelcontextprotocol/server": "2.0.0",
diff --git a/p3-mcp-capability-governance/pnpm-lock.yaml b/p3-mcp-capability-governance/pnpm-lock.yaml
index 08da2a7..73e9d94 100644
--- a/p3-mcp-capability-governance/pnpm-lock.yaml
+++ b/p3-mcp-capability-governance/pnpm-lock.yaml
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
'@modelcontextprotocol/client':
- specifier: 2.0.0
- version: 2.0.0
+ specifier: 2.2.0
+ version: 2.2.0
'@modelcontextprotocol/express':
specifier: 2.0.0
version: 2.0.0(@modelcontextprotocol/server@2.0.0)(express@5.2.1)
@@ -311,14 +311,18 @@ packages:
'@keyv/serialize@1.1.1':
resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==}
- '@modelcontextprotocol/client@2.0.0':
- resolution: {integrity: sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==}
+ '@modelcontextprotocol/client@2.2.0':
+ resolution: {integrity: sha512-LxCou/CSYQ6dwEnjhLZY0KnEuc8V4UJ3IEQCl/uR2yHobSQIEdJKUlKLRYRF5i5FqRGHy1eK7une3aAWDHLtig==}
engines: {node: '>=20'}
'@modelcontextprotocol/core@2.0.0':
resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==}
engines: {node: '>=20'}
+ '@modelcontextprotocol/core@2.2.0':
+ resolution: {integrity: sha512-iLhmprRmWI8EcosOA3wVvww22z02NkgqhV4fBH6f/odQBsi7xnJc0HGmi21yWO+/iKw2yzqVE127Zhna5/+JXw==}
+ engines: {node: '>=20'}
+
'@modelcontextprotocol/express@2.0.0':
resolution: {integrity: sha512-Snlr8j9FR9LcVvEJPF7qJ7d5zTL4Bes2dk7RcacN9eSZ7OLohwxqhEvWu1+UxELyScgLbLLOdeVEGdwKI1iwVQ==}
engines: {node: '>=20'}
@@ -1116,8 +1120,8 @@ packages:
engines: {node: '>=14'}
hasBin: true
- proxy-addr@2.0.7:
- resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ proxy-addr@2.0.8:
+ resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
punycode@2.3.1:
@@ -1195,8 +1199,8 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
@@ -1544,9 +1548,9 @@ snapshots:
'@keyv/serialize@1.1.1': {}
- '@modelcontextprotocol/client@2.0.0':
+ '@modelcontextprotocol/client@2.2.0':
dependencies:
- '@modelcontextprotocol/core': 2.0.0
+ '@modelcontextprotocol/core': 2.2.0
cross-spawn: 7.0.6
eventsource: 3.0.7
eventsource-parser: 3.1.1
@@ -1558,6 +1562,10 @@ snapshots:
dependencies:
zod: 4.6.5
+ '@modelcontextprotocol/core@2.2.0':
+ dependencies:
+ zod: 4.6.5
+
'@modelcontextprotocol/express@2.0.0(@modelcontextprotocol/server@2.0.0)(express@5.2.1)':
dependencies:
'@modelcontextprotocol/server': 2.0.0
@@ -2060,7 +2068,7 @@ snapshots:
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
- proxy-addr: 2.0.7
+ proxy-addr: 2.0.8
qs: 6.16.0
range-parser: 1.3.0
router: 2.2.0
@@ -2342,13 +2350,13 @@ snapshots:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
prelude-ls@1.2.1: {}
prettier@3.9.6: {}
- proxy-addr@2.0.7:
+ proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
@@ -2471,7 +2479,7 @@ snapshots:
siginfo@2.0.0: {}
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
stackback@0.0.2: {}
diff --git a/p4-editorial-publishing/README.md b/p4-editorial-publishing/README.md
index 914a41d..b4b0df0 100644
--- a/p4-editorial-publishing/README.md
+++ b/p4-editorial-publishing/README.md
@@ -2,36 +2,34 @@

-CedarPress is a focused editorial workspace where authors create articles and submit immutable
-revisions, editors review exact content, and publishers release a revision only
-while its approval evidence and reviewer authority remain current.
+P4 lets authors submit article revisions for review and publishers release
+approved content. Server-side Cedarling checks prevent self-review, reuse of
+approval across revisions, and publication after a reviewer's authority is revoked.
-Cedarling authorizes each read and mutation on the server using the signed-in
-user and current database facts. Its policies prevent self-review, approval
-reuse across revisions, and publication after reviewer authority is revoked.
+Follow the [tutorial](docs/tutorials.md) to add these checks to the [starting
+application](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p4-editorial-publishing).
## Architecture
-```text
-Riley / Ana / Omar ── sign in ──→ Tutorial IdP
- │
- └── article and revision forms ──→ Next.js App Router
- │
- Server Component / Action
- │
- ▼
- Cedarling PDP
- │ │
- DENY ALLOW
- │
- ▼
- conditional SQLite effect
+```mermaid
+flowchart TD
+ accTitle: Server authorization for editorial changes
+ accDescr: A Next.js Server Action authenticates the caller and loads current facts. Cedarling evaluates them, then the service either rejects the change or rechecks the facts in a database transaction.
+ Form["Browser form"] --> Action["Next.js Server Action: authenticate and validate"]
+ Action --> Facts["Load actor, revision, approval and authority from SQLite"]
+ Facts --> PDP["Embedded Cedarling: unsigned evaluation"]
+ PDP --> Check["Editorial service enforces decision"]
+ Check -->|"DENY or failure"| Stop["No mutation"]
+ Check -->|"ALLOW"| Transaction["SQLite transaction: recheck authorized facts"]
+ Transaction -->|"Facts still match"| Save["Commit change"]
```
## Prerequisites
-- Docker Desktop or Docker Engine with Compose, or
-- Node.js 24.21 or newer within 24.x, pnpm 10, and the project-local tutorial identity provider.
+- To run with Docker: Docker Desktop or Docker Engine with Compose.
+- For native development and checks: Node.js 24.21 or newer within 24.x and pnpm 10.
+
+Both startup paths include the project's tutorial identity provider.
The commands work from PowerShell, macOS terminals, and Ubuntu shells.
@@ -55,8 +53,8 @@ pnpm run setup
pnpm dev
```
-`pnpm dev` starts this project’s IdP and application together.
-Setup synchronizes the application listen port with its registered URL and
+`pnpm dev` starts this project's IdP and application together.
+Setup keeps the application port consistent with its registered URL and
preserves your editorial data.
`pnpm build` followed by `pnpm start` runs the compiled application stack and its project IdP.
@@ -65,12 +63,9 @@ Use `pnpm dev -- --reset` only when you want to restore the tutorial fixtures.
## Exercise
-The workflow demonstrates why an earlier editorial decision is not standing
-authority for later publication:
-
-- **Riley** — Author who drafts and submits articles but has no review authority.
-- **Ana** — Editor and publisher for the valid review and publication path.
-- **Omar** — Editor whose seeded authority can be revoked after approval.
+Riley is an author, Ana can review and publish, and Omar has revocable editor
+authority. All three can create articles in their tenant and edit their own
+drafts. Try these workflows:
- As Riley, choose **New article**, enter a title and body, then submit it.
Approval and rejection are disabled for its author. Sign in as Ana to approve
@@ -81,10 +76,8 @@ authority for later publication:
- Approve **Partner announcement** as Omar, revoke his authority, then try to
publish as Ana: publication is disabled because the approval is no longer valid.
-All three identities can create articles in their own tenant and edit their own
-drafts. Editor and publisher grants remain separate: no author can review their
-own content. Unavailable actions remain visible with a short explanation.
-Direct requests that bypass disabled buttons are still checked on the server.
+Unavailable actions stay visible but disabled, with a short explanation.
+The server also checks direct requests that bypass the buttons.
Revoke Omar's authority with:
@@ -107,35 +100,19 @@ the database file or stopping the application.
## Authorization
Readable schema and policies live in `policy-store/`. Setup, build, and tests
-use the shared builder to validate them and generate the ignored
-`.local/policy-store.cjar` archive. The server logs its version and SHA-256
-when loading it.
-
-`src/server/authorization.ts` calls Cedarling's `authorizeUnsigned()` directly.
-The application authenticates the user through OIDC; it constructs Cedarling
-principals, resources, and context from trusted server data, not form fields.
-Server Components and Actions enforce the decisions. Before writing, SQLite
-checks that the authorized revision and authority evidence have not changed.
-`CreateArticle` targets the authenticated user's `Tenant`, before an article
-exists. The server supplies its tenant and author and atomically saves the
-article and first draft. Page-render decisions guide controls; each mutation
-reloads facts and requests a fresh decision.
-
-`authorization.context` links the application `requestId`, actor, capability,
-and `preview` or `enforcement` phase to `cedarlingRequestId`. The following
-Cedarling JSON decision includes the full `diagnostics.reason` and `errors`.
-An empty reason on DENY means no permit matched, not necessarily an engine error.
-`editorial.action.completed` records a committed effect; `editorial.action.failed`
-records a controlled failure category. An ALLOW alone does not prove a write.
-Browser messages show readable outcomes without correlation IDs or policy diagnostics.
-Cedarling memory logs expire after
-five minutes; they are not a durable audit store. P4 uses the standard Next.js
-lifecycle, which does not guarantee an awaited Cedarling shutdown hook.
-
-Riley can also choose **New article** to create a tenant-scoped draft, then
-submit it through the same review workflow. Cedarling checks `CreateArticle`
-against the signed-in user's tenant before insertion; the Server Action also
-checks the session, CSRF token, and draft input.
+use the shared builder to validate them and generate the ignored `.local/policy-store.cjar` archive. The server logs its version and SHA-256 when loading it.
+
+[`src/server/authorization.ts`](src/server/authorization.ts) evaluates current
+database facts with `authorizeUnsigned()` after OIDC authentication. Server
+Components and Actions enforce the results. Each mutation gets a fresh decision,
+then SQLite rechecks the authorized facts before committing. See the
+[server integration](docs/tutorials.md#add-cedarling-to-the-server) for request
+construction and transaction checks.
+
+Server logs distinguish permission decisions from committed changes; browser
+messages show the outcome without policy diagnostics. The
+[log guide](docs/tutorials.md#read-the-decision-and-publication-logs) explains
+request correlation. Cedarling's five-minute memory logs are not a durable audit store.
## Commands
diff --git a/p4-editorial-publishing/docs/tutorials.md b/p4-editorial-publishing/docs/tutorials.md
index 4d9ff95..35b00f3 100644
--- a/p4-editorial-publishing/docs/tutorials.md
+++ b/p4-editorial-publishing/docs/tutorials.md
@@ -10,75 +10,82 @@ lastVerified: 2026-10-01T09:46:20Z
# Secure Editorial Publishing with Cedarling
-
-Project source and prerequisites
+Welcome! Our example is an editorial app where Riley writes articles and Ana
+reviews and publishes them. We'll use Cedarling to check permissions as an
+article moves from draft to review and publication.
-- [Complete P4 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p4-editorial-publishing-v1.0.0/p4-editorial-publishing) and [starting checkpoint](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p4-editorial-publishing).
-- Install Docker with Compose, or Node.js 24.21+ within 24.x and pnpm 10.17.1. The project supplies its own tutorial identity provider.
-- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta.
-- I prepared these steps on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
-- New to Cedarling? [Read the short introduction](https://cedarling.dev/learn/what-is-cedarling) when you need it.
-- Keep the official [Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and [Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html) references handy for the policy-store steps.
+What does an approval cover when the author changes the article afterward?
+Our editorial app accepts the old approval. It also lets an editor approve
+their own writing and accepts approvals from reviewers whose review permission has since been revoked.
-
+We'll start with self-approval and use Cedarling to close all three gaps:
+require a different reviewer, tie approval to the exact revision, and check
+that reviewer's current permission before publication. Existing tenant, author,
+and publisher restrictions will move into the same policy store. By the end,
+Riley and Ana will still be able to publish reviewed work together, with server
+checks on article creation, reads, edits, submission, review, and publication.
-Paths are relative to `p4-editorial-publishing/` unless stated otherwise. Use
-fresh synthetic records for each exercise so earlier edits do not change its
-expected result.
+## Build the integration or try the finished app
-## When is an earlier approval still valid?
+- To build the integration, start with [Run the starting application](#run-the-starting-application), then add the policies and server checks.
+- To try the finished app, run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p4-editorial-publishing-v1.0.1/p4-editorial-publishing) using its README, then go to [Check approvals and publication](#check-approvals-and-publication). This version already uses Cedarling.
-
+
+What you'll need
-_The author, reviewer, and revocable editor exercise different editorial decisions._
+- Git, Node.js 24.21+ within 24.x, and pnpm 10.17.1 for the coding steps. The project supplies its own tutorial identity provider (IdP).
+- Docker with Compose is optional for running the starting or finished application. Use native Node.js for the coding steps.
+- Familiarity with TypeScript, Next.js Server Actions, sessions, and basic HTTP requests.
+- New to Cedarling? [Read the short introduction](https://cedarling.dev/learn/what-is-cedarling) when you need it.
+- Keep the [Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and [Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html) references handy while editing policies.
-An editor approves a draft. The author changes it. A publisher clicks Publish.
-Should the earlier approval cover the new words?
+
-We'll test that question on a real revision, then tie permission to the content
-and authority that still exist when Publish is clicked.
+Copy whole files from GitHub's raw-file view into your baseline checkout; don't
+switch to the finished tag. The short examples aren't complete replacements.
+Create missing parent directories. Paths and commands are relative to
+`p4-editorial-publishing/`; repository-level `shared/` files go one directory above it.
-P4 is a small Next.js App Router application for creating articles,
-reviewing immutable revisions, and publishing approved content. We will use
-Cedarling to make permission depend on the exact revision and current authority,
-not merely on whether an approval once existed.
+## Meet the authors and editors
-- **Riley** authors and submits articles, but has no editorial review authority.
-- **Ana** can review other authors' work and publish eligible revisions.
-- **Omar** can review work until his editor authority is revoked.
+
-All three can create articles in their tenant. Authorship, editorial authority,
-and publication authority are separate facts. Being an editor does not allow
-someone to approve their own content.
+_Riley writes; Ana reviews and publishes. Omar lets us test what happens when a reviewer loses permission._
-```text
-Browser form --> Next.js Server Action (PEP)
- |
- authenticate + validate
- |
- load current SQLite facts
- actor / revision / approval / authority
- |
- embedded Cedarling PDP <-- policy store
- |
- DENY or failure --> no mutation
- |
- ALLOW
- v
- SQLite transaction
- recheck authorized facts --> commit effect
+P4 is a small Next.js App Router application for creating articles,
+reviewing fixed versions of their content, and publishing approved work.
+
+- **Riley** writes and submits articles, but has no permission to review them.
+- **Ana** can review other authors' work and publish approved revisions.
+- **Omar** can review work until his permission is revoked.
+
+All three can create articles in their tenant. Writing, reviewing, and publishing
+require separate permissions. Editors cannot approve their own content.
+
+```mermaid
+flowchart TD
+ accTitle: Server authorization for editorial changes
+ accDescr: A Next.js Server Action authenticates the caller and loads current facts. Cedarling evaluates them, then the service either rejects the change or rechecks the facts in a database transaction.
+ Form["Browser form"] --> Action["Next.js Server Action: authenticate and validate"]
+ Action --> Facts["Load actor, revision, approval and authority from SQLite"]
+ Facts --> PDP["Embedded Cedarling: unsigned evaluation"]
+ PDP --> Check["Editorial service enforces decision"]
+ Check -->|"DENY or failure"| Stop["No mutation"]
+ Check -->|"ALLOW"| Transaction["SQLite transaction: recheck authorized facts"]
+ Transaction -->|"Facts still match"| Save["Commit change"]
```
-Cedarling runs on the server using `authorizeUnsigned()`. “Unsigned” describes
-the authorization request, not an unauthenticated user. OIDC authenticates the
-session first; the server constructs the principal and facts from trusted state.
-The browser receives guidance, not authority over publication.
+Cedarling runs on the server using `authorizeUnsigned()`. The bundled Node.js
+`oidc-provider` authenticates the user first. The server then loads the current
+user, revision, and authority records from SQLite for Cedarling to evaluate.
+"Unsigned" describes this authorization request; it does not skip sign-in.
+The browser uses the server's decisions to enable or disable controls.
-## Observe the unsafe workflow first
+## Try the workflow before adding Cedarling

-_The baseline lacks a decision tied to independent approval of the current revision._
+_The starting app does not require a different person to approve the current revision._
### Run the starting application
@@ -98,8 +105,7 @@ Select Riley; the development IdP usually prefills `riley`, so enter it only
if the field is empty. Use a non-empty password such as `cedarling-is-awesome` on the
development IdP, and approve access.
-For native startup instead, use Node.js 24.21 or newer within 24.x and pnpm
-10.17.1, then run from the project directory:
+For native startup, run these commands from the project directory:
```bash
pnpm --dir ../shared/identity-provider install --frozen-lockfile
@@ -112,12 +118,12 @@ pnpm dev
This starts the IdP and Next.js together. Do not run native and Docker instances
on the same ports.
-### Show self-approval and stale approval
+### Approve your own work, then reuse an old approval
As Riley, open **Launch brief**, select **Submit for review**, then **Approve
revision**. The baseline reports that the exact revision was approved, despite
-Riley being its author without review authority. Authentication and the workflow
-state check succeeded; the missing condition is independent, authorized review.
+Riley being its author without permission to review. Sign-in and workflow
+checks passed; the app failed to require a different, authorized reviewer.
Two further exercises show why publication needs its own decision:
@@ -139,19 +145,20 @@ pnpm admin revoke-omar
docker compose exec cedarpress node --env-file=/run/config/app.env scripts/admin.ts revoke-omar
```
-Capture the author/reviewer, revision evidence, and published outcome. The baseline
-`e2e/editorial-gaps.e2e.ts` records these same gaps. Use separate seeded articles
-for each exercise. Stop the baseline before integrating, using `Ctrl+C` and,
-for Docker, `docker compose down` without removing its volume.
+Capture the reviewer, revision details, and publication result.
+Use a separate sample article for each exercise. Stop the baseline
+with `Ctrl+C` before editing; for Docker, also run `docker compose down` without
+removing its volume. If you started with Docker, install the native dependencies
+using the commands above before the coding steps.
-## Prepare the existing editorial workflow for authorization
+## Where should we check permission?
-No new article feature needs building before Cedarling. The starting application
-already has the New article form, authenticated Server Actions, revision and
-approval records, CSRF checks, and database version guards. Its service calls a
-baseline authorization gateway before each effect, but that gateway still
-permits publication from the presence of an approval rather than evidence for
-the current revision:[^3]
+Open the baseline's
+[`src/server/service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p4-editorial-publishing/src/server/service.ts)
+and find `publish()`. It calls the authorization function before writing to the
+database. In
+[`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p4-editorial-publishing/src/server/authorization.ts),
+that check only asks whether the user may publish and an approval exists:
```ts
// src/server/authorization.ts (starting checkpoint)
@@ -159,17 +166,17 @@ case "publication.publish":
return fact("publisherAuthorityCurrent") && fact("approvalPresent");
```
-Keep the existing workflow and write integrity. Replace this incomplete
-decision at the service boundary; do not make the button or a historical
-approval the authority for a new publication.
+Nothing here checks which revision was approved or whether its reviewer still
+has permission to review. We'll replace this check with Cedarling. The existing New article
+form, authentication, CSRF checks, and database version guards stay in place.
-## Model permission for exact content
+## Decide who can review and publish

-_A publish decision needs current content, an independent review, and current authority._
+_The current revision needs approval from a different person who still has review permission._
-### Answer the policy-store questions
+### Create the policy store
Create the readable [directory-based store](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format):
@@ -181,50 +188,56 @@ policy-store/
editorial.cedar
```
-Create these three files from the completed policy store.[^4]
+Create the complete policy-store files from the pinned version:
+
+- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/metadata.json)
+- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/schema.cedarschema)
+- [`policy-store/policies/editorial.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar)
+
Use the store's metadata and version `1.0.0`. Identity comes from the application's
-OIDC session, so this policy store needs no trusted-issuer mapping. Principal,
-revision, and authority facts arrive with each request; no default entities,
+verified OIDC session and current database user, so Cedarling needs no
+trusted-issuer mapping for these unsigned requests. Principal, revision, and
+permission records arrive with each request; no default entities,
templates, or custom issuers are needed.
-| Design question | P4 answer |
-| --------------------------------------- | ----------------------------------------------------------------------------------------------------- |
-| Who acts? | `P4EditorialPublishing::Principal`: database user ID and tenant |
-| Where does a new article belong? | `Tenant`, selected from the authenticated user |
-| What may be read? | An `Article` in that tenant |
-| What does review or publication target? | One `Revision`, with its ID, tenant, author, version, digest, and state |
-| What makes review legitimate? | Current editor authority and a different author |
-| What makes publication legitimate? | Current publisher authority plus exact approval evidence from a still-authorized independent reviewer |
-| Which facts change per request? | Revision state, approval evidence, and current authority |
-| What remains outside policy? | Input bounds, CSRF, state transitions, immutable content, and atomic writes |
-
-The digest[^1] binds evidence to normalized content. A digest is not permission:
-it helps compare the content that was reviewed with the content being published.
-Keep article version and revision version distinct. The former protects the
-workspace against stale mutations; the latter identifies the approved revision.
-
-### Define the request boundaries
-
-All requests use the current server-resolved principal. All action identifiers
-have the form `P4EditorialPublishing::Action::"CreateArticle"`.
-
-| Capability | Action | Resource | Additional context | Effect waiting for ALLOW |
-| --------------------- | ----------------- | ------------------------ | ------------------------------------------------- | ------------------------------------- |
-| `article.create` | `CreateArticle` | Authenticated tenant | Empty | Insert article and first draft |
-| `article.read` | `ReadArticle` | Current article | Empty | Return article and revision evidence |
-| `revision.edit` | `EditRevision` | Current revision | Empty | Save owned draft or create next draft |
-| `revision.submit` | `SubmitRevision` | Current revision | Empty | Submit owned content for review |
-| `revision.approve` | `ApproveRevision` | Exact submitted revision | Current editor authority | Record independent approval |
-| `revision.reject` | `RejectRevision` | Exact submitted revision | Current editor authority | Record independent rejection |
-| `publication.publish` | `PublishRevision` | Exact current revision | Current publisher authority and approval evidence | Publish that revision |
+| Design question | P4 answer |
+| --------------------------------------- | ------------------------------------------------------------------------------------------------- |
+| Who acts? | `P4EditorialPublishing::Principal`: database user ID and tenant |
+| Where does a new article belong? | `Tenant`, selected from the authenticated user |
+| What may be read? | An `Article` in that tenant |
+| What does review or publication target? | One `Revision`, with its ID, tenant, author, version, digest, and state |
+| Who may review? | A current editor who is not the author |
+| Who may publish? | A current publisher with exact approval from a different reviewer who still has review permission |
+| Which facts change per request? | Revision state, approval evidence, and current authority |
+| What remains outside policy? | Input limits, CSRF, valid state changes, fixed content, and all-or-nothing writes |
+
+The digest[^1] is a content fingerprint: it lets us compare what was reviewed
+with what is being published. Permission is checked separately.
+Keep article version and revision version separate. The article version stops
+outdated writes; the revision version identifies the approved content.
+
+### Choose an action and resource for each operation
+
+Each request uses the current user loaded by the server and an action
+such as `P4EditorialPublishing::Action::"CreateArticle"`.
+
+| Capability | Action | Resource | Additional context | Effect waiting for ALLOW |
+| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | ------------------------------------------------- | ------------------------------------- |
+| `article.create` | [`CreateArticle`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L51 "create-tenant-article") | Authenticated tenant | Empty | Insert article and first draft |
+| `article.read` | [`ReadArticle`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L2 "read-tenant-article") | Current article | Empty | Return article and revision evidence |
+| `revision.edit` | [`EditRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L10 "author-revision") | Current revision | Empty | Save owned draft or create next draft |
+| `revision.submit` | [`SubmitRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L10 "author-revision") | Current revision | Empty | Submit owned content for review |
+| `revision.approve` | [`ApproveRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L21 "independent-review") | Exact submitted revision | Current editor authority | Record independent approval |
+| `revision.reject` | [`RejectRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L21 "independent-review") | Exact submitted revision | Current editor authority | Record independent rejection |
+| `publication.publish` | [`PublishRevision`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/policy-store/policies/editorial.cedar#L34 "publish-exact-approved-revision") | Exact current revision | Current publisher authority and approval evidence | Publish that revision |
Creating an article targets the tenant because the article does not yet exist.
Its author and tenant are set by the server, not accepted from the form.
-### Write independent review and exact-publication rules
+### Require an independent reviewer and approval of the current revision
-In `policies/editorial.cedar`, independent review requires both authority and
-separation from the author:
+In `policies/editorial.cedar`, a reviewer must have permission and must not be
+the author:
```cedar
// policy-store/policies/editorial.cedar
@@ -241,7 +254,12 @@ permit (
};
```
-Publication has a separate permit:
+For Riley's submitted article, Riley fails two conditions: there is no current
+permission to review, and `principal.id` equals `resource.author_id`. Ana belongs to
+the same tenant, has permission to review, and is not the author, so she can review
+that submitted revision.
+
+Publication needs another rule:
```cedar
// policy-store/policies/editorial.cedar
@@ -263,19 +281,24 @@ permit (
};
```
+If Ana publishes content approved by Omar, the approval must match the current
+revision's ID, version, and digest. Revoking Omar's editor authority makes
+`context.approval.reviewer_authority_current` false even when the content hasn't
+changed. That approval can no longer permit publication.
+
The same file contains `read-tenant-article`, `author-revision`, and
`create-tenant-article`: tenant members read their articles, authors edit/submit
their own revisions, and tenant members create articles in their tenant.
-No matching permit means DENY. State and write integrity remain enforced by SQLite
-even after an authorization succeeds.
+No matching permit means DENY. SQLite still checks workflow state and guards
+against conflicting writes after authorization succeeds.
-## Integrate Cedarling with the server-owned workflow
+## Add Cedarling to the server

-_The server checks authorization and rechecks mutable facts before committing a change._
+_The server checks permission, then checks that the facts still match before saving a change._
-### Prepare the archive and initialize the SDK
+### Build the archive and load Cedarling
From P4, install exact versions:
@@ -284,20 +307,25 @@ pnpm add --save-exact @janssenproject/cedarling_wasm@0.0.468 fflate@0.8.3
pnpm add --save-dev --save-exact @cedar-policy/cedar-wasm@4.12.0
```
-Add the integration's repository-level `shared/policy-store.mjs` and
-`shared/policy-store.d.mts`. Run the shared builder to create the archive:
+Save the repository-level archive builder and its declaration:
+
+- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/shared/policy-store.mjs)
+- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/shared/policy-store.d.mts)
+
+Run the shared builder to create the archive:
```bash
node ../shared/policy-store.mjs
```
-It validates source and produces ignored `.local/policy-store.cjar`. Use the same
-artifact preparation in Docker; keep the source directory readable in Git.
+The command must finish without validation errors and create `.local/policy-store.cjar`.
+Keep the readable source directory in Git; we'll include the generated archive
+in the Docker image later.
-Replace the permissive implementation in `src/server/authorization.ts` with
-direct [SDK calls](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468).
-Initialize once through the existing server runtime. This excerpt assumes
-`archivePath` is the server-resolved path to the generated archive:
+We'll replace `src/server/authorization.ts` using the complete file in the next
+step. It initializes [Cedarling](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468)
+once through the server runtime. Here, `archivePath` resolves to the generated
+`.local/policy-store.cjar`:
```ts
// src/server/authorization.ts
@@ -316,18 +344,30 @@ const cedarling = await initFromArchiveBytes(
);
```
-Log the store version and SHA-256 at initialization. The module exposes
-`close: () => cedarling.shutDown()` for controlled lifecycle owners such as tests.
+The complete module logs the store version and SHA-256 at startup. It
+also returns `close: () => cedarling.shutDown()` for callers, such as tests,
+that explicitly stop the runtime.
+
+### Pass the current revision and approval to Cedarling
-### Construct the publication request from current records
+Save these complete files together to connect the permission checks:
-`src/server/service.ts` parses form candidates, loads the current article and
-revision, loads the latest approval, and loads the publisher's current authority.
-The authorization module maps those trusted records into Cedarling's request.
-The following expanded publication request illustrates what that function
-constructs from `principal`, `article`, `approval`, and
-`publisherAuthorityCurrent`; the completed source uses a shared capability
-mapping and a bounded failure handler:
+- [`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/authorization.ts)
+- [`src/server/models.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/models.ts)
+- [`src/server/errors.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/errors.ts)
+- [`src/server/database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/database.ts)
+- [`src/server/service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/service.ts)
+- [`src/server/runtime.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/src/server/runtime.ts)
+- [`app/actions.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/actions.ts)
+- [`app/action-button.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/action-button.tsx)
+- [`app/articles/[articleId]/page.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/articles/[articleId]/page.tsx)
+- [`next.config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/next.config.ts)
+
+`src/server/service.ts` validates form input and loads the current article,
+revision, latest approval, and publisher permission. The authorization module
+turns these into a Cedarling request using `principal`, `article`, `approval`, and
+`publisherAuthorityCurrent`. The full file shares this mapping across operations
+and handles failures:
```ts
// src/server/authorization.ts
@@ -377,21 +417,21 @@ if (result.response.diagnostics.errors.length > 0) throw unavailable();
return result.decision === true;
```
-`authorizeUnsigned()` expects a JSON string; `JSON.stringify()` serializes the
-current principal, resource, and context for that Cedarling call.[^2] The
+`authorizeUnsigned()` expects a JSON string; `JSON.stringify()` converts the
+current principal, resource, and context to that format.[^2] The
formatted log call makes nested reasons readable in this local exercise.
`unavailable()` is the existing application error from `src/server/errors.ts`.
-The surrounding catch maps SDK failures to that bounded error without printing
-raw request data. False becomes `FORBIDDEN` in the service. Neither path writes.
+The catch handler maps Cedarling failures to that error without printing raw
+request data. A false decision becomes `FORBIDDEN` in the service.
-For other capabilities, map the corresponding resource and context from the
-request table. Use current database authority, never a form field claiming that
-an editor or approval is still valid.
+The other capabilities use the resources and context in the request table.
+Permission always comes from the database, never a form field claiming that an
+editor or approval is still valid.
-### Keep ALLOW tied to the committed effect
+### Check permission before saving the publication
-In the publication service, the protected sequence is:
+In `publish()`, `this.allow()` must succeed before `this.database.publish()` runs:
```ts
// src/server/service.ts
@@ -418,33 +458,52 @@ this.database.publish(
);
```
-Here `article` was already resolved at the submitted expected article version.
-Inside the write transaction, recheck the article and authority/approval snapshots.
-If content or authority changed while awaiting Cedarling, reject with
-`STATE_CONFLICT`. Do not turn an ALLOW for earlier facts into permission for a
-new state. Apply the same principle to draft, submit, and review operations.
+`this.allow()` throws `forbidden()` when authorization returns false. A failed
+evaluation also throws, so neither outcome reaches the database write.
+
+Here `article` has already been loaded at the expected article version.
+Inside the write transaction, the database rechecks the article, approval, and
+permission records. A change while waiting for Cedarling causes `STATE_CONFLICT`
+instead of an outdated write. Draft, submit, and review operations also check
+whether another request changed the records.
+
+The Server Actions in `app/actions.ts` keep authentication, same-origin and
+CSRF verification. `"use server"` does not restrict who may call an action;
+these functions are reachable by network requests.
+
+Page rendering uses server permission previews to disable denied controls with
+a short explanation. Each submitted action reloads facts and authorizes again.
+OAuth tokens and raw policy diagnostics stay on the server; successful forms
+show readable outcomes without internal request IDs.
-Keep authentication, same-origin and CSRF verification inside the Server Action
-path in `app/actions.ts`. `"use server"` is not an access-control rule: these
-functions are reachable by network requests.
+## Finish setup and restart the app
-Apply the same pattern to creation: the **New article** form at
-`app/articles/new/` calls `createArticle` and `EditorialService.create()`.
-Enforce `CreateArticle` against the session tenant before saving the article
-and first draft. The author and tenant come from the authenticated session.
+Save the runtime preparation and packaging files:
-Page rendering requests permission previews for controls. Keep denied controls
-disabled with a short explanation; do not duplicate policy as client role checks.
-Every actual action reloads facts and authorizes again. OAuth tokens and raw
-policy diagnostics stay on the server. Successful forms show readable outcomes,
-not internal request IDs.[^5]
+- [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/scripts/setup.ts)
+- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/Dockerfile)
+- [`app/auth/callback/route.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/auth/callback/route.ts)
+- [`app/error.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/error.tsx)
+- [`app/styles.css`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/app/styles.css)
-## Finish the runnable editorial application
+Apply the `build` entry shown below to your existing
+[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.1/p4-editorial-publishing/package.json), keeping
+the other dependencies and scripts.
-Make the same archive available to development, production builds, and Docker.
-`scripts/setup.ts` builds it; the build script runs the shared builder before
-Next.js compilation. Docker includes the archive in the completed application
-image.[^6]
+Replace the `exclude` list in `tsconfig.json` with the following. The baseline's
+test files still reference the authorization gateway we replaced, so exclude
+them from this runtime build while keeping application type checking enabled.
+We'll use the finished checkout for the current test suite:
+
+```json
+{
+ "exclude": ["node_modules", "test", "e2e"]
+}
+```
+
+`scripts/setup.ts` builds the archive for development. The build script runs
+the shared builder before Next.js compilation, and the Dockerfile includes
+the archive in the application image:
```json
{
@@ -454,23 +513,54 @@ image.[^6]
}
```
-The server runtime also needs `serverExternalPackages` for
-`@janssenproject/cedarling_wasm` in `next.config.ts`; this is packaging for the
-server-only WASM dependency, not a browser PDP.[^7] The existing New article
-path remains one of the seven capabilities to enforce, not a separate feature
-to recreate.
+The `next.config.ts` copied earlier sets `serverExternalPackages` for
+`@janssenproject/cedarling_wasm`. This keeps the WASM dependency on the server;
+P4 does not run Cedarling in the browser.
-## Prove that approval is evidence, not permanent permission
+Stop the baseline development stack completely before restarting; the
+server runtime is cached and must not keep its old authorization implementation.
+Run:
+
+```bash
+pnpm run setup
+pnpm build
+pnpm dev
+```
+
+The app is available at `http://localhost:17004`. We'll check
+that Riley cannot self-approve and Ana can still review and publish his work.
+
+## Check approvals and publication

-_The same article can become ineligible to publish when its revision or reviewer authority changes._
+_Changing an article's revision or its reviewer's permission can block publication._
+
+Reset the exercise database first: restarting does not undo the article changes
+or restore Omar's revoked permission. Reset clears editorial records, sessions,
+and pending sign-ins, then restores the sample articles and Omar's review
+permission. Do not reset data you want to keep.
+
+In another terminal, from `p4-editorial-publishing/`, run the command matching
+your startup method:
-### Repeat the original attempt by bypassing the button
+```bash
+# Native
+pnpm reset
+```
+
+```bash
+# Docker
+docker compose exec cedarpress node --env-file=/run/config/app.env scripts/reset.ts
+```
+
+Sign in again as Riley before continuing.
+
+### Try self-approval without the button restriction
Use a fresh article for this exercise. As Riley, create an article, submit its
draft for review, and confirm that its current revision says **submitted**.
-The **Approve revision** button is disabled. To prove the server boundary,
+The **Approve revision** button is disabled. To check the server also denies approval,
open developer tools on that article's local page and run:
```js
@@ -486,15 +576,18 @@ Expect “This action is not allowed.” Reload: the revision remains submitted
and no approval was recorded. This modifies only the local control and submits
the real framework form; it does not grant authority.
-For a direct HTTP proof, capture that actual Server Action POST in the Network
+For a direct HTTP check, capture that Server Action POST in the Network
panel and replay it using the same authenticated local session. Preserve its
current action header, body, and CSRF value. Do not invent or hard-code a Next.js
-action ID, and do not publish the captured credentials. The automated browser
-check performs this replay. The transport can return HTTP 200 with an
-`x-action-redirect` containing `error-forbidden`; judge the application outcome
-and unchanged record, not the transport status alone.
+action ID, and do not publish the captured credentials. The response can return
+HTTP 200 with an `x-action-redirect` containing `error-forbidden`; judge the
+application outcome and unchanged record, not the HTTP status alone.
-### Complete legitimate work, then invalidate its evidence
+### Publish reviewed work, then change its content or authority
+
+If Omar's authority is revoked after approval but the content stays unchanged,
+can Ana still publish it? Check the publication policy, then compare your
+answer with the revocation row below.
| Exercise | Expected integrated outcome |
| ---------------------------------------------------------------- | ------------------------------------------------------------ |
@@ -507,34 +600,31 @@ and unchanged record, not the transport status alone.
Use **Customer migration guide** for the new-revision scenario and **Partner
announcement** for revocation, following the same steps as before integration.
-Use **Editorial handbook** as Ana's valid approval/publication control, or finish
+Use **Editorial handbook** to check Ana can approve and publish, or finish
the article Riley created during this exercise.
-For reproducible fresh data, use `pnpm reset` in native mode or
-`docker compose exec cedarpress node --env-file=/run/config/app.env scripts/reset.ts`
-in Docker. Reset deliberately clears editorial records, sessions, and pending
-sign-ins in this exercise database; sign in again afterward.
-
-### Read decisions separately from completed actions
+### Read the decision and publication logs
`authorization.context` links the application `requestId`, actor, capability,
-and `preview` or `enforcement` phase to a native `cedarlingRequestId`. Native JSON
+and `preview` or `enforcement` phase to the Cedarling request ID in `cedarlingRequestId`. Cedarling's JSON
prints all policy reasons and errors. An allowed publication cites
`publish-exact-approved-revision`.
-A self-approval DENY can have empty `diagnostics.reason` and `errors`: no permit
-matched, rather than the engine malfunctioning. Explain it using the trusted
-author and editor facts. A preview ALLOW may be followed by enforcement DENY
-after revocation; the preview was guidance for an earlier state.
+A self-approval DENY can have empty `diagnostics.reason` and `errors` because no
+permit matched. Use the author and editor facts to explain the denial. A preview
+ALLOW may be followed by enforcement DENY after revocation; the preview was
+guidance for an earlier state.
-`editorial.action.completed` appears after the effect commits.
-`editorial.action.failed` identifies a controlled failure. Pair these with the
-revision evidence and logs; an ALLOW alone does not establish publication.
-Memory logs expire after five minutes and are not a durable audit store.
+`editorial.action.completed` appears after the change is saved.
+`editorial.action.failed` records a handled failure. Compare these logs with the
+revision details; an ALLOW alone does not prove publication.
+Logs kept in memory expire after five minutes, so they are not a lasting audit record.
-### Check stale state and failures
+### Check concurrent changes and unavailable decisions
-Stop the learner app to free ports 17004 and 18004 before running:
+The coding steps update runtime files, not the baseline's historical tests.
+For the full automated checks, use a separate checkout of the [finished tag](https://github.com/GluuFederation/cedarling-tutorials/tree/p4-editorial-publishing-v1.0.1/p4-editorial-publishing) and install its locked project and shared IdP dependencies.
+Stop your learner stack to free ports 17004 and 18004, then run:
```bash
pnpm exec playwright install chromium
@@ -545,53 +635,46 @@ On Linux, use `pnpm exec playwright install --with-deps chromium` if browser
libraries are missing. The check includes formatting, lint, types, tests, and
a production build/browser workflow with disposable data.
-`test/authorization.test.ts` evaluates the real policies. `test/service.test.ts`
-checks self-review, changed revisions, revoked authority, and unavailable
-Cedarling without effects. It also introduces concurrent changes while decisions
-are pending. `e2e/editorial-authorization.e2e.ts` uses the real IdP and browser,
-including button tampering and direct Server Action replay.
+The checks evaluate real policies and verify that self-review, stale approvals,
+revoked authority, and unavailable Cedarling cannot change protected records.
+They also change facts while decisions are pending and repeat button tampering
+and direct Server Action requests through the real IdP and browser.
-Record one failed self-approval before and after integration, and one legitimate
-publication showing the exact approved revision. Keep tokens, cookies, and CSRF
-values out of recordings.
+Record the same self-approval attempt before and after integration: it succeeds
+in the baseline and is denied afterward. Also record an allowed publication
+of the exact approved revision. Keep tokens, cookies, and CSRF values out of
+recordings.
-## Apply the pattern to other approval workflows
+## Use the same checks in your own approval workflow

_Bind approval to the exact content being published, then enforce the current decision._
-Treat approval as evidence about a specific resource version. Before the next
-protected effect, check both that evidence and the authority that still exists.
-Then commit only if the facts authorized by Cedarling remain current.
+Treat approval as a record for a specific version of the content. Before using
+it, check that it still matches and that the reviewer still has permission.
+Save the change only if the facts checked by Cedarling remain current.
Follow `src/server/authorization.ts`, `src/server/service.ts`,
`src/server/database.ts`, `app/actions.ts`, `policy-store/`, and the tests in the
-[completed P4 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p4-editorial-publishing-v1.0.0/p4-editorial-publishing).
-
-Production needs a real identity provider, governed authority changes, secure
-transport, protected sessions, and durable audit retention. Policy decisions do
-not replace transaction integrity or the workflow's content-version rules.
+[completed P4 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p4-editorial-publishing-v1.0.1/p4-editorial-publishing).
For a production stack, consider Agama Lab Policy Designer for policy authoring,
-Jans Auth for token issuance, and Lock Server for centralized decision logs.
+Jans Auth for issuing tokens, and Lock Server for centralized decision logs.
See [Cedarling production solutions](https://cedarling.dev/solutions).
-Next, P5 applies current-fact authorization to a different form of disclosure:
-fields, aggregates, and CSV exports that must not inherit broad page access.
-
----
-
-[^1]: A digest is a reproducible fingerprint of the normalized revision content. P4 compares it with the approval's digest, while current reviewer authority and revision state remain separate requirements.
-
-[^2]: The pinned [`cedarling_wasm` JavaScript API](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468) accepts a JSON-string request. Serialization does not authenticate the values; P4 loads them from its trusted server state.
+In P5, we'll check access to individual fields, aggregates, and CSV exports
+instead of granting it to everyone who can open a page.
-[^3]: Starting-checkpoint source: [`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p4-editorial-publishing/src/server/authorization.ts) defines the baseline gateway; [`src/server/service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p4-editorial-publishing/src/server/service.ts) calls it before service effects.
+
+Warning: This setup is for local practice
-[^4]: Complete tagged store: [`metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/policy-store/metadata.json), [`schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/policy-store/schema.cedarschema), and [`editorial.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/policy-store/policies/editorial.cedar).
+- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta.
+- Control who can change editorial permissions, protect sessions, and store audit logs. Keep database transaction and content-version checks alongside policy decisions.
+- These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
-[^5]: Complete enforcement source: [`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/src/server/authorization.ts), [`src/server/service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/src/server/service.ts), [`src/server/database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/src/server/database.ts), and [`app/actions.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/app/actions.ts).
+
-[^6]: Archive build and distribution: [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/shared/policy-store.mjs), [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/scripts/setup.ts), [`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/package.json), and [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/Dockerfile).
+[^1]: A digest is a reproducible fingerprint of the normalized revision content. P4 compares it with the approval's digest, while current reviewer authority and revision state remain separate requirements.
-[^7]: Completed [`next.config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p4-editorial-publishing-v1.0.0/p4-editorial-publishing/next.config.ts) keeps the Cedarling WASM package server-side.
+[^2]: The pinned [`cedarling_wasm` JavaScript API](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468) accepts a JSON-string request. Converting to JSON does not verify the values; P4 loads them from its trusted server state.
diff --git a/p4-editorial-publishing/pnpm-lock.yaml b/p4-editorial-publishing/pnpm-lock.yaml
index fb509b7..3fef5f0 100644
--- a/p4-editorial-publishing/pnpm-lock.yaml
+++ b/p4-editorial-publishing/pnpm-lock.yaml
@@ -126,144 +126,144 @@ packages:
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
- '@img/sharp-darwin-arm64@0.35.4':
- resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
+ '@img/sharp-darwin-arm64@0.35.5':
+ resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
- '@img/sharp-darwin-x64@0.35.4':
- resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
+ '@img/sharp-darwin-x64@0.35.5':
+ resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
- '@img/sharp-freebsd-wasm32@0.35.4':
- resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
+ '@img/sharp-freebsd-wasm32@0.35.5':
+ resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
engines: {node: '>=20.9.0'}
os: [freebsd]
- '@img/sharp-libvips-darwin-arm64@1.3.3':
- resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
+ resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
cpu: [arm64]
os: [darwin]
- '@img/sharp-libvips-darwin-x64@1.3.3':
- resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
+ '@img/sharp-libvips-darwin-x64@1.3.4':
+ resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-linux-arm64@1.3.3':
- resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
+ '@img/sharp-libvips-linux-arm64@1.3.4':
+ resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linux-arm@1.3.3':
- resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
+ '@img/sharp-libvips-linux-arm@1.3.4':
+ resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
cpu: [arm]
os: [linux]
- '@img/sharp-libvips-linux-ppc64@1.3.3':
- resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
+ resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
cpu: [ppc64]
os: [linux]
- '@img/sharp-libvips-linux-riscv64@1.3.3':
- resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
+ resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
cpu: [riscv64]
os: [linux]
- '@img/sharp-libvips-linux-s390x@1.3.3':
- resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
+ '@img/sharp-libvips-linux-s390x@1.3.4':
+ resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
cpu: [s390x]
os: [linux]
- '@img/sharp-libvips-linux-x64@1.3.3':
- resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
+ '@img/sharp-libvips-linux-x64@1.3.4':
+ resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
cpu: [x64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
- resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
+ resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-x64@1.3.3':
- resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
+ resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
cpu: [x64]
os: [linux]
- '@img/sharp-linux-arm64@0.35.4':
- resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
+ '@img/sharp-linux-arm64@0.35.5':
+ resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
- '@img/sharp-linux-arm@0.35.4':
- resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
+ '@img/sharp-linux-arm@0.35.5':
+ resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
- '@img/sharp-linux-ppc64@0.35.4':
- resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
+ '@img/sharp-linux-ppc64@0.35.5':
+ resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
- '@img/sharp-linux-riscv64@0.35.4':
- resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
+ '@img/sharp-linux-riscv64@0.35.5':
+ resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
- '@img/sharp-linux-s390x@0.35.4':
- resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
+ '@img/sharp-linux-s390x@0.35.5':
+ resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
- '@img/sharp-linux-x64@0.35.4':
- resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
+ '@img/sharp-linux-x64@0.35.5':
+ resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
- '@img/sharp-linuxmusl-arm64@0.35.4':
- resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
+ '@img/sharp-linuxmusl-arm64@0.35.5':
+ resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
- '@img/sharp-linuxmusl-x64@0.35.4':
- resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
+ '@img/sharp-linuxmusl-x64@0.35.5':
+ resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
- '@img/sharp-wasm32@0.35.4':
- resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
+ '@img/sharp-wasm32@0.35.5':
+ resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
engines: {node: '>=20.9.0'}
- '@img/sharp-webcontainers-wasm32@0.35.4':
- resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
+ '@img/sharp-webcontainers-wasm32@0.35.5':
+ resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
- '@img/sharp-win32-arm64@0.35.4':
- resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
+ '@img/sharp-win32-arm64@0.35.5':
+ resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
- '@img/sharp-win32-ia32@0.35.4':
- resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
+ '@img/sharp-win32-ia32@0.35.5':
+ resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
- '@img/sharp-win32-x64@0.35.4':
- resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
+ '@img/sharp-win32-x64@0.35.5':
+ resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
@@ -711,8 +711,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
- sharp@0.35.4:
- resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
+ sharp@0.35.5:
+ resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
@@ -723,8 +723,8 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
@@ -911,108 +911,108 @@ snapshots:
'@img/colour@1.1.0':
optional: true
- '@img/sharp-darwin-arm64@0.35.4':
+ '@img/sharp-darwin-arm64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-arm64': 1.3.3
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
optional: true
- '@img/sharp-darwin-x64@0.35.4':
+ '@img/sharp-darwin-x64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-x64': 1.3.3
+ '@img/sharp-libvips-darwin-x64': 1.3.4
optional: true
- '@img/sharp-freebsd-wasm32@0.35.4':
+ '@img/sharp-freebsd-wasm32@0.35.5':
dependencies:
- '@img/sharp-wasm32': 0.35.4
+ '@img/sharp-wasm32': 0.35.5
optional: true
- '@img/sharp-libvips-darwin-arm64@1.3.3':
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
optional: true
- '@img/sharp-libvips-darwin-x64@1.3.3':
+ '@img/sharp-libvips-darwin-x64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-arm64@1.3.3':
+ '@img/sharp-libvips-linux-arm64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-arm@1.3.3':
+ '@img/sharp-libvips-linux-arm@1.3.4':
optional: true
- '@img/sharp-libvips-linux-ppc64@1.3.3':
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-riscv64@1.3.3':
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-s390x@1.3.3':
+ '@img/sharp-libvips-linux-s390x@1.3.4':
optional: true
- '@img/sharp-libvips-linux-x64@1.3.3':
+ '@img/sharp-libvips-linux-x64@1.3.4':
optional: true
- '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
optional: true
- '@img/sharp-libvips-linuxmusl-x64@1.3.3':
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
optional: true
- '@img/sharp-linux-arm64@0.35.4':
+ '@img/sharp-linux-arm64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm64': 1.3.3
+ '@img/sharp-libvips-linux-arm64': 1.3.4
optional: true
- '@img/sharp-linux-arm@0.35.4':
+ '@img/sharp-linux-arm@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm': 1.3.3
+ '@img/sharp-libvips-linux-arm': 1.3.4
optional: true
- '@img/sharp-linux-ppc64@0.35.4':
+ '@img/sharp-linux-ppc64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-ppc64': 1.3.3
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
optional: true
- '@img/sharp-linux-riscv64@0.35.4':
+ '@img/sharp-linux-riscv64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-riscv64': 1.3.3
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
optional: true
- '@img/sharp-linux-s390x@0.35.4':
+ '@img/sharp-linux-s390x@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-s390x': 1.3.3
+ '@img/sharp-libvips-linux-s390x': 1.3.4
optional: true
- '@img/sharp-linux-x64@0.35.4':
+ '@img/sharp-linux-x64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-x64': 1.3.3
+ '@img/sharp-libvips-linux-x64': 1.3.4
optional: true
- '@img/sharp-linuxmusl-arm64@0.35.4':
+ '@img/sharp-linuxmusl-arm64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
optional: true
- '@img/sharp-linuxmusl-x64@0.35.4':
+ '@img/sharp-linuxmusl-x64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.3.3
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
optional: true
- '@img/sharp-wasm32@0.35.4':
+ '@img/sharp-wasm32@0.35.5':
dependencies:
'@emnapi/runtime': 1.11.3
optional: true
- '@img/sharp-webcontainers-wasm32@0.35.4':
+ '@img/sharp-webcontainers-wasm32@0.35.5':
dependencies:
- '@img/sharp-wasm32': 0.35.4
+ '@img/sharp-wasm32': 0.35.5
optional: true
- '@img/sharp-win32-arm64@0.35.4':
+ '@img/sharp-win32-arm64@0.35.5':
optional: true
- '@img/sharp-win32-ia32@0.35.4':
+ '@img/sharp-win32-ia32@0.35.5':
optional: true
- '@img/sharp-win32-x64@0.35.4':
+ '@img/sharp-win32-x64@0.35.5':
optional: true
'@janssenproject/cedarling_wasm@0.0.468': {}
@@ -1284,7 +1284,7 @@ snapshots:
'@next/swc-win32-arm64-msvc': 16.3.6
'@next/swc-win32-x64-msvc': 16.3.6
'@playwright/test': 1.63.0
- sharp: 0.35.4(@types/node@24.19.0)
+ sharp: 0.35.5(@types/node@24.19.0)
transitivePeerDependencies:
- '@babel/core'
- '@types/node'
@@ -1317,13 +1317,13 @@ snapshots:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
postcss@8.5.28:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
react-dom@19.3.0(react@19.3.0):
dependencies:
@@ -1358,43 +1358,43 @@ snapshots:
semver@7.8.5:
optional: true
- sharp@0.35.4(@types/node@24.19.0):
+ sharp@0.35.5(@types/node@24.19.0):
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
- '@img/sharp-darwin-arm64': 0.35.4
- '@img/sharp-darwin-x64': 0.35.4
- '@img/sharp-freebsd-wasm32': 0.35.4
- '@img/sharp-libvips-darwin-arm64': 1.3.3
- '@img/sharp-libvips-darwin-x64': 1.3.3
- '@img/sharp-libvips-linux-arm': 1.3.3
- '@img/sharp-libvips-linux-arm64': 1.3.3
- '@img/sharp-libvips-linux-ppc64': 1.3.3
- '@img/sharp-libvips-linux-riscv64': 1.3.3
- '@img/sharp-libvips-linux-s390x': 1.3.3
- '@img/sharp-libvips-linux-x64': 1.3.3
- '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
- '@img/sharp-libvips-linuxmusl-x64': 1.3.3
- '@img/sharp-linux-arm': 0.35.4
- '@img/sharp-linux-arm64': 0.35.4
- '@img/sharp-linux-ppc64': 0.35.4
- '@img/sharp-linux-riscv64': 0.35.4
- '@img/sharp-linux-s390x': 0.35.4
- '@img/sharp-linux-x64': 0.35.4
- '@img/sharp-linuxmusl-arm64': 0.35.4
- '@img/sharp-linuxmusl-x64': 0.35.4
- '@img/sharp-webcontainers-wasm32': 0.35.4
- '@img/sharp-win32-arm64': 0.35.4
- '@img/sharp-win32-ia32': 0.35.4
- '@img/sharp-win32-x64': 0.35.4
+ '@img/sharp-darwin-arm64': 0.35.5
+ '@img/sharp-darwin-x64': 0.35.5
+ '@img/sharp-freebsd-wasm32': 0.35.5
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
+ '@img/sharp-libvips-darwin-x64': 1.3.4
+ '@img/sharp-libvips-linux-arm': 1.3.4
+ '@img/sharp-libvips-linux-arm64': 1.3.4
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
+ '@img/sharp-libvips-linux-s390x': 1.3.4
+ '@img/sharp-libvips-linux-x64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
+ '@img/sharp-linux-arm': 0.35.5
+ '@img/sharp-linux-arm64': 0.35.5
+ '@img/sharp-linux-ppc64': 0.35.5
+ '@img/sharp-linux-riscv64': 0.35.5
+ '@img/sharp-linux-s390x': 0.35.5
+ '@img/sharp-linux-x64': 0.35.5
+ '@img/sharp-linuxmusl-arm64': 0.35.5
+ '@img/sharp-linuxmusl-x64': 0.35.5
+ '@img/sharp-webcontainers-wasm32': 0.35.5
+ '@img/sharp-win32-arm64': 0.35.5
+ '@img/sharp-win32-ia32': 0.35.5
+ '@img/sharp-win32-x64': 0.35.5
'@types/node': 24.19.0
optional: true
siginfo@2.0.0: {}
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
stackback@0.0.2: {}
diff --git a/p5-dataguard/README.md b/p5-dataguard/README.md
index c083636..dfa52d2 100644
--- a/p5-dataguard/README.md
+++ b/p5-dataguard/README.md
@@ -2,32 +2,36 @@

-P5 is a workforce analytics application that compiles a bounded query plan to
-parameterized SQLite and creates expiring CSV exports. It shows how Cedarling
-centralizes independent authorization for rows, fields, aggregates, export
-creation, and downloads.
+P5 is a workforce analytics application. It compiles bounded query plans to
+parameterized SQLite queries and creates expiring CSV exports. Cedarling checks
+permission separately for fields, rows, aggregates, export creation, downloads,
+and revocation.
-Cedarling authorizes each field, query, aggregate, and export operation on the
-server. Authentication identifies the analyst; policies decide which data and
-effects that analyst may access.
+The server authenticates the analyst and enforces those decisions before
+releasing data. Follow the [tutorial](docs/tutorials.md) to add the checks to
+the [starting application](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p5-dataguard).
## Architecture
-```text
-Amina / Leah / Theo ── sign in ──→ Tutorial IdP
- │
- └── data request ──→ React UI → Node.js + Hono API (PEP)
- │ principal + query/export facts
- ▼
- Cedarling PDP
- │ │
- DENY ALLOW → query compiler → SQLite / CSV
+```mermaid
+flowchart TD
+ accTitle: Query and export authorization in the completed API
+ accDescr: Each query or export operation loads current facts and asks server-side Cedarling. The API enforces the decision and rechecks facts before releasing data or changing an export.
+ Query["React: query or create export"] --> Plan["API: validate plan and load current analyst, fields and group counts"]
+ Export["Download or revoke request"] --> Saved["API: reload analyst and saved export"]
+ Plan --> PDP["Embedded Cedarling: unsigned evaluation"]
+ Saved --> PDP
+ PDP --> Check["API enforces decision"]
+ Check -->|"DENY or failure"| Stop["No protected result or change"]
+ Check -->|"ALLOW"| Effect["Recheck facts and perform the authorized operation"]
```
## Prerequisites
-- Docker Desktop or Docker Engine with Compose, or
-- Node.js 24.21 or newer within 24.x, pnpm 10, and the project-local tutorial identity provider.
+- To run with Docker: Docker Desktop or Docker Engine with Compose.
+- For native development and checks: Node.js 24.21 or newer within 24.x and pnpm 10.
+
+Both startup paths include the project's tutorial identity provider.
## Run
@@ -48,8 +52,8 @@ pnpm install --frozen-lockfile
pnpm dev
```
-`pnpm dev` starts this project’s IdP and application together.
-Setup synchronizes the application listen port with its registered URL and
+`pnpm dev` starts this project's IdP and application together.
+Setup keeps the application port consistent with its registered URL and
preserves your workforce data and exports.
For a fresh `pnpm build` followed by `pnpm start`, run `pnpm run setup` first.
@@ -60,28 +64,24 @@ Use `pnpm dev -- --reset` only when you want to restore the tutorial fixtures.
## Exercise
-The business workflow explores workforce analytics and exports:
+Each account has a different reason to use the data:
-- **Amina** — Tenant A operational fields and tenant ID for `support`; no personal
- or compensation fields and no exports.
-- **Leah** — All Tenant A fields for `finance-review`; creates and manages only
- her own exports.
-- **Theo** — Tenant B counts for `external-audit`, grouped by operational
- attributes or tenant ID; no employee IDs, personal/compensation fields, rows,
- or exports.
+- Amina uses Tenant A operational fields and tenant ID for `support`. She has
+ no personal or compensation fields and cannot export data.
+- Leah uses all Tenant A fields for `finance-review`. She creates and manages
+ only her own exports.
+- Theo uses Tenant B counts for `external-audit`, grouped by operational
+ attributes or tenant ID. He has no employee IDs, personal or compensation
+ fields, rows, or exports.
Every plan must explicitly filter `tenantId` with `eq` and the analyst's tenant.
Every released aggregate group must contain at least five records, including
aggregate exports.
-The browser exposes a tenant-equality filter and starts aggregates with No grouping.
-Keep your tenant selected and choose your account's purpose before running a plan.
-Changing or omitting the tenant constraint demonstrates denial; the API still
-validates its bounded plan grammar and never silently adds a tenant filter.
-
-The server previews permissions for the exact selected plan. Denied actions
-remain visible but disabled with an explanation; no result rows or CSV files
-are produced by a preview. Every actual request is authorized again.
+Choose your account's purpose and keep its tenant filter before running a plan.
+The server previews the selected plan without returning rows or creating CSV
+files. Denied actions stay disabled with an explanation; each execution still
+requires a fresh decision. The API never silently adds a tenant filter.
1. As Amina, run the default Tenant A support query: allowed. Change the tenant
to `tenant-b`: Run query becomes disabled. Salary, bonus, names, and emails are absent from her
@@ -107,9 +107,7 @@ an expired download is rejected even when the browser still holds its reference.
Editing a plan does not replace an already displayed result. Export controls
remain bound to the last completed query, not the next plan under construction.
-`pnpm dev` prepares and supervises the IdP and application. For compiled
-production, run `pnpm build`, keep the IdP running separately, and run
-`pnpm start`. Reset with `pnpm reset`, then sign in again. For Docker:
+Reset the sample data with `pnpm reset`, then sign in again. For Docker:
```bash
docker compose exec dataguard node --env-file=/run/config/app.env scripts/reset.ts
@@ -121,40 +119,25 @@ The readable `policy-store/` uses namespace `P5DataGuard`. The shared builder
validates and packages it into ignored `.local/policy-store.cjar` during setup,
build, and tests. The server logs its version and SHA-256 at startup.
-`src/server/authorization.ts` calls `authorizeUnsigned()` directly, using the
-current SQLite analyst and validated plan. Field inspection uses
-`authorizeUnsignedBatch()`. OAuth tokens stay server-side; React receives only
-authorized field metadata and bounded results.
-
-`POST /api/authorization` returns only UI action availability, using the same
-policy requests and bounded cardinality checks as execution. React discards
-outdated preview responses and disables controls while checking or unavailable.
-No browser-side role rules or preview decision can authorize a protected effect.
-
-Before an aggregate decision, SQLite computes group counts using the requested
-grouping column; the application receives only cardinalities, not column values.
-Protected result values are returned only after ALLOW. After ALLOW,
-the server rechecks current facts in the transaction that performs the effect.
-A changed entitlement, group size, or export produces a conflict, not a reused
-decision. Policy errors fail closed.
-
-Server logs are formatted JSON. `authorization.context` links the application
-request, actor, capability, and preview/enforcement phase to the Cedarling request
-ID. The native decision follows with full `diagnostics.reason` and `errors`,
-including each allowed or denied field-inspection decision. An empty reason on
-DENY means no permit matched; it is not by itself an engine failure.
-
-`data.query.completed`, `data.aggregate.completed`, `export.created`, and
-`export.revoked` record completed work, separately from ALLOW. `export.download.prepared`
-means the server prepared the response, not that the browser saved a file.
-`request.failed` records a bounded error category and HTTP status. Browser console
-objects carry the operation, status, and request ID; Cedarling runs only on the
-server. Logs exclude tokens, download references, and workforce values.
-
-Cancelled requests stop before protected effects. Export revocation and expiry
-commit before CSV cleanup; a cleanup failure never restores download access.
-Lifecycle requests, startup, and reset retry orphan cleanup without deleting
-referenced exports or unrelated files.
+[`src/server/authorization.ts`](src/server/authorization.ts) uses server-side
+unsigned evaluation with the current SQLite analyst and validated plan; field
+inspection uses a batch. React receives authorized field metadata and results,
+never OAuth tokens. Permission previews cannot authorize an actual request.
+
+Aggregate decisions use current group counts without releasing column values.
+The server rechecks authorized facts in the transaction that performs the effect;
+changed facts produce a conflict and policy errors fail closed. See the
+[API integration](docs/tutorials.md#add-cedarling-to-the-api) for each enforcement point.
+
+JSON logs separate Cedarling decisions from completed operations. A prepared
+download response does not prove the browser saved the file. The
+[log guide](docs/tutorials.md#read-the-query-and-export-logs) covers correlation
+and failure categories. Tokens, download references, and workforce values stay
+out of logs.
+
+Revocation and expiry prevent downloads even if CSV cleanup fails. Cleanup
+retries preserve referenced exports and unrelated files; cancelled requests
+stop before protected effects.
The five-record rule illustrates one disclosure constraint, not comprehensive
protection against statistical inference. Cedarling memory logs expire after
diff --git a/p5-dataguard/docs/tutorials.md b/p5-dataguard/docs/tutorials.md
index d398723..52c6374 100644
--- a/p5-dataguard/docs/tutorials.md
+++ b/p5-dataguard/docs/tutorials.md
@@ -10,39 +10,51 @@ lastVerified: 2026-10-01T09:46:20Z
# Protect Sensitive Data Exports with Cedarling
+Good to have you here! We're working with a reporting app used by support staff,
+finance staff, and external reviewers. They need different views of the data,
+so we'll use Cedarling to check what each person may query or export.
+
+Amina needs operational records for support, but the starting API also returns
+salary and bonus data when she requests it. We'll use her salary request as our
+first example, while keeping her ordinary support queries available.
+
+We'll also check which field names users may see and which fields they may query,
+filter, or group by. Queries must use the caller's tenant and a purpose allowed
+for their role. External reviewers may only receive aggregates, with at least
+five records per returned group. Creating an export needs a separate decision;
+downloading or revoking it requires its owner to still have the finance role.
+Leah's allowed finance reports and exports should keep working.
+
+## Build the integration or try the finished app
+
+- To build the integration, start with [Run the starting application](#run-the-starting-application), then add the policies and server checks.
+- To try the finished app, run the [complete tagged project](https://github.com/GluuFederation/cedarling-tutorials/tree/p5-dataguard-v1.0.1/p5-dataguard) using its README, then go to [Check queries and exports for each user](#check-queries-and-exports-for-each-user). This version already uses Cedarling.
+
-Project source and prerequisites
+What you'll need
-- [Complete P5 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p5-dataguard-v1.0.0/p5-dataguard) and [starting checkpoint](https://github.com/GluuFederation/cedarling-tutorials/tree/21b0832be4b31271320df992d04e9d97667d0e38/p5-dataguard).
-- Install Docker with Compose, or Node.js 24.21+ within 24.x and pnpm 10.17.1. The project supplies its own tutorial identity provider.
-- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta.
-- I prepared these steps on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
+- Git, Node.js 24.21+ within 24.x, and pnpm 10.17.1 for the coding steps. The project supplies its own tutorial identity provider (IdP).
+- Docker with Compose is optional for the baseline or finished example. Use native Node.js for the coding steps.
+- Familiarity with TypeScript, HTTP requests, sessions, and basic SQL.
- New to Cedarling? [Read the short introduction](https://cedarling.dev/learn/what-is-cedarling) when you need it.
-- Keep the official [Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and [Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html) references handy for the policy-store steps.
+- Keep the [Cedar policy syntax](https://docs.cedarpolicy.com/policies/syntax-policy.html) and [Cedar schema syntax](https://docs.cedarpolicy.com/schema/human-readable-schema.html) references handy while editing policies.
-Paths are relative to `p5-dataguard/` unless stated otherwise. Use fresh
-synthetic fixtures for the examples; a changed query or export state can change
-later results.
+Copy whole files from GitHub's raw-file view into your baseline checkout; don't
+switch to the finished tag. The short examples aren't complete replacements.
+Create missing parent directories. Paths and commands are relative to
+`p5-dataguard/`; repository-level `shared/` files go one directory above it.
-## Does permission to open a dashboard include every field?
+## Who should see which data?

-_Amina, Leah, and Theo have different authorized views of the same dataset._
-
-A support analyst needs operational records. A finance lead needs compensation.
-An external reviewer needs aggregate evidence[^1], not employee-level records.
-Giving all three access to one page does not make the underlying data equally
-available to them.
-
-I'll start with a direct request that bypasses the field picker, then show how
-the server can authorize each kind of data release.
+_Amina, Leah, and Theo may see different parts of the same dataset._
P5 is a React application with a Hono Node.js API and SQLite. The browser
-submits a bounded query plan, not SQL. We will use Cedarling to protect field
-metadata, row queries, aggregates, and the full CSV export lifecycle.
+submits a query plan with limits on what it can request, not SQL. It can request
+rows or aggregates[^1], such as a count of employees by department.
- **Amina**, Tenant A support analyst: operational fields and tenant ID for
`support`; no personal or compensation fields and no exports.
@@ -51,43 +63,37 @@ metadata, row queries, aggregates, and the full CSV export lifecycle.
- **Theo**, Tenant B external reviewer: permitted count aggregates for
`external-audit`; no rows, employee IDs, personal/compensation fields, or exports.
-An aggregate must also contain at least five records in every released group.
-A valid purpose and role do not override that disclosure constraint.
-
-```text
-React query plan --> Node.js API (PEP)
- |
- authenticate + validate plan
- |
- current analyst + field catalog
- aggregate counts, when needed
- |
- Cedarling PDP <-- policy store
- |
- DENY / failure --> no result or export
- |
- ALLOW
- v
- transaction: recheck facts
- |
- parameterized query --> rows / aggregate / CSV
-
-Download or revoke --> reload saved export --> new decision --> effect
+An aggregate must include at least five records in every returned group.
+This minimum applies even when the purpose and role are allowed.
+
+```mermaid
+flowchart TD
+ accTitle: Query and export authorization in the completed API
+ accDescr: Each query or export operation loads current facts and asks server-side Cedarling. The API enforces the decision and rechecks facts before releasing data or changing an export.
+ Query["React: query or create export"] --> Plan["API: validate plan and load current analyst, fields and group counts"]
+ Export["Download or revoke request"] --> Saved["API: reload analyst and saved export"]
+ Plan --> PDP["Embedded Cedarling: unsigned evaluation"]
+ Saved --> PDP
+ PDP --> Check["API enforces decision"]
+ Check -->|"DENY or failure"| Stop["No protected result or change"]
+ Check -->|"ALLOW"| Effect["Recheck facts and perform the authorized operation"]
```
-Cedarling runs only on the server. It receives a trusted application principal
-through `authorizeUnsigned()`. OIDC has already authenticated that principal;
-“unsigned” does not mean the browser may invent its role or tenant.
+Cedarling runs only on the server, using `authorizeUnsigned()`. The bundled
+Node.js `oidc-provider` authenticates the user first; SQLite supplies the
+current analyst's role and tenant. "Unsigned" describes the authorization
+request built from those facts. The browser cannot supply its own identity or
+permissions.
-## Reproduce a sensitive-field disclosure
+## Request salary data as a support analyst

-_The baseline field picker exposes Salary and Bonus; a direct API request tests the same server boundary independently of that UI._
+_The starting app lists Salary and Bonus; a direct API request checks access without using the field picker._
-### Start a separate baseline
+### Run the starting application
-Use disposable tutorial data in a new checkout:
+Use a new checkout with its own sample data:
```bash
git clone https://github.com/GluuFederation/cedarling-tutorials.git cedarling-p5
@@ -102,8 +108,7 @@ Open `http://localhost:17005`. The development IdP is at
`amina`; enter it if the field is empty. Use a non-empty password such as
`cedarling-is-awesome`, and approve access.
-For native development instead, use Node.js 24.21 or newer within 24.x and pnpm
-10.17.1. From the project directory:
+For native startup, run these commands from the project directory:
```bash
pnpm --dir ../shared/identity-provider install --frozen-lockfile
@@ -137,45 +142,46 @@ const response = await fetch("/api/query/rows", {
console.log(response.status, await response.json());
```
-The baseline returns **200** with compensation columns. This is Amina's real
-session and a valid same-origin request. SQL parameterization prevents values
-from becoming SQL instructions, but does not decide whether Amina may see salary.
+The baseline returns **200** with salary and bonus columns. This uses Amina's
+real session and a valid same-origin request. Parameterized SQL keeps input values
+separate from SQL instructions, but does not decide whether Amina may see salary.
-The same baseline also permits cross-tenant queries, small-group aggregates,
-and another user's export access. Its `e2e/sensitive-data-gaps.e2e.ts` exercises
-those gaps. Keep this exact compensation request for the final comparison rather
-than relying only on a hidden checkbox.
+The baseline also permits cross-tenant queries, small-group aggregates, and
+access to another user's exports. Keep this request to repeat after integration.
-Capture the response using synthetic data only. Stop the baseline before
+Capture the response using fictional data only. Stop the baseline before
integrating; for Docker use `Ctrl+C`, then `docker compose down` without deleting
-the volume.
+the volume. If you started with Docker, install the native dependencies using
+the commands above before the coding steps.
-## Prepare the existing data workflow for authorization
+## Where should we check permission?
-No separate dashboard or export feature needs adding before Cedarling. The
-starting application already authenticates requests, checks CSRF, validates a
-bounded query grammar, uses parameterized SQL, and stores exports with expiry
-and revocation. Its `evaluatePlan()` compiles and executes the query before a
-permissive trace; that trace is not a permission decision:[^3]
+Open the baseline's
+[`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p5-dataguard/src/server/app.ts)
+and find `evaluatePlan()`. After authentication, CSRF, and input validation,
+it executes the query here:
```ts
// src/server/app.ts (starting checkpoint)
const evaluation = database.evaluate(compiled, compileCardinalityQuery(plan));
```
-Keep the input and SQL safeguards. The integration must move the authorization
-gate before protected row or aggregate values are read or released. Only a
-bounded group-cardinality probe may precede ALLOW, to supply trusted facts for
-the aggregate decision. Do not add a browser role table in place of that server
-boundary.
+The log printed afterward has not checked whether Amina may read
+those fields. We'll ask Cedarling before this query reads protected values,
+while keeping input validation, parameterized SQL, and export expiry and
+revocation checks.
-## Design permission for the requested data and its derivatives
+Aggregates need one query first: a count of the records in each group, within the
+plan's limits. Those counts supply policy facts. The requested results must still
+wait for `ALLOW`.
+
+## Decide which queries and exports to allow

-_Each response surface gets its own authorization decision before disclosure._
+_Queries and exports need separate decisions, even when they use the same data._
-### Translate responsibilities into a policy store
+### Create the policy store
Use the [directory-based policy-store format](https://docs.jans.io/stable/cedarling/reference/cedarling-policy-store/#2-new-directory-based-format):
@@ -189,53 +195,55 @@ policy-store/
exports.cedar
```
-Create these five files from the completed policy store.[^4]
+Create the complete policy-store files from the pinned version:
+
+- [`policy-store/metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/metadata.json)
+- [`policy-store/schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/schema.cedarschema)
+- [`policy-store/policies/fields.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/fields.cedar)
+- [`policy-store/policies/plans.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/plans.cedar)
+- [`policy-store/policies/exports.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/exports.cedar)
+
Use the store's metadata and version `1.0.0`. Namespace `P5DataGuard` contains
four entity types: `Analyst`, `Dataset`, `Field`, and `Export`. OIDC and SQLite
supply identity and current facts, so this store needs no trusted issuers,
default entities, templates, or custom issuers.
-| Design question | P5 answer |
-| ------------------------------------------------ | --------------------------------------------------------------------------------------------- |
-| Who acts? | `Analyst`: current database ID, tenant, and role |
-| Which fields may be discovered? | A `Field` with its server-owned name and classification |
-| What do plans target? | `Dataset::"workforce"` |
-| What does a saved download or revocation target? | `Export` with current owner, tenant, and purpose |
-| What does the request ask for? | Plan kind, purpose, tenant constraint, fields, and classifications |
-| Which additional fact protects aggregates? | Current minimum count among released groups |
-| What protects saved artifacts? | Current finance authority, same tenant, ownership, plus application-enforced expiry and state |
+| Design question | P5 answer |
+| ------------------------------------------------ | ------------------------------------------------------------------------------------------ |
+| Who acts? | `Analyst`: current database ID, tenant, and role |
+| Which fields may be discovered? | A `Field` with its server-owned name and classification |
+| What do plans target? | `Dataset::"workforce"` |
+| What does a saved download or revocation target? | `Export` with current owner, tenant, and purpose |
+| What does the request ask for? | Plan kind, purpose, tenant constraint, fields, and classifications |
+| Which additional fact protects aggregates? | Current minimum count among released groups |
+| What protects saved exports? | Current finance role, same tenant, ownership, plus application checks for expiry and state |
-Field classifications come from the closed server catalog, not from the browser.
-Include fields used for filters and grouping as well as returned columns. A
-hidden field can still leak information through a predicate or a group key.
+Field classifications come from the server's fixed catalog, not from the browser.
+Include fields used for filters and grouping as well as returned columns.
+Filtering or grouping by a hidden field can still reveal information about it.
The plan must explicitly contain `tenantId eq `. The server does
not silently add or repair that filter. A different field, operator, or omitted
-filter does not establish the required tenant constraint.
+filter does not meet the tenant requirement.
-### Identify every enforcement boundary
+### Choose an action and resource for each operation
All requests use the current database analyst as principal. Actions below use
the `P5DataGuard::Action` namespace.
-| Capability | Action | Resource | Context | Effect waiting for ALLOW |
-| ----------------- | ---------------- | -------------------- | ----------------------------------------------------- | --------------------------------------- |
-| `dataset.inspect` | `InspectDataset` | Each candidate field | Empty | Return field metadata to React |
-| `data.query` | `Query` | Workforce dataset | Validated row-plan facts | Return bounded rows |
-| `data.aggregate` | `Aggregate` | Workforce dataset | Plan facts and current minimum group size | Return aggregate values |
-| `data.export` | `CreateExport` | Workforce dataset | Saved candidate plan facts; group size for aggregates | Materialize bounded CSV |
-| `export.download` | `DownloadExport` | Current saved export | Empty | Prepare CSV response |
-| `export.revoke` | `RevokeExport` | Current saved export | Empty | Commit revocation and clean up its file |
+| Capability | Action | Resource | Context | Effect waiting for ALLOW |
+| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------- | ----------------------------------------------------- | --------------------------------------- |
+| `dataset.inspect` | [`InspectDataset`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/fields.cedar#L2 "inspect-fields") | Each candidate field | Empty | Return field metadata to React |
+| `data.query` | [`Query`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/plans.cedar#L2 "authorized-plan") | Workforce dataset | Validated row-plan facts | Return rows within the plan's limit |
+| `data.aggregate` | [`Aggregate`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/plans.cedar#L2 "authorized-plan") | Workforce dataset | Plan facts and current minimum group size | Return aggregate values |
+| `data.export` | [`CreateExport`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/plans.cedar#L2 "authorized-plan") | Workforce dataset | Saved candidate plan facts; group size for aggregates | Create CSV within the plan's limits |
+| `export.download` | [`DownloadExport`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/exports.cedar#L2 "manage-own-finance-export") | Current saved export | Empty | Prepare CSV response |
+| `export.revoke` | [`RevokeExport`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/policy-store/policies/exports.cedar#L2 "manage-own-finance-export") | Current saved export | Empty | Commit revocation and clean up its file |
-Before an aggregate decision, the server obtains bounded group counts from SQLite.
-That probe returns cardinalities to the application, not protected group values.
-Protected result values wait for ALLOW. This is a deliberate source of trusted
-policy facts, not a query-then-redact design.
+### Check the plan when querying or exporting
-### Share the plan rule across query and export
-
-The complete rule in `policies/plans.cedar` prevents export from bypassing the
-query's tenant, field, purpose, or group-size restrictions:
+The rule in `policies/plans.cedar` checks tenant, fields, purpose, and group size
+for exports as well as queries:
```cedar
// policy-store/policies/plans.cedar
@@ -265,12 +273,21 @@ when {
};
```
-The separate `inspect-fields` policy limits field metadata by the same role and
-classification boundaries. Inspection is guidance, not permission for later
-queries. Every submitted plan is still checked independently.
+For Amina's salary request, the tenant matches, her role is `Support analyst`,
+and the purpose is `support`. But `salary` and `bonus` belong to the
+`compensation` classification. The support rule only accepts `operational` and
+`tenant`, so it denies the request.
+
+Leah's `Finance lead` role with `finance-review` permits compensation fields.
+She must still request her own tenant, and any aggregate she requests must
+meet the five-record minimum. The role conditions do not bypass those
+shared conditions.
+
+The `inspect-fields` policy limits field metadata by role and classification.
+This guides the field picker; each submitted query still needs its own check.
-For saved artifacts, `policies/exports.cedar` requires ownership and current
-finance authority:
+For saved exports, `policies/exports.cedar` requires ownership and a current
+finance role:
```cedar
// policy-store/policies/exports.cedar
@@ -288,17 +305,17 @@ when {
};
```
-An opaque reference locates an export; it is not permission to download it.
-Expiry, reference validation, file integrity, and lifecycle state remain
-application checks. No matching permit gives DENY.
+Knowing an export's download reference does not satisfy its ownership and finance
+conditions. The application also checks expiry, reference validity, file
+integrity, and whether the export is still active. No matching permit gives DENY.
-## Integrate Cedarling before data leaves the server
+## Add Cedarling to the API

_The API enforces the decision; browser controls are guidance, not authority._
-### Initialize the embedded runtime
+### Build the archive and load Cedarling
Install pinned dependencies from P5:
@@ -307,16 +324,22 @@ pnpm add --save-exact @janssenproject/cedarling_wasm@0.0.468 fflate@0.8.3
pnpm add --save-dev --save-exact @cedar-policy/cedar-wasm@4.12.0
```
-Add the integration's repository-level `shared/policy-store.mjs` and declaration,
-which are absent from the starting commit. Run the builder to create the archive:
+Save the repository-level archive builder and its declaration:
+
+- [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/shared/policy-store.mjs)
+- [`shared/policy-store.d.mts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/shared/policy-store.d.mts)
+
+Run the builder to create the archive:
```bash
node ../shared/policy-store.mjs
```
-Keep readable policy source in Git and ignored `.local/policy-store.cjar` as the
-runtime artifact. In `src/server/authorization.ts`, initialize one instance using
-the [pinned SDK](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468):
+The command must finish without validation errors and create
+`.local/policy-store.cjar`. Keep the readable policy source in Git and the
+generated archive ignored. The complete `src/server/authorization.ts` in the
+next step initializes one [Cedarling](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468)
+instance:
```ts
// src/server/authorization.ts
@@ -335,25 +358,35 @@ const cedarling = await initFromArchiveBytes(
);
```
-`archivePath` resolves to this project's `.local/policy-store.cjar` by default;
-it is a server-side artifact, not a browser path. Log its version and SHA-256. Have
-`src/server/main.ts` create the authorization dependency, pass it to `buildApp()`,
-and close Cedarling through `shutDown()` during controlled application shutdown.
-Replace the permissive trace path rather than retaining an alternative unguarded
-execution path.
+`archivePath` defaults to `.local/policy-store.cjar` on the server. The module
+logs its version and SHA-256. `src/server/main.ts` creates the authorization
+functions, passes them to `buildApp()`, and calls `shutDown()` when shutting down.
+
+### Pass the analyst and query facts to Cedarling
-### Build requests from the exact validated plan
+Save these complete files together to connect the permission checks:
-The server validates the closed plan grammar before authorization. Its field-name
-set includes selected columns or aggregate operands/grouping, plus any filter
-field. From the server catalog, derive the set of classifications. Include
+- [`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/authorization.ts)
+- [`src/server/errors.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/errors.ts)
+- [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/app.ts)
+- [`src/server/database.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/database.ts)
+- [`src/server/query.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/query.ts)
+- [`src/server/export-service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/export-service.ts)
+- [`src/server/main.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/main.ts)
+- [`src/server/config.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/config.ts)
+- [`src/shared/contracts.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/shared/contracts.ts)
+
+Remove `src/server/permissive-trace.ts`; the completed handlers no longer use it.
+
+Before authorization, the server checks that the plan uses only supported forms.
+It collects fields used in selected columns, calculations, grouping, and filters.
+It gets their classifications from the server catalog and includes
`tenant_id` only when the submitted filter is an exact tenant equality.
-The following expanded request illustrates the direct Cedarling call inside
-the authorization function. `analyst` comes from the current database session;
-`action` is the mapped action; `resource` and `context` are constructed from the
-request table. The completed file factors out `principal(analyst)` and logs
-through `logDecision()`:
+In this call, `analyst` comes from the current database session; `action`,
+`resource`, and `context` follow the request table. The full file builds the
+principal with `principal(analyst)` and logs
+decisions through `logDecision()`:
```ts
// src/server/authorization.ts
@@ -382,16 +415,16 @@ if (result.response.diagnostics.errors.length > 0) {
return result.decision === true;
```
-`authorizeUnsigned()` expects a JSON string, so `JSON.stringify()` serializes
-the server-validated decision request for Cedarling.[^2] The log formatting
+`authorizeUnsigned()` expects a JSON string, so `JSON.stringify()` converts
+the server-validated request to that format.[^2] The log formatting
call prints nested reasons for this local exercise.
-Add the integration's `AuthorizationError` type in `src/server/errors.ts`. The surrounding
-catch maps Cedarling failure to the same bounded unavailable outcome. A valid false
-decision instead becomes `403 authorization_denied` at the protected route.
+`AuthorizationError` comes from `src/server/errors.ts`, copied above. The catch
+handler maps Cedarling failures to `503 authorization_unavailable`. A valid
+false decision becomes `403 authorization_denied` at the protected route.
-For Amina's compensation request, the resource is `Dataset::"workforce"` and the
-constructed context has this shape:
+For Amina's salary request, the resource is `Dataset::"workforce"` and the
+context has this shape:
```json
{
@@ -403,54 +436,103 @@ constructed context has this shape:
}
```
-Set order is not significant. Compensation prevents the support permit from
-matching. For an aggregate, add server-computed `minimum_group_size`; do not
-accept a count asserted by the browser.
+Set order is not significant. For an aggregate, the context also contains
+`minimum_group_size`, computed by the server rather than accepted from the
+browser.
-For dataset inspection, call `authorizeUnsignedBatch()` with this principal and
-one `InspectDataset` item per catalog field. Require complete results, check
-`item.is_ok`, unwrap valid results, reject diagnostics errors, and return only
-allowed metadata. Log allowed and denied items; a batch failure must not expose
-unchecked fields.
+Dataset inspection uses `authorizeUnsignedBatch()` with this principal and
+one `InspectDataset` item per catalog field. The module checks that every item
+returned a result, checks `item.is_ok` and diagnostic errors, and returns only
+allowed metadata. It logs both allowed and denied items; a failed batch returns
+no unchecked fields.
-### Gate execution and recheck current facts
+### Check permission before running the query
-In `src/server/app.ts`, the common `executePlan()` path validates current facts,
-asks Cedarling, then enters the transaction that executes the query or creates
-an export. Within that transaction, recheck the session's analyst/entitlements
-and the aggregate cardinality used by the decision. A change produces
-`409 authorization_state_changed`, not reuse of an earlier ALLOW.
+Row queries, aggregates, and export creation use `executePlan()` in
+`src/server/app.ts`. It requires permission before `database.execute()`:
-Compile and execute protected-value SQL only after authorization. Keep fixed SQL
-identifiers and bound values in `src/server/query.ts`. SQL safety and access
-control solve different problems.
+```ts
+// src/server/app.ts
+requireActiveRequest(context);
+const { cardinality, minimumGroupSize } = planFacts(plan);
+if (
+ !(await authorization.authorize({
+ requestId,
+ analyst: session.user,
+ capability,
+ plan,
+ ...(minimumGroupSize !== undefined ? { minimumGroupSize } : {}),
+ }))
+)
+ throw new AuthorizationError(403, "authorization_denied");
+return database.withCurrentSession(
+ getCookie(context, sessionCookie) ?? "",
+ config,
+ session,
+ () => {
+ // Only the bounded cardinality probe precedes ALLOW; protected values do not.
+ requireActiveRequest(context);
+ if (
+ cardinality &&
+ database.minimumGroupSize(cardinality) !== minimumGroupSize
+ )
+ throw new AuthorizationError(409, "authorization_state_changed");
+ const compiled = compileQuery(plan);
+ return effect(compiled.outputColumns, database.execute(compiled));
+ },
+);
+```
+
+`planFacts()` supplies the counts needed to check an aggregate. If authorization
+denies or throws, execution stops before the protected query. After `ALLOW`,
+`withCurrentSession()` checks the session and analyst's current permissions
+inside the transaction. The count is checked again there too. If the session or
+access token expired while waiting for the decision, or the analyst or count
+changed, it throws `409 authorization_state_changed` without running the query.
+
+`effect` uses the allowed rows to build the response or create the CSV.
+The SQL compiler continues to use fixed identifiers and bound values in
+[`src/server/query.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/server/query.ts).
-Download and revoke routes reload the saved export and authorize it separately.
-Check ownership using the saved owner, never a request field. Verify expiry,
-state, and current facts again before the effect. Revocation remains committed
-even if subsequent CSV cleanup fails; failed cleanup must not restore access.
+Download and revoke routes reload the saved export and authorize it separately,
+using its stored owner. They recheck expiry, state, and current facts before
+sending the file or revoking access. Revocation stays saved even if deleting the
+CSV file then fails; failed cleanup must not restore access.
-### Make controls reflect the server's decision
+### Show which actions are available
+
+Save the matching React controls and API client:
+
+- [`src/web/App.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/web/App.tsx)
+- [`src/web/api.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/web/api.ts)
+- [`src/web/Icon.tsx`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/web/Icon.tsx)
+- [`src/web/styles.css`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/src/web/styles.css)
`POST /api/authorization` previews the exact selected plan and export operations.
-It returns action availability, not rows or a CSV. React discards obsolete preview
+It returns which actions are allowed, not rows or a CSV. React ignores outdated preview
responses and disables controls while checking or when decisions are unavailable.
Each actual query/export request still authorizes again.
-Bind export controls to the last completed query, not the next edited form.
-Editing a draft query does not silently replace the result being exported.
-Do not add a second role-permission table or a browser Cedarling instance.
+Export controls use the last completed query, so editing the form does not
+silently change the result being exported. These controls use server previews;
+there is no browser Cedarling instance or separate client permission table.
+
+## Finish setup and restart the app
-For native use of the completed project, install/build the shared IdP and install
-P5 dependencies. The server's query, export, and preview gates are connected in
-`src/server/app.ts`; each still reloads current facts before an effect.[^5]
+Save the runtime preparation and packaging files:
-## Finish the runnable data-guard application
+- [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/scripts/setup.ts)
+- [`scripts/reset.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/scripts/reset.ts)
+- [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/Dockerfile)
-Make archive creation part of setup and the production build. The
-existing development supervisor already runs setup and build before starting
-the IdP and API, so `pnpm dev` now receives the archive through those steps;
-there is no new development launcher to add.[^6]
+Apply the `build` entry shown below to your existing
+[`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/package.json), keeping
+the other dependencies and scripts.
+
+The setup script builds the archive; the production build must too. The existing
+[`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.1/p5-dataguard/scripts/dev.mjs)
+runs both steps before starting the IdP and API, so `pnpm dev` needs no further
+change:
```json
{
@@ -460,33 +542,48 @@ there is no new development launcher to add.[^6]
}
```
-Copy the generated `.local/policy-store.cjar` into the Docker runtime image.[^7]
+The copied Dockerfile includes `.local/policy-store.cjar` in the runtime image.
For compiled native startup, run `pnpm run setup` and `pnpm build`, keep
`node --env-file=.local/idp/.env ../shared/identity-provider/dist/main.js`
running in another terminal, and run `pnpm start`. Docker remains
`docker compose up --build`.
-## Prove both restricted and useful access
+Stop the baseline development stack before restarting it. Run:
+
+```bash
+pnpm run setup
+pnpm build
+pnpm dev
+```
+
+Open `http://localhost:17005` and sign in again as Amina after the IdP restart.
+We'll retry her salary request, then check the allowed queries and exports.
+
+## Check queries and exports for each user

_The allowed response depends on the requested fields, result type, and caller._
-### Repeat the exact direct request
+### Retry Amina's salary request
As Amina, repeat the compensation request from the baseline section. Expect
**403** with `error: "authorization_denied"` and a request ID, without result
-rows. Salary and bonus are also absent from her field picker, but the direct API
-request proves that hiding controls is not the security boundary.
+rows. Salary and bonus are also absent from her field picker; the direct request
+proves that the server enforces this restriction too.
Change the fields to `employeeId`, `department`, and `tenantId`, retaining the
-Tenant A filter and `support` purpose. This is legitimate operational work and
+Tenant A filter and `support` purpose. This is allowed support work and
returns **200**. Changing the tenant to `tenant-b` or removing the tenant filter
must deny again.
-### Exercise aggregates and finance exports
+### Try counts and finance exports
+
+Amina can run an allowed support query. Can she export that same result?
+Check the `CreateExport` condition in the plan policy before trying the cases
+below.
-Use fresh fixtures and select the purpose matching each account:
+Use fresh sample data and select the purpose matching each account:
| Account and plan | Expected result |
| -------------------------------------------------------------- | ------------------------------------------------------------- |
@@ -500,9 +597,9 @@ Use fresh fixtures and select the purpose matching each account:
| Leah: No grouping, Average Salary / Average Bonus | 5,270,000 / 338,250 |
| Leah: Department-grouped aggregates releasing small groups | DENY, including export |
-The grouping/limit example applies to the deterministic fixture ordering. It
-teaches a rule over released groups, not a general defense against statistical
-inference.
+The grouping/limit example relies on the sample data's fixed order. It checks
+returned group sizes; it does not prevent users from learning private facts by
+combining query results.
To exercise a direct aggregate request, use the authenticated browser session's
CSRF header as in the first example and POST this body to
@@ -519,20 +616,20 @@ CSRF header as in the first example and POST this body to
```
For the export lifecycle, sign in as Leah, run an allowed finance plan, and create
-an export. Download it, inspect the synthetic columns, then revoke it. Download
+an export. Download it, inspect the fictional data, then revoke it. Download
must fail afterward. Exports also expire ten minutes after creation.
-An Amina or Theo session must not download or revoke Leah's export even with its
-reference or ID. Use separate local sessions, or the automated test, to avoid
-confusing a role denial with a missing reference. The routes are
+Amina and Theo must not download or revoke Leah's export even with its reference
+or ID. Use separate sessions or the automated test to distinguish a role denial
+from a missing reference. The routes are
`POST /api/exports/download` with `{ "downloadRef": "" }` and
`POST /api/exports//revoke`. Both require that session's CSRF header and
same-origin request. Never publish actual session or download credentials.
-### Explain what each log proves
+### Read the query and export logs
`authorization.context` connects the application request, actor, capability,
-and preview/enforcement phase with a native Cedarling request ID. Native decision
+and preview/enforcement phase with a Cedarling request ID. Cedarling's decision
JSON includes full `diagnostics.reason` and `errors`. Field inspection has one
decision per field; seeing both ALLOW and DENY in that batch is normal.
@@ -543,17 +640,19 @@ allowed owned download cites `manage-own-finance-export`.
`data.query.completed` and `data.aggregate.completed` describe completed reads.
`export.created` and `export.revoked` describe completed export changes.
`export.download.prepared` means the server prepared a response, not that the
-browser saved a file. Match the request ID and inspect the protected outcome;
+browser saved a file. Match the request ID and check the result;
Cedarling ALLOW alone is not proof of success.
-`request.failed` records bounded failures. Browser console objects contain the
+`request.failed` records limited error details. Browser console objects contain the
operation, status, and request ID, not browser-side Cedarling decisions. Server
-logs exclude raw workforce values, tokens, and download references. Memory logs
-expire after five minutes and are not a durable audit store.
+logs exclude raw workforce values, tokens, and download references. Logs kept in
+memory expire after five minutes, so they are not a lasting audit record.
-### Verify stale state and unavailable decisions
+### Check changed permissions and unavailable decisions
-Stop this project's learner instances to free ports 17005 and 18005, then run:
+The coding steps update runtime files, not the baseline's historical tests.
+For the full automated checks, use a separate checkout of the [finished tag](https://github.com/GluuFederation/cedarling-tutorials/tree/p5-dataguard-v1.0.1/p5-dataguard) and install its locked project and shared IdP dependencies.
+Stop your learner stack to free ports 17005 and 18005, then run:
```bash
pnpm exec playwright install chromium
@@ -564,63 +663,54 @@ On Linux, use `pnpm exec playwright install --with-deps chromium` if browser
libraries are missing. The check includes formatting, lint, types, tests, and
a production build/browser workflow with disposable data and exports.
-`test/authorization.test.ts` evaluates real policies and field batches.
-`test/app.test.ts` checks direct requests, cross-owner exports, each released
-group's minimum size, entitlement/cardinality changes while authorization is
-pending, and unavailable Cedarling without protected results or files.
-`e2e/sensitive-data-authorization.e2e.ts` exercises real sign-in and the UI with
-direct API bypass attempts.
+The checks use real policies, field batches, direct API
+requests, and attempts to access another user's export. They verify each
+returned group's minimum size, change permissions or counts while authorization
+is pending, and confirm that unavailable Cedarling releases no protected results
+or files. Browser checks use real sign-in and bypass the UI to call the API.
-For a fresh learner exercise, `pnpm reset` deliberately clears the synthetic
+For a fresh exercise, `pnpm reset` clears the sample
records, exports, and sessions; sign in again. In Docker use
`docker compose exec dataguard node --env-file=/run/config/app.env scripts/reset.ts`.
Do not reset a database you want to keep. Capture the identical unauthorized
-compensation request before and after, and Leah's legitimate finance export.
+compensation request before and after, and Leah's allowed finance export.
-## Reuse separate decisions for separate disclosures
+## Protect data in your own application

_Authorization remains necessary when a generated export is downloaded later._
Opening a dashboard, seeing a field name, reading a row, releasing an aggregate,
-and downloading a derived file are different capabilities. Authorize each at
-the server boundary that controls its effect, using current trusted facts.
+and downloading a generated file are different capabilities. Check each one
+where the server releases the data, using current trusted facts.
The same approach can protect a GraphQL API: authorize each sensitive field,
row set, aggregate, or export at the resolver or service boundary that releases
-it, not merely at the query entry point. P5 itself uses Hono, not GraphQL;
+it, not just at the query entry point. P5 itself uses Hono, not GraphQL;
see [GraphQL's authorization guidance](https://graphql.org/learn/authorization/).
Follow `src/server/authorization.ts`, `src/server/app.ts`, `src/server/query.ts`,
`src/server/export-service.ts`, `policy-store/`, and the tests in the
-[completed P5 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p5-dataguard-v1.0.0/p5-dataguard).
-
-Production needs real identity, governed entitlements, secure storage/transport,
-and appropriate audit retention. Five records per group is one teaching
-constraint, not complete privacy protection against inference across repeated
-queries. Cedarling also does not replace parameterized SQL, transactions, expiry,
-or file cleanup.
+[completed P5 project](https://github.com/GluuFederation/cedarling-tutorials/tree/p5-dataguard-v1.0.1/p5-dataguard).
For a production stack, consider Agama Lab Policy Designer for policy authoring,
-Jans Auth for token issuance, and Lock Server for centralized decision logs.
+Jans Auth for issuing tokens, and Lock Server for centralized decision logs.
See [Cedarling production solutions](https://cedarling.dev/solutions).
Next, P6 moves to field-inspection workflows and authorization over submitted
-work rather than analytical projections.
-
----
-
-[^1]: An aggregate reports a calculation over multiple records, such as a count by department, without returning the individual rows. Small groups and repeated queries can still reveal sensitive facts, so P5 treats aggregate release as its own authorization boundary.
-
-[^2]: The pinned [`cedarling_wasm` JavaScript API](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468) accepts a JSON-string request. Serialization does not validate the query plan; the server must do that before calling Cedarling.
+work rather than query results.
-[^3]: Starting-checkpoint source: [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/21b0832be4b31271320df992d04e9d97667d0e38/p5-dataguard/src/server/app.ts) runs `database.evaluate()` and prints a permissive trace without authorization.
+
+Warning: This setup is for local practice
-[^4]: Complete tagged store: [`metadata.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/policy-store/metadata.json), [`schema.cedarschema`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/policy-store/schema.cedarschema), [`fields.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/policy-store/policies/fields.cedar), [`plans.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/policy-store/policies/plans.cedar), and [`exports.cedar`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/policy-store/policies/exports.cedar).
+- Local HTTP and the bundled IdP are for learning only. Production requires HTTPS and a configured OIDC/OAuth issuer, such as [Jans Auth](https://docs.jans.io/stable/janssen-server/planning/use-cases/), Gluu, Auth0, or Okta.
+- Control permission changes, secure stored data, and store audit logs. Keep parameterized SQL, transactions, expiry, and file cleanup alongside authorization.
+- A five-record minimum is a teaching example. Users can still learn sensitive facts through repeated queries; this rule alone cannot prevent that.
+- These steps were prepared on Ubuntu 24.04+. Native project checks also run in CI on macOS and Windows. If a platform-specific step fails, [open an issue](https://github.com/GluuFederation/cedarling-tutorials/issues).
-[^5]: Complete enforcement source: [`src/server/authorization.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/src/server/authorization.ts), [`src/server/app.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/src/server/app.ts), [`src/server/query.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/src/server/query.ts), and [`src/server/export-service.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/src/server/export-service.ts).
+
-[^6]: Completed [`scripts/setup.ts`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/scripts/setup.ts), [`scripts/dev.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/scripts/dev.mjs), [`package.json`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/package.json), and repository-level [`shared/policy-store.mjs`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/shared/policy-store.mjs) show how the existing launcher receives the archive.
+[^1]: An aggregate reports a calculation over multiple records, such as a count by department, without returning the individual rows. Small groups and repeated queries can still reveal sensitive facts, so P5 treats aggregate release as its own authorization boundary.
-[^7]: Completed [`Dockerfile`](https://github.com/GluuFederation/cedarling-tutorials/blob/p5-dataguard-v1.0.0/p5-dataguard/Dockerfile) copies the generated archive into the runtime image.
+[^2]: The pinned [`cedarling_wasm` JavaScript API](https://www.npmjs.com/package/@janssenproject/cedarling_wasm/v/0.0.468) accepts a JSON-string request. Converting to JSON does not validate the query plan; the server must do that before calling Cedarling.
diff --git a/p5-dataguard/pnpm-lock.yaml b/p5-dataguard/pnpm-lock.yaml
index 79bbce6..a1d0b64 100644
--- a/p5-dataguard/pnpm-lock.yaml
+++ b/p5-dataguard/pnpm-lock.yaml
@@ -505,8 +505,8 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
@@ -920,7 +920,7 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
react-dom@19.3.0(react@19.3.0):
dependencies:
@@ -954,7 +954,7 @@ snapshots:
siginfo@2.0.0: {}
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
stackback@0.0.2: {}
diff --git a/p6-field-inspection/pnpm-lock.yaml b/p6-field-inspection/pnpm-lock.yaml
index 3a17563..03509ed 100644
--- a/p6-field-inspection/pnpm-lock.yaml
+++ b/p6-field-inspection/pnpm-lock.yaml
@@ -826,8 +826,8 @@ packages:
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
@@ -1344,7 +1344,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
optional: true
csstype@3.2.3: {}
@@ -1635,7 +1635,7 @@ snapshots:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
process-warning@4.0.1: {}
@@ -1713,7 +1713,7 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
split2@4.2.0: {}
diff --git a/p7-collaborative-docs/pnpm-lock.yaml b/p7-collaborative-docs/pnpm-lock.yaml
index 3d3acf8..27dde6b 100644
--- a/p7-collaborative-docs/pnpm-lock.yaml
+++ b/p7-collaborative-docs/pnpm-lock.yaml
@@ -882,8 +882,8 @@ packages:
sonic-boom@4.2.1:
resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
split2@4.2.0:
@@ -1625,7 +1625,7 @@ snapshots:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
process-warning@4.0.1: {}
@@ -1695,7 +1695,7 @@ snapshots:
dependencies:
atomic-sleep: 1.0.0
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
split2@4.2.0: {}
diff --git a/p8-cedarfile/pnpm-lock.yaml b/p8-cedarfile/pnpm-lock.yaml
index 7e06911..ff60207 100644
--- a/p8-cedarfile/pnpm-lock.yaml
+++ b/p8-cedarfile/pnpm-lock.yaml
@@ -791,8 +791,8 @@ packages:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
- proxy-addr@2.0.7:
- resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ proxy-addr@2.0.8:
+ resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
punycode@2.3.1:
@@ -873,8 +873,8 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
@@ -1393,7 +1393,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
csstype@3.2.3: {}
@@ -1467,7 +1467,7 @@ snapshots:
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
- proxy-addr: 2.0.7
+ proxy-addr: 2.0.8
qs: 6.16.0
range-parser: 1.3.0
router: 2.2.0
@@ -1711,9 +1711,9 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
- proxy-addr@2.0.7:
+ proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
@@ -1839,7 +1839,7 @@ snapshots:
siginfo@2.0.0: {}
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
stackback@0.0.2: {}
diff --git a/p9-cedarrealtime/pnpm-lock.yaml b/p9-cedarrealtime/pnpm-lock.yaml
index d495f3d..60722bd 100644
--- a/p9-cedarrealtime/pnpm-lock.yaml
+++ b/p9-cedarrealtime/pnpm-lock.yaml
@@ -812,8 +812,8 @@ packages:
resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==}
engines: {node: ^10 || ^12 || >=14}
- proxy-addr@2.0.7:
- resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ proxy-addr@2.0.8:
+ resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
punycode@2.3.1:
@@ -909,8 +909,8 @@ packages:
resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==}
engines: {node: '>=10.2.0'}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
@@ -1441,7 +1441,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
csstype@3.2.3: {}
@@ -1545,7 +1545,7 @@ snapshots:
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
- proxy-addr: 2.0.7
+ proxy-addr: 2.0.8
qs: 6.16.0
range-parser: 1.3.0
router: 2.2.0
@@ -1786,9 +1786,9 @@ snapshots:
dependencies:
nanoid: 3.3.18
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
- proxy-addr@2.0.7:
+ proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
@@ -1955,7 +1955,7 @@ snapshots:
- supports-color
- utf-8-validate
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
stackback@0.0.2: {}
diff --git a/shared/identity-provider/pnpm-lock.yaml b/shared/identity-provider/pnpm-lock.yaml
index c83ec42..9be845d 100644
--- a/shared/identity-provider/pnpm-lock.yaml
+++ b/shared/identity-provider/pnpm-lock.yaml
@@ -1200,8 +1200,8 @@ packages:
engines: {node: '>=14'}
hasBin: true
- proxy-addr@2.0.7:
- resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
+ proxy-addr@2.0.8:
+ resolution: {integrity: sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==}
engines: {node: '>= 0.10'}
punycode@2.3.1:
@@ -1282,8 +1282,8 @@ packages:
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
stackback@0.0.2:
@@ -2212,7 +2212,7 @@ snapshots:
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
- proxy-addr: 2.0.7
+ proxy-addr: 2.0.8
qs: 6.16.0
range-parser: 1.3.0
router: 2.2.0
@@ -2572,13 +2572,13 @@ snapshots:
dependencies:
nanoid: 3.3.19
picocolors: 1.1.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
prelude-ls@1.2.1: {}
prettier@3.9.6: {}
- proxy-addr@2.0.7:
+ proxy-addr@2.0.8:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
@@ -2703,7 +2703,7 @@ snapshots:
siginfo@2.0.0: {}
- source-map-js@1.2.1: {}
+ source-map-js@1.2.2: {}
stackback@0.0.2: {}
diff --git a/shared/tools/repository-automation/package.json b/shared/tools/repository-automation/package.json
index 59d9f3f..30d4de6 100644
--- a/shared/tools/repository-automation/package.json
+++ b/shared/tools/repository-automation/package.json
@@ -15,7 +15,7 @@
"dompurify": "3.4.16",
"jsdom": "30.1.1",
"mdast-util-from-markdown": "2.0.3",
- "sharp": "0.35.4",
+ "sharp": "0.35.5",
"yaml": "2.9.0",
"zod": "4.6.5"
}
diff --git a/shared/tools/repository-automation/pnpm-lock.yaml b/shared/tools/repository-automation/pnpm-lock.yaml
index 8f00995..b8290f7 100644
--- a/shared/tools/repository-automation/pnpm-lock.yaml
+++ b/shared/tools/repository-automation/pnpm-lock.yaml
@@ -18,8 +18,8 @@ importers:
specifier: 2.0.3
version: 2.0.3
sharp:
- specifier: 0.35.4
- version: 0.35.4
+ specifier: 0.35.5
+ version: 0.35.5
yaml:
specifier: 2.9.0
version: 2.9.0
@@ -93,144 +93,144 @@ packages:
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
- '@img/sharp-darwin-arm64@0.35.4':
- resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==}
+ '@img/sharp-darwin-arm64@0.35.5':
+ resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [darwin]
- '@img/sharp-darwin-x64@0.35.4':
- resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==}
+ '@img/sharp-darwin-x64@0.35.5':
+ resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [darwin]
- '@img/sharp-freebsd-wasm32@0.35.4':
- resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==}
+ '@img/sharp-freebsd-wasm32@0.35.5':
+ resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
engines: {node: '>=20.9.0'}
os: [freebsd]
- '@img/sharp-libvips-darwin-arm64@1.3.3':
- resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==}
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
+ resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
cpu: [arm64]
os: [darwin]
- '@img/sharp-libvips-darwin-x64@1.3.3':
- resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==}
+ '@img/sharp-libvips-darwin-x64@1.3.4':
+ resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
cpu: [x64]
os: [darwin]
- '@img/sharp-libvips-linux-arm64@1.3.3':
- resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==}
+ '@img/sharp-libvips-linux-arm64@1.3.4':
+ resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linux-arm@1.3.3':
- resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==}
+ '@img/sharp-libvips-linux-arm@1.3.4':
+ resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
cpu: [arm]
os: [linux]
- '@img/sharp-libvips-linux-ppc64@1.3.3':
- resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==}
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
+ resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
cpu: [ppc64]
os: [linux]
- '@img/sharp-libvips-linux-riscv64@1.3.3':
- resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==}
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
+ resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
cpu: [riscv64]
os: [linux]
- '@img/sharp-libvips-linux-s390x@1.3.3':
- resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==}
+ '@img/sharp-libvips-linux-s390x@1.3.4':
+ resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
cpu: [s390x]
os: [linux]
- '@img/sharp-libvips-linux-x64@1.3.3':
- resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==}
+ '@img/sharp-libvips-linux-x64@1.3.4':
+ resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
cpu: [x64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
- resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==}
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
+ resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
cpu: [arm64]
os: [linux]
- '@img/sharp-libvips-linuxmusl-x64@1.3.3':
- resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==}
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
+ resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
cpu: [x64]
os: [linux]
- '@img/sharp-linux-arm64@0.35.4':
- resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==}
+ '@img/sharp-linux-arm64@0.35.5':
+ resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
- '@img/sharp-linux-arm@0.35.4':
- resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==}
+ '@img/sharp-linux-arm@0.35.5':
+ resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
engines: {node: '>=20.9.0'}
cpu: [arm]
os: [linux]
- '@img/sharp-linux-ppc64@0.35.4':
- resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==}
+ '@img/sharp-linux-ppc64@0.35.5':
+ resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
engines: {node: '>=20.9.0'}
cpu: [ppc64]
os: [linux]
- '@img/sharp-linux-riscv64@0.35.4':
- resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==}
+ '@img/sharp-linux-riscv64@0.35.5':
+ resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
engines: {node: '>=20.9.0'}
cpu: [riscv64]
os: [linux]
- '@img/sharp-linux-s390x@0.35.4':
- resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==}
+ '@img/sharp-linux-s390x@0.35.5':
+ resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
engines: {node: '>=20.9.0'}
cpu: [s390x]
os: [linux]
- '@img/sharp-linux-x64@0.35.4':
- resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==}
+ '@img/sharp-linux-x64@0.35.5':
+ resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
- '@img/sharp-linuxmusl-arm64@0.35.4':
- resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==}
+ '@img/sharp-linuxmusl-arm64@0.35.5':
+ resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [linux]
- '@img/sharp-linuxmusl-x64@0.35.4':
- resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==}
+ '@img/sharp-linuxmusl-x64@0.35.5':
+ resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [linux]
- '@img/sharp-wasm32@0.35.4':
- resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==}
+ '@img/sharp-wasm32@0.35.5':
+ resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
engines: {node: '>=20.9.0'}
- '@img/sharp-webcontainers-wasm32@0.35.4':
- resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==}
+ '@img/sharp-webcontainers-wasm32@0.35.5':
+ resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
engines: {node: '>=20.9.0'}
cpu: [wasm32]
- '@img/sharp-win32-arm64@0.35.4':
- resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==}
+ '@img/sharp-win32-arm64@0.35.5':
+ resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
engines: {node: '>=20.9.0'}
cpu: [arm64]
os: [win32]
- '@img/sharp-win32-ia32@0.35.4':
- resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==}
+ '@img/sharp-win32-ia32@0.35.5':
+ resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
engines: {node: ^20.9.0}
cpu: [ia32]
os: [win32]
- '@img/sharp-win32-x64@0.35.4':
- resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==}
+ '@img/sharp-win32-x64@0.35.5':
+ resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
engines: {node: '>=20.9.0'}
cpu: [x64]
os: [win32]
@@ -412,8 +412,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
- sharp@0.35.4:
- resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==}
+ sharp@0.35.5:
+ resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
engines: {node: '>=20.9.0'}
peerDependencies:
'@types/node': '*'
@@ -421,8 +421,8 @@ packages:
'@types/node':
optional: true
- source-map-js@1.2.1:
- resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
engines: {node: '>=0.10.0'}
tldts-core@7.4.14:
@@ -543,108 +543,108 @@ snapshots:
'@img/colour@1.1.0': {}
- '@img/sharp-darwin-arm64@0.35.4':
+ '@img/sharp-darwin-arm64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-arm64': 1.3.3
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
optional: true
- '@img/sharp-darwin-x64@0.35.4':
+ '@img/sharp-darwin-x64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-darwin-x64': 1.3.3
+ '@img/sharp-libvips-darwin-x64': 1.3.4
optional: true
- '@img/sharp-freebsd-wasm32@0.35.4':
+ '@img/sharp-freebsd-wasm32@0.35.5':
dependencies:
- '@img/sharp-wasm32': 0.35.4
+ '@img/sharp-wasm32': 0.35.5
optional: true
- '@img/sharp-libvips-darwin-arm64@1.3.3':
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
optional: true
- '@img/sharp-libvips-darwin-x64@1.3.3':
+ '@img/sharp-libvips-darwin-x64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-arm64@1.3.3':
+ '@img/sharp-libvips-linux-arm64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-arm@1.3.3':
+ '@img/sharp-libvips-linux-arm@1.3.4':
optional: true
- '@img/sharp-libvips-linux-ppc64@1.3.3':
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-riscv64@1.3.3':
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
optional: true
- '@img/sharp-libvips-linux-s390x@1.3.3':
+ '@img/sharp-libvips-linux-s390x@1.3.4':
optional: true
- '@img/sharp-libvips-linux-x64@1.3.3':
+ '@img/sharp-libvips-linux-x64@1.3.4':
optional: true
- '@img/sharp-libvips-linuxmusl-arm64@1.3.3':
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
optional: true
- '@img/sharp-libvips-linuxmusl-x64@1.3.3':
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
optional: true
- '@img/sharp-linux-arm64@0.35.4':
+ '@img/sharp-linux-arm64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm64': 1.3.3
+ '@img/sharp-libvips-linux-arm64': 1.3.4
optional: true
- '@img/sharp-linux-arm@0.35.4':
+ '@img/sharp-linux-arm@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-arm': 1.3.3
+ '@img/sharp-libvips-linux-arm': 1.3.4
optional: true
- '@img/sharp-linux-ppc64@0.35.4':
+ '@img/sharp-linux-ppc64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-ppc64': 1.3.3
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
optional: true
- '@img/sharp-linux-riscv64@0.35.4':
+ '@img/sharp-linux-riscv64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-riscv64': 1.3.3
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
optional: true
- '@img/sharp-linux-s390x@0.35.4':
+ '@img/sharp-linux-s390x@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-s390x': 1.3.3
+ '@img/sharp-libvips-linux-s390x': 1.3.4
optional: true
- '@img/sharp-linux-x64@0.35.4':
+ '@img/sharp-linux-x64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linux-x64': 1.3.3
+ '@img/sharp-libvips-linux-x64': 1.3.4
optional: true
- '@img/sharp-linuxmusl-arm64@0.35.4':
+ '@img/sharp-linuxmusl-arm64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
optional: true
- '@img/sharp-linuxmusl-x64@0.35.4':
+ '@img/sharp-linuxmusl-x64@0.35.5':
optionalDependencies:
- '@img/sharp-libvips-linuxmusl-x64': 1.3.3
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
optional: true
- '@img/sharp-wasm32@0.35.4':
+ '@img/sharp-wasm32@0.35.5':
dependencies:
'@emnapi/runtime': 1.11.3
optional: true
- '@img/sharp-webcontainers-wasm32@0.35.4':
+ '@img/sharp-webcontainers-wasm32@0.35.5':
dependencies:
- '@img/sharp-wasm32': 0.35.4
+ '@img/sharp-wasm32': 0.35.5
optional: true
- '@img/sharp-win32-arm64@0.35.4':
+ '@img/sharp-win32-arm64@0.35.5':
optional: true
- '@img/sharp-win32-ia32@0.35.4':
+ '@img/sharp-win32-ia32@0.35.5':
optional: true
- '@img/sharp-win32-x64@0.35.4':
+ '@img/sharp-win32-x64@0.35.5':
optional: true
'@types/debug@4.1.13':
@@ -671,7 +671,7 @@ snapshots:
css-tree@3.2.1:
dependencies:
mdn-data: 2.27.1
- source-map-js: 1.2.1
+ source-map-js: 1.2.2
data-urls@7.0.0:
dependencies:
@@ -911,39 +911,39 @@ snapshots:
semver@7.8.5: {}
- sharp@0.35.4:
+ sharp@0.35.5:
dependencies:
'@img/colour': 1.1.0
detect-libc: 2.1.2
semver: 7.8.5
optionalDependencies:
- '@img/sharp-darwin-arm64': 0.35.4
- '@img/sharp-darwin-x64': 0.35.4
- '@img/sharp-freebsd-wasm32': 0.35.4
- '@img/sharp-libvips-darwin-arm64': 1.3.3
- '@img/sharp-libvips-darwin-x64': 1.3.3
- '@img/sharp-libvips-linux-arm': 1.3.3
- '@img/sharp-libvips-linux-arm64': 1.3.3
- '@img/sharp-libvips-linux-ppc64': 1.3.3
- '@img/sharp-libvips-linux-riscv64': 1.3.3
- '@img/sharp-libvips-linux-s390x': 1.3.3
- '@img/sharp-libvips-linux-x64': 1.3.3
- '@img/sharp-libvips-linuxmusl-arm64': 1.3.3
- '@img/sharp-libvips-linuxmusl-x64': 1.3.3
- '@img/sharp-linux-arm': 0.35.4
- '@img/sharp-linux-arm64': 0.35.4
- '@img/sharp-linux-ppc64': 0.35.4
- '@img/sharp-linux-riscv64': 0.35.4
- '@img/sharp-linux-s390x': 0.35.4
- '@img/sharp-linux-x64': 0.35.4
- '@img/sharp-linuxmusl-arm64': 0.35.4
- '@img/sharp-linuxmusl-x64': 0.35.4
- '@img/sharp-webcontainers-wasm32': 0.35.4
- '@img/sharp-win32-arm64': 0.35.4
- '@img/sharp-win32-ia32': 0.35.4
- '@img/sharp-win32-x64': 0.35.4
-
- source-map-js@1.2.1: {}
+ '@img/sharp-darwin-arm64': 0.35.5
+ '@img/sharp-darwin-x64': 0.35.5
+ '@img/sharp-freebsd-wasm32': 0.35.5
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
+ '@img/sharp-libvips-darwin-x64': 1.3.4
+ '@img/sharp-libvips-linux-arm': 1.3.4
+ '@img/sharp-libvips-linux-arm64': 1.3.4
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
+ '@img/sharp-libvips-linux-s390x': 1.3.4
+ '@img/sharp-libvips-linux-x64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
+ '@img/sharp-linux-arm': 0.35.5
+ '@img/sharp-linux-arm64': 0.35.5
+ '@img/sharp-linux-ppc64': 0.35.5
+ '@img/sharp-linux-riscv64': 0.35.5
+ '@img/sharp-linux-s390x': 0.35.5
+ '@img/sharp-linux-x64': 0.35.5
+ '@img/sharp-linuxmusl-arm64': 0.35.5
+ '@img/sharp-linuxmusl-x64': 0.35.5
+ '@img/sharp-webcontainers-wasm32': 0.35.5
+ '@img/sharp-win32-arm64': 0.35.5
+ '@img/sharp-win32-ia32': 0.35.5
+ '@img/sharp-win32-x64': 0.35.5
+
+ source-map-js@1.2.2: {}
tldts-core@7.4.14: {}