{
  "info": {
    "_postman_id": "d26c962d-ae5b-471c-98d1-7d8a3d94ac7a",
    "name": "secureFlows-Happy-Path",
    "description": "## secureFlows Full Happy-Path Collection\n\nThis collection exercises every public API endpoint in a realistic end-to-end flow. It is also used as the CI reference collection, so it is always in sync with the live API.\n\n### How to use\n\n1. Import this collection into Postman.\n    \n2. Create a Postman Environment and add the variables listed in the **Required environment variables** section of each folder description below — or see the summary in folder **0 — Setup**.\n    \n3. Run folder **0** first (it self-populates all derived tokens and IDs).\n    \n4. After that, folders can be run in order or individually.\n    \n\n### Required environment variables (set before running)\n\n| Variable | Description |\n| --- | --- |\n| `firebase-api-key` | Your Firebase Web API key (from Firebase console → Project Settings) |\n| `owner-email` | Email of the workspace owner Firebase account |\n| `owner-password` | Password of the workspace owner Firebase account |\n| `admin-email` | Email of an admin Firebase account (will be added to the workspace via invite) |\n| `admin-password` | Password of the admin Firebase account |\n| `anon-email` | Email of a regular/anon Firebase account |\n| `anon-password` | Password of the anon Firebase account |\n\nAll other variables (`firebase_owner_token`, `owner_token`, session IDs, etc.) are populated automatically by the collection's test scripts.\n\n### Base URL\n\nThe collection uses `{{protocol}}://{{address}}` which defaults to `https://www.secure-flows.com`. Override in your environment to point at a local instance.\n\n### Conventions\n\n- **`skip_200_check`** — set in `pre-request` to suppress the global 200 assertion for requests that legitimately return a non-200 status.\n    \n- All requests that mutate state return a response body that is asserted in the `test` script.\n    \n- Token lifetimes are short — if a long test run causes token expiry, re-run folder 0.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
    "_exporter_id": "12016134"
  },
  "item": [
    {
      "name": "0 — Setup (run first)",
      "item": [
        {
          "name": "0.1 Firebase login — owner",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Exchanges owner credentials for a Firebase ID token.",
                  "// The ID token is used to create the workspace and authenticate owner API calls.",
                  "var j = pm.response.json();",
                  "pm.environment.set('firebase_owner_token', j.idToken);",
                  "pm.environment.set('firebase_owner_uid', j.localId);",
                  "pm.test('Got firebase_owner_token', function () {",
                  "    pm.expect(j.idToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{owner-email}}\",\n  \"password\": \"{{owner-password}}\",\n  \"returnSecureToken\": true\n}"
            },
            "url": {
              "raw": "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={{firebase-api-key}}",
              "protocol": "https",
              "host": [
                "identitytoolkit",
                "googleapis",
                "com"
              ],
              "path": [
                "v1",
                "accounts:signInWithPassword"
              ],
              "query": [
                {
                  "key": "key",
                  "value": "{{firebase-api-key}}"
                }
              ]
            },
            "description": "Signs in the owner account via the Firebase Identity Toolkit.\nProduces `firebase_owner_token` used for workspace and session creation."
          },
          "response": []
        },
        {
          "name": "0.2 Firebase login — admin",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Exchanges admin credentials for a Firebase ID token.",
                  "// Used later to redeem an admin invite and obtain a workspace USER JWT.",
                  "var j = pm.response.json();",
                  "pm.environment.set('firebase_admin_token', j.idToken);",
                  "pm.environment.set('firebase_admin_uid', j.localId);",
                  "pm.test('Got firebase_admin_token', function () {",
                  "    pm.expect(j.idToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{admin-email}}\",\n  \"password\": \"{{admin-password}}\",\n  \"returnSecureToken\": true\n}"
            },
            "url": {
              "raw": "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={{firebase-api-key}}",
              "protocol": "https",
              "host": [
                "identitytoolkit",
                "googleapis",
                "com"
              ],
              "path": [
                "v1",
                "accounts:signInWithPassword"
              ],
              "query": [
                {
                  "key": "key",
                  "value": "{{firebase-api-key}}"
                }
              ]
            },
            "description": "Signs in the admin account via the Firebase Identity Toolkit."
          },
          "response": []
        },
        {
          "name": "0.3 Firebase login — anon user",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Exchanges anon-user credentials for a Firebase ID token.",
                  "// Used to create an anonymous workspace user and session.",
                  "var j = pm.response.json();",
                  "pm.environment.set('firebase_anon_token', j.idToken);",
                  "pm.environment.set('firebase_anon_uid', j.localId);",
                  "pm.test('Got firebase_anon_token', function () {",
                  "    pm.expect(j.idToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"email\": \"{{anon-email}}\",\n  \"password\": \"{{anon-password}}\",\n  \"returnSecureToken\": true\n}"
            },
            "url": {
              "raw": "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={{firebase-api-key}}",
              "protocol": "https",
              "host": [
                "identitytoolkit",
                "googleapis",
                "com"
              ],
              "path": [
                "v1",
                "accounts:signInWithPassword"
              ],
              "query": [
                {
                  "key": "key",
                  "value": "{{firebase-api-key}}"
                }
              ]
            },
            "description": "Signs in the anon account via the Firebase Identity Toolkit."
          },
          "response": []
        },
        {
          "name": "0.4 Create workspace (acceptAnonimous: true)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Creates the primary demo workspace and stores the owner USER JWT.",
                  "// The accessToken returned here is an internal USER JWT with OWNER role.",
                  "pm.test('Workspace created', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code !== 200) { return; }",
                  "var j = pm.response.json();",
                  "pm.environment.set('owner_token', j.accessToken);",
                  "pm.test('acceptAnonimous is true as set', function () {",
                  "    pm.expect(j.workspace.acceptAnonimous).to.be.true;",
                  "});",
                  "pm.test('accessToken returned', function () {",
                  "    pm.expect(j.accessToken).to.be.a('string').and.not.empty;",
                  "});",
                  "pm.test('workspace has id and name', function () {",
                  "    pm.expect(j.workspace.id).to.be.a('number');",
                  "    pm.expect(j.workspace.name).to.eql('demo-workspace');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"name\": \"demo-workspace\",\n    \"acceptAnonimous\": true\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/workspaces",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "workspaces"
              ]
            },
            "description": "Creates a new workspace. The caller's Firebase UID becomes the OWNER.\nReturns `accessToken` — a USER JWT with OWNER role — and the workspace object.\n\n`acceptAnonimous: true` allows Firebase anonymous-sign-in accounts to create sessions in this workspace."
          },
          "response": []
        },
        {
          "name": "0.5 Create admin invite",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Creates a one-time invite link that lets another Firebase user",
                  "// join this workspace as ADMIN.",
                  "pm.test('Admin invite created', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code !== 200) { return; }",
                  "var j = pm.response.json();",
                  "pm.environment.set('setup_admin_invite_key', j.secretKey);",
                  "pm.test('Invite secretKey returned', function () {",
                  "    pm.expect(j.secretKey).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"role\": \"admin\",\n    \"ttlSeconds\": 3600\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/invites",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "invites"
              ]
            },
            "description": "Creates a single-use invite secret. The recipient redeems it via `POST /users/from-invite`.\n`ttlSeconds` is how long the invite is valid after creation (here 1 hour)."
          },
          "response": []
        },
        {
          "name": "0.6 Redeem admin invite",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Redeems the invite and creates the admin user in the workspace.",
                  "pm.test('Admin user created', function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "if (pm.response.code !== 200) { return; }",
                  "var j = pm.response.json();",
                  "pm.environment.set('admin_user_id', String(j.id));",
                  "pm.test('Admin role is ADMIN', function () {",
                  "    pm.expect(j.role).to.eql('ADMIN');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_admin_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"secretKey\": \"{{setup_admin_invite_key}}\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users/from-invite",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users",
                "from-invite"
              ]
            },
            "description": "The admin Firebase account redeems the invite secret to join the workspace.\nThe server creates a workspace user row with role ADMIN and returns it."
          },
          "response": []
        },
        {
          "name": "0.7 Admin login — get USER JWT",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Exchanges the admin Firebase token for an internal USER JWT scoped to this workspace.",
                  "// Use admin_token for admin-level management API calls.",
                  "pm.test('Admin USER JWT issued', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code !== 200) { return; }",
                  "var j = pm.response.json();",
                  "pm.environment.set('admin_token', j.accessToken);",
                  "pm.test('accessToken returned', function () {",
                  "    pm.expect(j.accessToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_admin_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users/login",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users",
                "login"
              ]
            },
            "description": "Exchanges a Firebase ID token for an internal USER JWT (`tokenType=USER`).\nThis token carries workspace and role claims and is used for management API calls.\nDoes NOT create or interact with sessions."
          },
          "response": []
        },
        {
          "name": "0.8 Register app with multiple redirectUris",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Registers an application under this workspace.",
                  "// The app's redirectUris allowlist is enforced by the hosted-login redirect.",
                  "pm.test('App created with multiple redirectUris', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code !== 200) { return; }",
                  "var j = pm.response.json();",
                  "pm.test('appId and redirectUris correct', function () {",
                  "    pm.expect(j.appId).to.eql('app-1');",
                  "    pm.expect(j.redirectUris).to.be.an('array').with.lengthOf(2);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"appId\": \"app-1\",\n    \"displayName\": \"App One\",\n    \"redirectUris\": [\"http://stam.com\", \"http://stam.com/callback\"]\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications"
              ]
            },
            "description": "Registers a client application under the workspace.\n`appId` is the stable identifier used in hosted-login URL parameters.\n`redirectUris` is the allowlist of URIs the hosted-login flow may redirect to after sign-in.\nAny redirect to an URI not on this list is rejected (open-redirect protection)."
          },
          "response": []
        },
        {
          "name": "0.9 Create anonymous user",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Creates (or returns existing) anonymous workspace user for the Firebase UID.",
                  "// Requires acceptAnonimous: true on the workspace.",
                  "pm.test('Anon user created', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code !== 200) { return; }",
                  "var j = pm.response.json();",
                  "pm.environment.set('anon_user_id', String(j.id));",
                  "pm.test('Anon role is ANONIMOUS', function () {",
                  "    pm.expect(j.role).to.eql('ANONIMOUS');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users/anonymous",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users",
                "anonymous"
              ]
            },
            "description": "Creates or returns an anonymous workspace user entry for the authenticated Firebase UID.\nRequires `acceptAnonimous: true` on the workspace. Returns a workspace user with role `ANONIMOUS`."
          },
          "response": []
        },
        {
          "name": "0.10 Anon login — get USER JWT",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Anon USER JWT issued', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code !== 200) { return; }",
                  "var j = pm.response.json();",
                  "pm.environment.set('anon_token', j.accessToken);",
                  "pm.test('accessToken returned', function () {",
                  "    pm.expect(j.accessToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users/login",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users",
                "login"
              ]
            },
            "description": "Exchanges the anon Firebase token for a workspace USER JWT.\nUsed for `GET /sessions/my` and `POST /sessions/revoke/{sessionId}` calls in later folders."
          },
          "response": []
        },
        {
          "name": "0.11 Create session — anon, with payload (POST /sessions)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Creates a brand-new session. Prefer get-or-create in most integrations —",
                  "// this endpoint always creates a new row even if one already exists.",
                  "pm.test('Anon session created', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code !== 200) { return; }",
                  "var j = pm.response.json();",
                  "pm.environment.set('anon_session_token', j.sessionToken);",
                  "// Decode the JWT payload (no signature verification — local use only).",
                  "var parts = j.sessionToken.split('.');",
                  "var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));",
                  "pm.environment.set('anon_session_id', String(payload.sub));",
                  "pm.test('sessionToken returned', function () {",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-1\",\n    \"payload\": {\n        \"name\": \"Alice\",\n        \"score\": 42,\n        \"active\": true\n    }\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            },
            "description": "Creates a new session row for the authenticated Firebase user in the given workspace.\nThe `payload` object is encrypted at rest; the `sessionToken` is returned and stored in memory.\n\nNOTE: `POST /sessions` always creates a new row. Use `POST /sessions/get-or-create` in most app integrations to reuse an existing active session."
          },
          "response": []
        },
        {
          "name": "0.12 Create session — owner, with payload (POST /sessions)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Owner session created', function () { pm.response.to.have.status(200); });",
                  "if (pm.response.code !== 200) { return; }",
                  "var j = pm.response.json();",
                  "pm.environment.set('owner_session_token', j.sessionToken);",
                  "var parts = j.sessionToken.split('.');",
                  "var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));",
                  "pm.environment.set('owner_session_id', String(payload.sub));",
                  "pm.test('sessionToken returned', function () {",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-1\",\n    \"payload\": { \"role\": \"owner\", \"setup\": true }\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            },
            "description": "Creates a second session (owner account) for populating admin-view and audit tests."
          },
          "response": []
        }
      ],
      "description": "Authenticates three Firebase accounts (owner, admin, anon), creates a workspace, registers an application, and seeds the tokens and IDs used by every subsequent folder.\n\nRun this folder once before executing any other folder. All output is stored in environment variables by the test scripts."
    },
    {
      "name": "1 — Workspace",
      "item": [
        {
          "name": "1.1 GET /workspaces — read current workspace",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Workspace returned', function () { pm.response.to.have.status(200); });",
                  "pm.test('Correct workspace name', function () {",
                  "    pm.expect(j.name).to.eql('demo-workspace');",
                  "});",
                  "pm.test('Has id and acceptAnonimous', function () {",
                  "    pm.expect(j.id).to.be.a('number');",
                  "    pm.expect(j.acceptAnonimous).to.be.a('boolean');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/workspaces",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "workspaces"
              ]
            },
            "description": "Returns the workspace associated with the USER JWT's workspace claim.\nCaller must be ADMIN or OWNER."
          },
          "response": []
        },
        {
          "name": "1.2 Create second workspace (acceptAnonimous omitted — defaults to true)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// When acceptAnonimous is omitted from the request body, the server defaults it to true.",
                  "var j = pm.response.json();",
                  "pm.environment.set('second_workspace_token', j.accessToken);",
                  "pm.test('Workspace created', function () { pm.response.to.have.status(200); });",
                  "pm.test('acceptAnonimous defaults to true when omitted', function () {",
                  "    pm.expect(j.workspace.acceptAnonimous).to.be.true;",
                  "});",
                  "pm.test('workspace name correct', function () {",
                  "    pm.expect(j.workspace.name).to.eql('second-workspace');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"name\": \"second-workspace\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/workspaces",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "workspaces"
              ]
            },
            "description": "Creates a second workspace to verify that `acceptAnonimous` defaults to `true` when omitted, and to provide an isolated workspace for cross-workspace isolation tests in the audit folder."
          },
          "response": []
        }
      ],
      "description": "Covers the full workspace lifecycle: get, update (PATCH), and secondary workspace creation.\n\nWorkspace creation itself is in folder 0 since it is a prerequisite for all other folders."
    },
    {
      "name": "2 — Users",
      "item": [
        {
          "name": "2.1 GET /users — list all, capture owner and admin IDs",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "var items = j.items || j;",
                  "pm.test('User list returned', function () { pm.response.to.have.status(200); });",
                  "pm.test('Pagination envelope present', function () {",
                  "    pm.expect(j).to.have.property('items');",
                  "    pm.expect(j).to.have.property('page');",
                  "    pm.expect(j).to.have.property('pageSize');",
                  "    pm.expect(j).to.have.property('totalItems');",
                  "    pm.expect(j).to.have.property('totalPages');",
                  "});",
                  "pm.test('Each user has required fields', function () {",
                  "    items.forEach(function(u) {",
                  "        pm.expect(u).to.have.property('id');",
                  "        pm.expect(u).to.have.property('role');",
                  "        pm.expect(u).to.have.property('providerUid');",
                  "        pm.expect(u).to.have.property('status');",
                  "    });",
                  "});",
                  "var owner = items.find(function(u) { return u.role === 'OWNER'; });",
                  "var admin  = items.find(function(u) { return u.role === 'ADMIN'; });",
                  "if (owner) pm.environment.set('owner_user_id', String(owner.id));",
                  "if (admin)  pm.environment.set('admin_user_id',  String(admin.id));"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users"
              ]
            },
            "description": "Returns all workspace users as a paginated list. Requires ADMIN or OWNER role.\nThe test script captures `owner_user_id` and `admin_user_id` for use in subsequent requests.\n\nOptional query parameters: `page`, `pageSize` (default 20, max 200), `q` (search), `sort`, `dir`."
          },
          "response": []
        },
        {
          "name": "2.2 GET /users — with search and sort",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Search returns 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('items is array', function () {",
                  "    pm.expect(pm.response.json().items).to.be.an('array');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users?sort=role&dir=asc&pageSize=10",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users"
              ],
              "query": [
                {
                  "key": "sort",
                  "value": "role"
                },
                {
                  "key": "dir",
                  "value": "asc"
                },
                {
                  "key": "pageSize",
                  "value": "10"
                }
              ]
            },
            "description": "Demonstrates sorting and pagination parameters. `sort=role&dir=asc` returns users alphabetically by role."
          },
          "response": []
        },
        {
          "name": "2.3 GET /users/{id} — fetch admin user by ID",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('User returned by ID', function () { pm.response.to.have.status(200); });",
                  "pm.test('Correct user ID returned', function () {",
                  "    pm.expect(String(j.id)).to.eql(pm.environment.get('admin_user_id'));",
                  "});",
                  "pm.test('User has all required fields', function () {",
                  "    pm.expect(j).to.have.property('role');",
                  "    pm.expect(j).to.have.property('providerUid');",
                  "    pm.expect(j).to.have.property('status');",
                  "    pm.expect(j).to.have.property('workspaceId');",
                  "    pm.expect(j).to.have.property('authProvider');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users/{{admin_user_id}}",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users",
                "{{admin_user_id}}"
              ]
            },
            "description": "Fetches a single user by numeric ID within the caller's workspace."
          },
          "response": []
        },
        {
          "name": "2.4 PATCH /users/{id} — update admin's userName",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('User updated', function () { pm.response.to.have.status(200); });",
                  "pm.test('userName updated', function () {",
                  "    pm.expect(j.userName).to.eql('alice-admin');",
                  "});",
                  "pm.test('Role unchanged', function () {",
                  "    pm.expect(j.role).to.eql('ADMIN');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"userName\": \"alice-admin\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users/{{admin_user_id}}",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users",
                "{{admin_user_id}}"
              ]
            },
            "description": "Updates a user record. Only the fields included in the body are changed."
          },
          "response": []
        },
        {
          "name": "2.5 GET /users/{id} — verify PATCH persisted",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Updated userName persisted', function () {",
                  "    pm.expect(pm.response.json().userName).to.eql('alice-admin');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users/{{admin_user_id}}",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users",
                "{{admin_user_id}}"
              ]
            },
            "description": "Confirms that the `userName` change from 2.4 was persisted."
          },
          "response": []
        }
      ],
      "description": "Covers user listing, GET by ID, PATCH (update userName), and DELETE.\n\nAnon user delete is intentionally deferred to after session payload tests (folder 5b) so the anon session token remains valid during folder 5."
    },
    {
      "name": "3 — Invites",
      "item": [
        {
          "name": "3.1 POST /invites — create user-role invite",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.environment.set('detail_invite_key', j.secretKey);",
                  "pm.test('Invite created', function () { pm.response.to.have.status(200); });",
                  "pm.test('secretKey present', function () {",
                  "    pm.expect(j.secretKey).to.be.a('string').and.not.empty;",
                  "});",
                  "pm.test('expiresAt present', function () {",
                  "    pm.expect(j.expiresAt).to.be.a('string');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"role\": \"user\",\n    \"ttlSeconds\": 7200\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/invites",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "invites"
              ]
            },
            "description": "Creates a new invite. `secretKey` is returned once and stored in the environment for subsequent GET and DELETE tests.\n`role` must be `admin` or `user` (lowercase). `ttlSeconds` is the invite's validity window."
          },
          "response": []
        },
        {
          "name": "3.2 GET /invites — list and capture ID of new invite",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Invite list returned', function () { pm.response.to.have.status(200); });",
                  "var active = j.filter(function(i) { return i.status === 'active'; });",
                  "active.sort(function(a, b) { return b.id - a.id; });",
                  "if (active.length > 0) {",
                  "    pm.environment.set('detail_invite_id', String(active[0].id));",
                  "}",
                  "pm.test('At least one active invite exists', function () {",
                  "    pm.expect(active.length).to.be.above(0);",
                  "});",
                  "pm.test('Each invite has id, role, expiresAt, status, createdAt', function () {",
                  "    j.forEach(function(i) {",
                  "        pm.expect(i).to.have.all.keys('id', 'role', 'expiresAt', 'status', 'createdAt');",
                  "    });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/invites",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "invites"
              ]
            },
            "description": "Lists all invites for the workspace. Note: the list response does NOT include `secretKey` — use `GET /invites/{id}` to retrieve it."
          },
          "response": []
        },
        {
          "name": "3.3 GET /invites/{id} — includes secretKey",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Invite detail returned', function () { pm.response.to.have.status(200); });",
                  "pm.test('secretKey present and matches', function () {",
                  "    pm.expect(j.secretKey).to.be.a('string').and.not.empty;",
                  "    pm.expect(j.secretKey).to.eql(pm.environment.get('detail_invite_key'));",
                  "});",
                  "pm.test('All detail fields present', function () {",
                  "    pm.expect(j).to.have.all.keys('id', 'secretKey', 'role', 'expiresAt', 'status', 'createdAt');",
                  "});",
                  "pm.test('Status is active', function () {",
                  "    pm.expect(j.status).to.eql('active');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/invites/{{detail_invite_id}}",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "invites",
                "{{detail_invite_id}}"
              ]
            },
            "description": "Returns full invite details including `secretKey`. This is the only endpoint that exposes the secret — use it to share or display the invite link."
          },
          "response": []
        },
        {
          "name": "3.4 DELETE /invites/{id} — revoke invite",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Invite revoked (204)', function () {",
                  "    pm.expect(pm.response.code).to.equal(204);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/invites/{{detail_invite_id}}",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "invites",
                "{{detail_invite_id}}"
              ]
            },
            "description": "Revokes (deletes) an invite. After revocation the `secretKey` can no longer be redeemed. Returns 204 No Content."
          },
          "response": []
        }
      ],
      "description": "Full invite lifecycle: create, list, GET by ID (retrieves the secret key), and DELETE (revoke)."
    },
    {
      "name": "4 — Applications",
      "item": [
        {
          "name": "4.1 GET /applications — list all apps for workspace",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Applications list returned', function () { pm.response.to.have.status(200); });",
                  "pm.test('Is an array', function () { pm.expect(j).to.be.an('array'); });",
                  "pm.test('At least one app present', function () { pm.expect(j.length).to.be.above(0); });",
                  "pm.test('Each app has required fields', function () {",
                  "    j.forEach(function(app) {",
                  "        pm.expect(app).to.have.property('appId');",
                  "        pm.expect(app).to.have.property('displayName');",
                  "        pm.expect(app).to.have.property('redirectUris');",
                  "        pm.expect(app).to.have.property('workspaceId');",
                  "    });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications"
              ]
            },
            "description": "Returns all registered applications for the workspace in the USER JWT."
          },
          "response": []
        },
        {
          "name": "4.2 GET /applications/app-1 — verify two redirectUris stored",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('App returned', function () { pm.response.to.have.status(200); });",
                  "pm.test('Two redirectUris from setup', function () {",
                  "    pm.expect(j.redirectUris).to.be.an('array').with.lengthOf(2);",
                  "    pm.expect(j.redirectUris).to.include('http://stam.com');",
                  "    pm.expect(j.redirectUris).to.include('http://stam.com/callback');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications/app-1",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications",
                "app-1"
              ]
            },
            "description": "Returns a single application by its `appId`."
          },
          "response": []
        },
        {
          "name": "4.3 PATCH /applications/app-1 — add a third redirectUri",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('App updated', function () { pm.response.to.have.status(200); });",
                  "pm.test('redirectUris list now has 3 entries', function () {",
                  "    pm.expect(j.redirectUris).to.be.an('array').with.lengthOf(3);",
                  "    pm.expect(j.redirectUris).to.include('http://stam.com/alt');",
                  "});",
                  "pm.test('displayName unchanged', function () {",
                  "    pm.expect(j.displayName).to.eql('App One');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "PATCH",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"redirectUris\": [\"http://stam.com\", \"http://stam.com/callback\", \"http://stam.com/alt\"]\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications/app-1",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications",
                "app-1"
              ]
            },
            "description": "Replaces the `redirectUris` list. Omitted fields (`displayName`, TTL settings) are not changed.\n\nIMPORTANT: the new list replaces the old one entirely — it is not additive."
          },
          "response": []
        },
        {
          "name": "4.4 POST /applications/validate-redirect — allowlisted URI → valid",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Response is 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('valid=true for allowlisted URI', function () { pm.expect(j.valid).to.be.true; });",
                  "pm.test('reason is ok', function () { pm.expect(j.reason).to.eql('ok'); });"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"appId\": \"app-1\",\n    \"redirectUri\": \"http://stam.com\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications/validate-redirect",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications",
                "validate-redirect"
              ]
            },
            "description": "Public endpoint (no auth required). Validates whether a given `redirectUri` is allowlisted for the given `appId`.\nUsed internally by the hosted-login flow before redirecting back to the third-party app.\nAlways returns 200; inspect `valid` and `reason` in the response body."
          },
          "response": []
        },
        {
          "name": "4.5 POST /applications/validate-redirect — unknown appId → valid=false",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Response is 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('valid=false for unknown appId', function () {",
                  "    pm.expect(pm.response.json().valid).to.be.false;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"appId\": \"nonexistent-app\",\n    \"redirectUri\": \"http://stam.com\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications/validate-redirect",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications",
                "validate-redirect"
              ]
            },
            "description": "Demonstrates that an unknown `appId` causes `valid=false`, not an error status."
          },
          "response": []
        },
        {
          "name": "4.6 POST /applications — register second app (for delete test)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Second app created', function () {",
                  "    pm.response.to.have.status(200);",
                  "    pm.expect(pm.response.json().appId).to.eql('app-temp');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"appId\": \"app-temp\",\n    \"displayName\": \"Temp App\",\n    \"redirectUris\": [\"http://temp.example.com\"]\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications"
              ]
            },
            "description": "Creates a temporary second application to demonstrate DELETE in the next request."
          },
          "response": []
        },
        {
          "name": "4.7 DELETE /applications/app-temp — delete app",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('App deleted (200)', function () {",
                  "    pm.expect(pm.response.code).to.equal(200);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications/app-temp",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications",
                "app-temp"
              ]
            },
            "description": "Deletes a registered application. After deletion, hosted-login redirects to any of its URIs will be rejected."
          },
          "response": []
        }
      ],
      "description": "Application registration, listing, GET by ID, redirect-URI PATCH, redirect validation, and DELETE."
    },
    {
      "name": "5 — Session CRUD and payload types",
      "item": [
        {
          "name": "5.1 GET /sessions — read full payload",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Session payload returned (200)', function () { pm.response.to.have.status(200); });",
                  "pm.test('Response is a flat JSON object', function () {",
                  "    pm.expect(j).to.be.an('object');",
                  "    pm.expect(Array.isArray(j)).to.be.false;",
                  "});",
                  "pm.test('Seed payload keys present', function () {",
                  "    pm.expect(j).to.have.property('name');",
                  "    pm.expect(j.name).to.eql('Alice');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            },
            "description": "Returns the full decrypted payload for the authenticated session.\nThe response is a **flat JSON object** — keys are payload keys, values are payload values. An empty payload returns `{}`."
          },
          "response": []
        },
        {
          "name": "5.2 GET /sessions/get/{key} — read single key",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Key read (200)', function () { pm.response.to.have.status(200); });",
                  "pm.test('Response shape is { key, value }', function () {",
                  "    pm.expect(j).to.have.property('key');",
                  "    pm.expect(j).to.have.property('value');",
                  "});",
                  "pm.test('Correct key and value returned', function () {",
                  "    pm.expect(j.key).to.eql('name');",
                  "    pm.expect(j.value).to.eql('Alice');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/get/name",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "get",
                "name"
              ]
            },
            "description": "Reads a single key from the session payload.\nResponse shape: `{ \"key\": \"name\", \"value\": \"Alice\" }`\n\nNOTE: a 404 response here means the key was never written — it is not an error; return a sensible default in your application."
          },
          "response": []
        },
        {
          "name": "5.3 POST /sessions/set/{key} — write string value",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Key written (200)', function () { pm.response.to.have.status(200); });",
                  "pm.test('Response is updated full payload', function () {",
                  "    pm.expect(j.theme).to.eql('dark');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{ \"value\": \"dark\" }"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/set/theme",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "set",
                "theme"
              ]
            },
            "description": "Sets a key in the session payload. Wrap the value in `{ \"value\": ... }`.\nThe response is the **full updated payload** (same shape as `GET /sessions`)."
          },
          "response": []
        },
        {
          "name": "5.4 POST /sessions/set/{key} — write number value",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Number value stored', function () {",
                  "    pm.expect(pm.response.json().score).to.eql(99);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{ \"value\": 99 }"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/set/score",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "set",
                "score"
              ]
            },
            "description": "Demonstrates that the payload layer stores a JSON number without coercing it to a string."
          },
          "response": []
        },
        {
          "name": "5.5 GET /sessions/get/score — number round-trips correctly",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Number round-trips as number (not string)', function () {",
                  "    pm.expect(j.value).to.eql(99);",
                  "    pm.expect(typeof j.value).to.eql('number');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/get/score",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "get",
                "score"
              ]
            },
            "description": "Confirms the value returned is a JSON number, not the string `\"99\"`."
          },
          "response": []
        },
        {
          "name": "5.6 POST /sessions/set/{key} — write boolean value",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Boolean value stored', function () {",
                  "    pm.expect(pm.response.json().verified).to.eql(true);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{ \"value\": true }"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/set/verified",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "set",
                "verified"
              ]
            }
          },
          "response": []
        },
        {
          "name": "5.7 GET /sessions/get/verified — boolean round-trips correctly",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Boolean round-trips as boolean (not string)', function () {",
                  "    pm.expect(j.value).to.eql(true);",
                  "    pm.expect(typeof j.value).to.eql('boolean');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/get/verified",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "get",
                "verified"
              ]
            }
          },
          "response": []
        },
        {
          "name": "5.8 POST /sessions/set/{key} — write object value",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Object value stored', function () {",
                  "    var j = pm.response.json();",
                  "    pm.expect(j.address).to.be.an('object');",
                  "    pm.expect(j.address.city).to.eql('Tel Aviv');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{ \"value\": { \"city\": \"Tel Aviv\", \"country\": \"IL\" } }"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/set/address",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "set",
                "address"
              ]
            }
          },
          "response": []
        },
        {
          "name": "5.9 GET /sessions/get/address — object round-trips correctly",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Object round-trips intact', function () {",
                  "    pm.expect(j.value).to.be.an('object');",
                  "    pm.expect(j.value.city).to.eql('Tel Aviv');",
                  "    pm.expect(j.value.country).to.eql('IL');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/get/address",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "get",
                "address"
              ]
            }
          },
          "response": []
        },
        {
          "name": "5.10 POST /sessions/set/{key} — write array value",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Array value stored', function () {",
                  "    pm.expect(pm.response.json().tags).to.be.an('array').with.lengthOf(3);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{ \"value\": [\"alpha\", \"beta\", \"gamma\"] }"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/set/tags",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "set",
                "tags"
              ]
            }
          },
          "response": []
        },
        {
          "name": "5.11 GET /sessions/get/tags — array round-trips correctly",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Array round-trips intact', function () {",
                  "    pm.expect(j.value).to.be.an('array').with.lengthOf(3);",
                  "    pm.expect(j.value[0]).to.eql('alpha');",
                  "    pm.expect(j.value[2]).to.eql('gamma');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/get/tags",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "get",
                "tags"
              ]
            }
          },
          "response": []
        },
        {
          "name": "5.12 DELETE /sessions/delete/{key} — delete existing key",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Existing key deleted — returns true', function () {",
                  "    pm.response.to.have.status(200);",
                  "    pm.expect(pm.response.json()).to.eql(true);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/delete/theme",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "delete",
                "theme"
              ]
            },
            "description": "Removes a key from the session payload. Returns `true` if the key existed and was deleted."
          },
          "response": []
        },
        {
          "name": "5.13 DELETE /sessions/delete/{key} — absent key returns false",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Deleting a key that was never written returns false (not an error).",
                  "pm.test('Absent key delete returns false — not an error', function () {",
                  "    pm.response.to.have.status(200);",
                  "    pm.expect(pm.response.json()).to.eql(false);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/delete/key_that_was_never_written",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "delete",
                "key_that_was_never_written"
              ]
            },
            "description": "Returns `false` (not an error) when the key does not exist. Your application should treat this as a no-op."
          },
          "response": []
        }
      ],
      "description": "Exercises the core session read/write/delete operations and verifies that the payload layer round-trips all JSON types correctly: number, boolean, object, and array.\n\nAll requests use the `anon_session_token` from folder 0."
    },
    {
      "name": "6 — Session advanced: get-or-create and direct renew",
      "item": [
        {
          "name": "6.0 Register app-goc — isolated app for get-or-create tests",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('app-goc registered (200)', function () {",
                  "    pm.response.to.have.status(200);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"appId\": \"app-goc\",\n    \"displayName\": \"Get-or-create test app\",\n    \"redirectUris\": [\"http://stam.com\"]\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications"
              ]
            },
            "description": "Dedicated app id so get-or-create tests are not affected by earlier POST /sessions rows on app-1."
          },
          "response": []
        },
        {
          "name": "6.1 POST /sessions/get-or-create — first call creates session",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.environment.set('goc_session_token', j.sessionToken);",
                  "pm.test('Session created or reused (200)', function () {",
                  "    pm.response.to.have.status(200);",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-goc\",\n    \"payload\": { \"source\": \"get-or-create\" }\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/get-or-create",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "get-or-create"
              ]
            },
            "description": "Idempotent session creation. If an **active** session already exists for `(workspaceName, Firebase UID, app_id)`, returns a fresh token for that session (no duplicate row). Otherwise creates a new session.\n\nThis is the recommended endpoint for most integrations — it is safe to call on every app load."
          },
          "response": []
        },
        {
          "name": "6.2 POST /sessions/get-or-create — second call reuses active session",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// The session ID embedded in both tokens must be the same,",
                  "// proving the server reused the active session rather than creating a duplicate.",
                  "var j = pm.response.json();",
                  "var prev = pm.environment.get('goc_session_token');",
                  "pm.test('Returns 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('Returns a valid sessionToken', function () {",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});",
                  "// Compare subject claim — same session row.",
                  "function sub(token) {",
                  "    try {",
                  "        return JSON.parse(atob(token.split('.')[1].replace(/-/g,'+').replace(/_/g,'/'))).sub;",
                  "    } catch(e) { return null; }",
                  "}",
                  "pm.test('Same session row reused (same sub claim)', function () {",
                  "    pm.expect(sub(j.sessionToken)).to.eql(sub(prev));",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-goc\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/get-or-create",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "get-or-create"
              ]
            },
            "description": "Second identical call — the server finds the existing active session and returns a new token for it instead of creating a new row. The JWT `sub` claim (session ID) is asserted to be the same."
          },
          "response": []
        },
        {
          "name": "6.3 POST /sessions/renew/{sessionToken} — direct token renewal",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// URL-encode the session JWT because it contains '.' and '+' characters",
                  "// that would be misinterpreted as path delimiters or spaces.",
                  "var token = pm.environment.get('goc_session_token') || '';",
                  "pm.variables.set('goc_session_token_encoded', encodeURIComponent(token));"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Renew returns 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('New sessionToken returned', function () {",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});",
                  "pm.test('Payload returned alongside token', function () {",
                  "    pm.expect(j.payload).to.be.an('object');",
                  "});",
                  "pm.environment.set('goc_renewed_token', j.sessionToken);"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/renew/{{goc_session_token_encoded}}",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "renew",
                "{{goc_session_token_encoded}}"
              ]
            },
            "description": "Renews a session JWT using a Firebase ID token (no hosted-login redirect required).\nThe previous session token (even if expired) is passed URL-encoded in the path.\nReturns a new `sessionToken` (with incremented `tokenRevision`) and the current decrypted `payload`.\n\nThis invalidates the previous token — it will return 401/410 after renewal."
          },
          "response": []
        },
        {
          "name": "6.4 GET /sessions — renewed token works, data intact",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Renewed token grants access (200)', function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "pm.test('Session data intact after renew', function () {",
                  "    var j = pm.response.json();",
                  "    pm.expect(j.source).to.eql('get-or-create');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{goc_renewed_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            },
            "description": "Confirms that the renewed token grants access and that the original session payload is still intact. Data is never lost on token renewal."
          },
          "response": []
        }
      ],
      "description": "Demonstrates `POST /sessions/get-or-create` (safe for repeated calls — reuses active session) and `POST /sessions/renew/{sessionToken}` (direct token renewal using a Firebase ID token, without going through hosted-login)."
    },
    {
      "name": "7 — sessions/my (user self-service)",
      "item": [
        {
          "name": "7.1 Re-create anon user and get fresh USER JWT",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Anon user available', function () { pm.response.to.have.status(200); });"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users/anonymous",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users",
                "anonymous"
              ]
            },
            "description": "Ensures the anon user still exists (it may have been deleted in a previous run). Idempotent — returns existing user if already present."
          },
          "response": []
        },
        {
          "name": "7.2 Anon user login — get USER JWT",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.environment.set('anon_token', j.accessToken);",
                  "pm.test('Anon USER JWT issued', function () {",
                  "    pm.expect(j.accessToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/users/login",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "users",
                "login"
              ]
            }
          },
          "response": []
        },
        {
          "name": "7.3 Create anon session — seed /sessions/my",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.environment.set('anon_session_token', j.sessionToken);",
                  "var parts = j.sessionToken.split('.');",
                  "var payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));",
                  "pm.environment.set('my_session_id_for_revoke', String(payload.sub));",
                  "pm.test('Anon session created', function () {",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-1\",\n    \"payload\": { \"name\": \"Alice\", \"score\": 42 }\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            }
          },
          "response": []
        },
        {
          "name": "7.4 GET /sessions/my — default listing with full shape assertion",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Response is 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('Pagination envelope present', function () {",
                  "    pm.expect(j).to.have.property('items');",
                  "    pm.expect(j).to.have.property('page');",
                  "    pm.expect(j).to.have.property('pageSize');",
                  "    pm.expect(j).to.have.property('totalItems');",
                  "    pm.expect(j).to.have.property('totalPages');",
                  "});",
                  "pm.test('items is array and non-empty', function () {",
                  "    pm.expect(j.items).to.be.an('array').and.not.empty;",
                  "});",
                  "var s = j.items[0];",
                  "pm.test('Session has all required fields', function () {",
                  "    pm.expect(s).to.have.property('id');",
                  "    pm.expect(s).to.have.property('workspaceId');",
                  "    pm.expect(s).to.have.property('providerUid');",
                  "    pm.expect(s).to.have.property('status');",
                  "    pm.expect(s).to.have.property('payload');",
                  "    pm.expect(s).to.have.property('createdAt');",
                  "});",
                  "// Capture ID of first active session for revoke test.",
                  "var active = j.items.find(function(x) { return x.status === 'active'; });",
                  "if (active) pm.environment.set('my_session_id_for_revoke', String(active.id));"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/my",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "my"
              ]
            },
            "description": "Returns the caller's own sessions as a paginated list. No ADMIN/OWNER privilege required.\nPayload is included only for active sessions. Sort defaults to `created desc`."
          },
          "response": []
        },
        {
          "name": "7.5 GET /sessions/my — filter status=active",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('All returned sessions are active', function () {",
                  "    pm.response.to.have.status(200);",
                  "    j.items.forEach(function(s) { pm.expect(s.status).to.eql('active'); });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/my?status=active",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "my"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "active"
                }
              ]
            },
            "description": "Filters to active sessions only. Valid `status` values: `all`, `active`, `expired`, `revoked`."
          },
          "response": []
        },
        {
          "name": "7.6 GET /sessions/my — filter by app_id",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Response is 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('All items belong to app-1', function () {",
                  "    pm.response.json().items.forEach(function(s) {",
                  "        if (s.appId) pm.expect(s.appId).to.eql('app-1');",
                  "    });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/my?app_id=app-1",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "my"
              ],
              "query": [
                {
                  "key": "app_id",
                  "value": "app-1"
                }
              ]
            }
          },
          "response": []
        },
        {
          "name": "7.7 POST /sessions/revoke/{sessionId} — user revokes own session",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Session revoked by USER JWT', function () {",
                  "    pm.response.to.have.status(200);",
                  "    pm.expect(j.revoked).to.be.true;",
                  "    pm.expect(String(j.sessionId)).to.eql(pm.environment.get('my_session_id_for_revoke'));",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/revoke/{{my_session_id_for_revoke}}",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "revoke",
                "{{my_session_id_for_revoke}}"
              ]
            },
            "description": "Lets a user revoke one of their own sessions by session ID using a USER JWT (not a session token). A user cannot revoke sessions belonging to other users."
          },
          "response": []
        },
        {
          "name": "7.8 GET /sessions — SESSION token dead after USER-JWT revoke (410)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// The session token for the revoked session must now return 410.",
                  "// 410 ≠ data loss — the session row and payload still exist;",
                  "// only the credential (token) is invalidated.",
                  "pm.test('Revoked session token returns 410', function () {",
                  "    pm.expect(pm.response.code).to.equal(410);",
                  "});",
                  "pm.test('Error body shape correct', function () {",
                  "    var body = pm.response.json();",
                  "    pm.expect(body.status).to.equal(410);",
                  "    pm.expect(body.error).to.eql('Session revoked');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{anon_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            },
            "description": "Confirms that the session token corresponding to the just-revoked session ID is now rejected with 410."
          },
          "response": []
        }
      ],
      "description": "The `GET /sessions/my` endpoint lets a workspace user list and manage their own sessions without needing ADMIN/OWNER privileges. Covers default listing, status filter, app_id filter, and sort.\n\nNote: `POST /sessions/revoke/{sessionId}` (user-scoped revoke by session ID) is tested at the end of this folder after the listing tests."
    },
    {
      "name": "8 — sessions/admin (workspace view)",
      "item": [
        {
          "name": "8.1 GET /sessions/admin — default listing, envelope shape",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('200 returned', function () { pm.response.to.have.status(200); });",
                  "pm.test('Pagination envelope present', function () {",
                  "    ['items','page','pageSize','totalItems','totalPages'].forEach(function(k) {",
                  "        pm.expect(j).to.have.property(k);",
                  "    });",
                  "});",
                  "pm.test('items is an array', function () { pm.expect(j.items).to.be.an('array'); });",
                  "pm.test('Admin list items have no payload field', function () {",
                  "    j.items.forEach(function(s) {",
                  "        pm.expect(s).to.not.have.property('payload');",
                  "    });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/admin",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "admin"
              ]
            },
            "description": "Cross-user session list for the workspace. Returns metadata only — payload content is never included in list views. Requires ADMIN or OWNER."
          },
          "response": []
        },
        {
          "name": "8.2 GET /sessions/admin — filter status=active",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('All returned sessions are active', function () {",
                  "    pm.response.to.have.status(200);",
                  "    j.items.forEach(function(s) { pm.expect(s.status).to.eql('active'); });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/admin?status=active",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "admin"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "active"
                }
              ]
            }
          },
          "response": []
        },
        {
          "name": "8.3 Seed — admin revoke owner session (for revoked filter)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Owner session revoked for filter test', function () {",
                  "    pm.response.to.have.status(200);",
                  "    pm.expect(pm.response.json().revoked).to.be.true;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/admin/revoke/{{owner_session_id}}",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "admin",
                "revoke",
                "{{owner_session_id}}"
              ]
            },
            "description": "Admin-revokes the owner's session to seed a `revoked` row for the next filter test."
          },
          "response": []
        },
        {
          "name": "8.4 GET /sessions/admin — filter status=revoked",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Response 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('All returned sessions are revoked', function () {",
                  "    j.items.forEach(function(s) { pm.expect(s.status).to.eql('revoked'); });",
                  "});",
                  "pm.test('At least one revoked session', function () {",
                  "    pm.expect(j.items.length).to.be.above(0);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/admin?status=revoked",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "admin"
              ],
              "query": [
                {
                  "key": "status",
                  "value": "revoked"
                }
              ]
            }
          },
          "response": []
        },
        {
          "name": "8.5 GET /sessions/admin — filter app_id + sort + page",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('200 returned', function () { pm.response.to.have.status(200); });",
                  "pm.test('Correct page number echoed', function () {",
                  "    pm.expect(pm.response.json().page).to.eql(0);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/admin?app_id=app-1&sort=created&dir=desc&page=0&status=all",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "admin"
              ],
              "query": [
                {
                  "key": "app_id",
                  "value": "app-1"
                },
                {
                  "key": "sort",
                  "value": "created"
                },
                {
                  "key": "dir",
                  "value": "desc"
                },
                {
                  "key": "page",
                  "value": "0"
                },
                {
                  "key": "status",
                  "value": "all"
                }
              ]
            },
            "description": "Combines app_id filter, sort, direction, and page number in one call. All query params are optional and composable."
          },
          "response": []
        },
        {
          "name": "8.6 GET /sessions/admin — search by session ID (q param)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Search returns 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('Returned row matches searched ID', function () {",
                  "    var target = pm.environment.get('owner_session_id');",
                  "    var match = j.items.find(function(s) { return String(s.id) === target; });",
                  "    pm.expect(match).to.not.be.undefined;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/admin?q={{owner_session_id}}&status=all",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "admin"
              ],
              "query": [
                {
                  "key": "q",
                  "value": "{{owner_session_id}}"
                },
                {
                  "key": "status",
                  "value": "all"
                }
              ]
            },
            "description": "The `q` parameter searches by session ID, user identifier, or app ID."
          },
          "response": []
        },
        {
          "name": "8.7 GET /sessions/admin/{sessionId} — single session detail with payload",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// The single-session detail endpoint returns the decrypted payload",
                  "// for ADMIN/OWNER — unlike the list endpoint which omits it.",
                  "pm.test('200 returned', function () { pm.response.to.have.status(200); });",
                  "pm.test('Session detail includes id', function () {",
                  "    pm.expect(pm.response.json().id).to.not.be.undefined;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/admin/{{owner_session_id}}",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "admin",
                "{{owner_session_id}}"
              ]
            },
            "description": "Returns full detail for a single workspace session, including the decrypted payload for ADMIN/OWNER. All such accesses are recorded in the audit log."
          },
          "response": []
        },
        {
          "name": "8.8 Seed — create active owner session for TTL test",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "var parts = j.sessionToken.split('.');",
                  "var payload = JSON.parse(atob(parts[1].replace(/-/g,'+').replace(/_/g,'/')));",
                  "pm.environment.set('ttl_test_session_id', String(payload.sub));",
                  "pm.test('Session for TTL test created', function () {",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-1\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            }
          },
          "response": []
        },
        {
          "name": "8.9 POST /sessions/admin/{sessionId}/ttl — update session TTL",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('TTL updated (200)', function () { pm.response.to.have.status(200); });",
                  "pm.test('updated=true', function () { pm.expect(j.updated).to.be.true; });",
                  "pm.test('sessionId matches', function () {",
                  "    pm.expect(String(j.sessionId)).to.eql(pm.environment.get('ttl_test_session_id'));",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"ttlSeconds\": 7200\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/admin/{{ttl_test_session_id}}/ttl",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "admin",
                "{{ttl_test_session_id}}",
                "ttl"
              ]
            },
            "description": "Overrides the payload expiration for a specific active session. `ttlSeconds=0` means unlimited. Allowed range: `0` or `60–315360000`."
          },
          "response": []
        },
        {
          "name": "8.10 GET /sessions/admin/analytics/concurrent-sessions",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Analytics returned (200)', function () { pm.response.to.have.status(200); });",
                  "pm.test('Response is an array', function () { pm.expect(j).to.be.an('array'); });",
                  "pm.test('Each sample has date and count', function () {",
                  "    j.forEach(function(s) {",
                  "        pm.expect(s).to.have.property('date');",
                  "        pm.expect(s).to.have.property('count');",
                  "        pm.expect(s.count).to.be.a('number');",
                  "    });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/admin/analytics/concurrent-sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "admin",
                "analytics",
                "concurrent-sessions"
              ]
            },
            "description": "Returns daily concurrent active session counts for the current calendar month (default). Accepts optional `start` and `end` query params (ISO-8601 dates, max 61-day range).\n\nUseful for dashboard widgets and capacity planning."
          },
          "response": []
        }
      ],
      "description": "Admin/owner cross-user session management: listing with all filters, GET single session with payload, update TTL, and admin revoke."
    },
    {
      "name": "9 — sessions/export",
      "item": [
        {
          "name": "9.1 Seed — create active owner session for export",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Export seed session created', function () {",
                  "    pm.expect(pm.response.json().sessionToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-1\",\n    \"payload\": { \"exportSeed\": true }\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            }
          },
          "response": []
        },
        {
          "name": "9.2 GET /sessions/export — full shape assertion",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Export response is 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('Pagination envelope present', function () {",
                  "    pm.expect(j).to.have.property('items');",
                  "    pm.expect(j).to.have.property('totalItems');",
                  "    pm.expect(j).to.have.property('page');",
                  "    pm.expect(j).to.have.property('pageSize');",
                  "    pm.expect(j).to.have.property('totalPages');",
                  "});",
                  "pm.test('items is a non-empty array', function () {",
                  "    pm.expect(j.items).to.be.an('array').and.not.empty;",
                  "});",
                  "pm.test('Each export row has userEmail, appId, payload', function () {",
                  "    j.items.forEach(function(row) {",
                  "        pm.expect(row).to.have.property('userEmail');",
                  "        pm.expect(row).to.have.property('appId');",
                  "        pm.expect(row).to.have.property('payload');",
                  "    });",
                  "});",
                  "pm.test('Payload is a flat object (no properties wrapper)', function () {",
                  "    j.items.forEach(function(row) {",
                  "        if (row.payload !== null) {",
                  "            pm.expect(row.payload).to.be.an('object');",
                  "            pm.expect(row.payload).to.not.have.property('properties');",
                  "        }",
                  "    });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/export",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "export"
              ]
            },
            "description": "Returns active sessions with decrypted payloads. Unlike `GET /sessions/admin`, this endpoint includes the full payload content.\n\nOnly active sessions are returned (no `status` filter). Page size uses server config `sessions.export.page-size` (default 100, max 200)."
          },
          "response": []
        }
      ],
      "description": "Admin/owner data export with decrypted payload. Returns `email`, `appId`, and a flat payload map for each active session."
    },
    {
      "name": "10 — Audit logs",
      "item": [
        {
          "name": "10.1 GET /audit/events — field-level shape assertion",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Audit events returned (200)', function () { pm.response.to.have.status(200); });",
                  "pm.test('items array present and non-empty', function () {",
                  "    pm.expect(j.items).to.be.an('array').and.not.empty;",
                  "});",
                  "pm.test('Each event has required fields', function () {",
                  "    j.items.forEach(function(e) {",
                  "        pm.expect(e).to.have.property('id');",
                  "        pm.expect(e).to.have.property('eventType');",
                  "        pm.expect(e).to.have.property('occurredAt');",
                  "        pm.expect(e).to.have.property('sessionUserIdentifier');",
                  "        pm.expect(e).to.have.property('sessionAppId');",
                  "        pm.expect(e).to.have.property('sessionStatus');",
                  "        pm.expect(e).to.have.property('actorProviderUid');",
                  "        pm.expect(e).to.have.property('actorDisplayName');",
                  "        pm.expect(e).to.have.property('actorEmail');",
                  "    });",
                  "});",
                  "pm.test('No event leaks payload content', function () {",
                  "    j.items.forEach(function(e) {",
                  "        pm.expect(e).to.not.have.property('payload');",
                  "    });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/audit/events",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "audit",
                "events"
              ]
            },
            "description": "Workspace audit trail: session creation, revocation, key writes, user logins, and admin actions. Always newest-first.\n\nKey fields: `eventType` (e.g. `SESSION_CREATED`, `SESSION_KEY_WRITTEN`), `occurredAt`, `sessionUserEmail`/`sessionUserIdentifier`, `actorEmail`/`actorProviderUid`.\nPayload content is **never** stored or returned here."
          },
          "response": []
        },
        {
          "name": "10.2 GET /audit/events — page 2 (pagination)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('200 returned', function () { pm.response.to.have.status(200); });",
                  "pm.test('page=1 echoed', function () {",
                  "    pm.expect(pm.response.json().page).to.eql(1);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/audit/events?page=1&pageSize=5",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "audit",
                "events"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "1"
                },
                {
                  "key": "pageSize",
                  "value": "5"
                }
              ]
            }
          },
          "response": []
        },
        {
          "name": "10.3 GET /audit/api-access — field-level shape assertion",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('API access log returned (200)', function () { pm.response.to.have.status(200); });",
                  "pm.test('items array present and non-empty', function () {",
                  "    pm.expect(j.items).to.be.an('array').and.not.empty;",
                  "});",
                  "pm.test('Each entry has required fields', function () {",
                  "    j.items.forEach(function(e) {",
                  "        pm.expect(e).to.have.property('id');",
                  "        pm.expect(e).to.have.property('httpMethod');",
                  "        pm.expect(e).to.have.property('endpoint');",
                  "        pm.expect(e).to.have.property('httpStatus');",
                  "        pm.expect(e).to.have.property('occurredAt');",
                  "        pm.expect(e).to.have.property('durationMs');",
                  "    });",
                  "});",
                  "pm.test('No entry leaks payload content', function () {",
                  "    j.items.forEach(function(e) {",
                  "        pm.expect(e).to.not.have.property('payload');",
                  "    });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/audit/api-access",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "audit",
                "api-access"
              ]
            },
            "description": "HTTP API access log: method, path, status, duration, session ID, user email. Newest-first. No request or response bodies are stored."
          },
          "response": []
        },
        {
          "name": "10.4 GET /audit/events — second workspace sees no cross-workspace events",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// Workspace isolation: the second workspace must not see events from workspace 1.",
                  "pm.test('Second workspace audit returns 200', function () { pm.response.to.have.status(200); });",
                  "pm.test('No workspaceId=1 rows visible from second workspace token', function () {",
                  "    var items = pm.response.json().items || [];",
                  "    items.forEach(function(e) {",
                  "        if (e.workspaceId !== undefined) {",
                  "            pm.expect(e.workspaceId).to.not.eql(1);",
                  "        }",
                  "    });",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{second_workspace_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/audit/events",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "audit",
                "events"
              ]
            },
            "description": "Verifies workspace isolation in the audit log: the second workspace's token must not expose events from the first workspace."
          },
          "response": []
        }
      ],
      "description": "Workspace audit trail (`/audit/events`) and HTTP access log (`/audit/api-access`). Both are append-only, newest-first, and never contain payload content."
    },
    {
      "name": "11 — auth/callback (hosted-login integration)",
      "item": [
        {
          "name": "11.0 Register app-callback — isolated app for auth/callback tests",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('app-callback registered (200)', function () {",
                  "    pm.response.to.have.status(200);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"appId\": \"app-callback\",\n    \"displayName\": \"Auth callback test app\",\n    \"redirectUris\": [\"http://stam.com\"]\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/applications",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "applications"
              ]
            },
            "description": "Dedicated app id so auth/callback tests are not affected by earlier POST /sessions rows on app-1."
          },
          "response": []
        },
        {
          "name": "11.1 Create path — 302 redirect with sessionToken in Location",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "// Build the full URL dynamically so we can URL-encode the payload parameter.",
                  "pm.variables.set('skip_200_check', true);",
                  "var base = pm.variables.replaceIn('{{protocol}}') + '://' + pm.variables.replaceIn('{{address}}') + '/api/v1/auth/callback';",
                  "var payload = encodeURIComponent(JSON.stringify({ source: 'callback-test' }));",
                  "var qs = 'firebaseToken=' + encodeURIComponent(pm.environment.get('firebase_anon_token') || '')",
                  "    + '&client_redirect_uri=' + encodeURIComponent('http://stam.com')",
                  "    + '&workspace_name=demo-workspace&app_id=app-callback&payload=' + payload;",
                  "pm.request.url = base + '?' + qs;"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Callback returns 302', function () {",
                  "    pm.expect(pm.response.code).to.equal(302);",
                  "});",
                  "var location = pm.response.headers.get('Location') || '';",
                  "pm.test('Location header present', function () {",
                  "    pm.expect(location).to.be.a('string').and.not.empty;",
                  "});",
                  "pm.test('sessionToken present in redirect URL', function () {",
                  "    pm.expect(location).to.include('sessionToken=');",
                  "});",
                  "var match = location.match(/sessionToken=([^&]+)/);",
                  "if (match) pm.environment.set('callback_session_token', decodeURIComponent(match[1]));"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "protocolProfileBehavior": {
            "followRedirects": false
          },
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/auth/callback",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "callback"
              ]
            },
            "description": "Simulates the browser redirect that the hosted-login UI performs after sign-in.\n\nParameters:\n- `firebaseToken` — the Firebase ID token obtained after sign-in\n- `client_redirect_uri` — must be allowlisted for `app_id`\n- `workspace_name` — target workspace\n- `app_id` — registered application\n- `payload` (optional) — URL-encoded JSON object to seed the session payload on creation\n- `session_token` (optional, mutually exclusive with `payload`) — previous token to renew\n- `state` (optional) — CSRF token echoed back in the redirect\n- `ttl_seconds` (optional) — session lifetime (0=unlimited, or 60–604800)"
          },
          "response": []
        },
        {
          "name": "11.2 Created session is usable — GET /sessions with callback token",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Callback-created session is usable (200)', function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "pm.test('Seeded payload key present', function () {",
                  "    pm.expect(pm.response.json().source).to.eql('callback-test');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{callback_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            }
          },
          "response": []
        },
        {
          "name": "11.3 State passthrough — state echoed in redirect",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Callback returns 302', function () { pm.expect(pm.response.code).to.equal(302); });",
                  "var loc = pm.response.headers.get('Location') || '';",
                  "pm.test('state parameter echoed in redirect', function () {",
                  "    pm.expect(loc).to.include('state=csrf-token-abc123');",
                  "});",
                  "pm.test('sessionToken also present', function () { pm.expect(loc).to.include('sessionToken='); });"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "protocolProfileBehavior": {
            "followRedirects": false
          },
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/auth/callback?firebaseToken={{firebase_owner_token}}&client_redirect_uri=http%3A%2F%2Fstam.com&workspace_name=demo-workspace&app_id=app-callback&state=csrf-token-abc123",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "callback"
              ],
              "query": [
                {
                  "key": "firebaseToken",
                  "value": "{{firebase_owner_token}}"
                },
                {
                  "key": "client_redirect_uri",
                  "value": "http%3A%2F%2Fstam.com"
                },
                {
                  "key": "workspace_name",
                  "value": "demo-workspace"
                },
                {
                  "key": "app_id",
                  "value": "app-callback"
                },
                {
                  "key": "state",
                  "value": "csrf-token-abc123"
                }
              ]
            },
            "description": "The `state` parameter (used for CSRF protection in OAuth-style flows) must be echoed verbatim in the redirect URL."
          },
          "response": []
        },
        {
          "name": "11.4 TTL boundary — ttl_seconds=60 (minimum)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('ttl_seconds=60 accepted (302)', function () {",
                  "    pm.expect(pm.response.code).to.equal(302);",
                  "    pm.expect(pm.response.headers.get('Location')).to.include('sessionToken=');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "protocolProfileBehavior": {
            "followRedirects": false
          },
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/auth/callback?firebaseToken={{firebase_owner_token}}&client_redirect_uri=http%3A%2F%2Fstam.com&workspace_name=demo-workspace&app_id=app-callback&ttl_seconds=60",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "callback"
              ],
              "query": [
                {
                  "key": "firebaseToken",
                  "value": "{{firebase_owner_token}}"
                },
                {
                  "key": "client_redirect_uri",
                  "value": "http%3A%2F%2Fstam.com"
                },
                {
                  "key": "workspace_name",
                  "value": "demo-workspace"
                },
                {
                  "key": "app_id",
                  "value": "app-callback"
                },
                {
                  "key": "ttl_seconds",
                  "value": "60"
                }
              ]
            }
          },
          "response": []
        },
        {
          "name": "11.5 TTL boundary — ttl_seconds=604800 (maximum, 7 days)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('ttl_seconds=604800 accepted (302)', function () {",
                  "    pm.expect(pm.response.code).to.equal(302);",
                  "    pm.expect(pm.response.headers.get('Location')).to.include('sessionToken=');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "protocolProfileBehavior": {
            "followRedirects": false
          },
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/auth/callback?firebaseToken={{firebase_owner_token}}&client_redirect_uri=http%3A%2F%2Fstam.com&workspace_name=demo-workspace&app_id=app-callback&ttl_seconds=604800",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "callback"
              ],
              "query": [
                {
                  "key": "firebaseToken",
                  "value": "{{firebase_owner_token}}"
                },
                {
                  "key": "client_redirect_uri",
                  "value": "http%3A%2F%2Fstam.com"
                },
                {
                  "key": "workspace_name",
                  "value": "demo-workspace"
                },
                {
                  "key": "app_id",
                  "value": "app-callback"
                },
                {
                  "key": "ttl_seconds",
                  "value": "604800"
                }
              ]
            }
          },
          "response": []
        },
        {
          "name": "11.6 TTL=0 (unlimited session)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('ttl_seconds=0 (unlimited) accepted (302)', function () {",
                  "    pm.expect(pm.response.code).to.equal(302);",
                  "    pm.expect(pm.response.headers.get('Location')).to.include('sessionToken=');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "protocolProfileBehavior": {
            "followRedirects": false
          },
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/auth/callback?firebaseToken={{firebase_owner_token}}&client_redirect_uri=http%3A%2F%2Fstam.com&workspace_name=demo-workspace&app_id=app-callback&ttl_seconds=0",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "callback"
              ],
              "query": [
                {
                  "key": "firebaseToken",
                  "value": "{{firebase_owner_token}}"
                },
                {
                  "key": "client_redirect_uri",
                  "value": "http%3A%2F%2Fstam.com"
                },
                {
                  "key": "workspace_name",
                  "value": "demo-workspace"
                },
                {
                  "key": "app_id",
                  "value": "app-callback"
                },
                {
                  "key": "ttl_seconds",
                  "value": "0"
                }
              ]
            }
          },
          "response": []
        },
        {
          "name": "11.7 Renew path — session_token present, no payload",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);",
                  "// session_token must be URL-encoded in the query string.",
                  "var base = pm.variables.replaceIn('{{protocol}}') + '://' + pm.variables.replaceIn('{{address}}') + '/api/v1/auth/callback';",
                  "var sessionToken = pm.environment.get('callback_session_token') || '';",
                  "var qs = 'firebaseToken=' + encodeURIComponent(pm.environment.get('firebase_anon_token') || '')",
                  "    + '&client_redirect_uri=' + encodeURIComponent('http://stam.com')",
                  "    + '&workspace_name=demo-workspace&app_id=app-callback'",
                  "    + '&session_token=' + encodeURIComponent(sessionToken);",
                  "pm.request.url = base + '?' + qs;"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Renew via callback returns 302', function () {",
                  "    pm.expect(pm.response.code).to.equal(302);",
                  "});",
                  "var loc = pm.response.headers.get('Location') || '';",
                  "pm.test('New sessionToken present in redirect', function () {",
                  "    pm.expect(loc).to.include('sessionToken=');",
                  "});",
                  "pm.test('No error in redirect', function () {",
                  "    pm.expect(loc).to.not.include('error=');",
                  "});",
                  "var match = loc.match(/sessionToken=([^&]+)/);",
                  "if (match) pm.environment.set('callback_renewed_token', decodeURIComponent(match[1]));"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "protocolProfileBehavior": {
            "followRedirects": false
          },
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/auth/callback",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "callback"
              ]
            },
            "description": "Renew path: pass the previous `session_token` (may be expired) instead of `payload`. The server renews the session and returns a new token pointing to the same session data.\n\n`payload` and `session_token` are mutually exclusive — never send both."
          },
          "response": []
        },
        {
          "name": "11.8 Renewed token works — same session data",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Renewed token grants access (200)', function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "pm.test('Original session data intact after renew', function () {",
                  "    pm.expect(pm.response.json().source).to.eql('callback-test');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{callback_renewed_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            },
            "description": "Data is never lost on token renewal — the `source` key written during session creation must still be present."
          },
          "response": []
        },
        {
          "name": "11.9 Old callback token dead after renew (401 or 410)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "// After renewal, the old token's tokenRevision is superseded.",
                  "// Any call with the old token returns 401 or 410.",
                  "pm.test('Old token rejected after renew (401 or 410)', function () {",
                  "    pm.expect([401, 410]).to.include(pm.response.code);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{callback_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            },
            "description": "After renewing a token (via direct renew or callback renew), the previous token is invalidated and must return 401 or 410."
          },
          "response": []
        }
      ],
      "description": "Tests the `GET /api/v1/auth/callback` endpoint — the server-side leg of the hosted-login redirect flow.\n\nThis endpoint is normally called automatically by the hosted-login UI after sign-in, not by the app directly. It is tested here to verify the create path, renew path, state passthrough, and TTL boundary values."
    },
    {
      "name": "12 — Logout",
      "item": [
        {
          "name": "12.1 Seed — create session for logout test",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.environment.set('logout_session_token', j.sessionToken);",
                  "pm.test('Logout test session created', function () {",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-1\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            }
          },
          "response": []
        },
        {
          "name": "12.2 GET /auth/logout — redirect logout (browser-safe)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);",
                  "// Build redirect logout URL with URL-encoded session_token.",
                  "var base = pm.variables.replaceIn('{{protocol}}') + '://' + pm.variables.replaceIn('{{address}}') + '/api/v1/auth/logout';",
                  "var token = pm.environment.get('logout_session_token') || '';",
                  "var qs = 'session_token=' + encodeURIComponent(token)",
                  "    + '&redirect_uri=' + encodeURIComponent('http://stam.com');",
                  "pm.request.url = base + '?' + qs;"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Redirect logout returns 302', function () {",
                  "    pm.expect(pm.response.code).to.equal(302);",
                  "});",
                  "pm.test('Location redirects to redirect_uri', function () {",
                  "    var loc = pm.response.headers.get('Location') || '';",
                  "    pm.expect(loc).to.include('stam.com');",
                  "});",
                  "pm.test('Clear-Site-Data header set', function () {",
                  "    var csd = pm.response.headers.get('Clear-Site-Data') || '';",
                  "    pm.expect(csd).to.include('cookies');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "protocolProfileBehavior": {
            "followRedirects": false
          },
          "request": {
            "auth": {
              "type": "noauth"
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/auth/logout",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "logout"
              ]
            },
            "description": "**Recommended for browser apps.** Performs a top-level navigation to this URL (not an XHR/fetch call). The server:\n1. Invalidates `session_token` (increments `tokenRevision`)\n2. Revokes Firebase refresh tokens for the session owner\n3. Sets `Clear-Site-Data: \"cookies\"` — which browsers honour in a first-party context\n4. Redirects to `redirect_uri`\n\nIMPORTANT: `redirect_uri` must NOT contain `session_token` — doing so silently renews the session, defeating logout."
          },
          "response": []
        },
        {
          "name": "12.3 Verify — session token dead after redirect logout (401 or 410)",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Session token rejected after redirect logout (401 or 410)', function () {",
                  "    pm.expect([401, 410]).to.include(pm.response.code);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{logout_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            },
            "description": "Confirms the session token used in the logout request is now rejected."
          },
          "response": []
        },
        {
          "name": "12.4 Seed — create session for programmatic logout test",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.environment.set('prog_logout_session_token', j.sessionToken);",
                  "pm.test('Programmatic logout test session created', function () {",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-1\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            }
          },
          "response": []
        },
        {
          "name": "12.5 POST /auth/logout — programmatic logout (non-browser clients)",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Programmatic logout returns 200', function () {",
                  "    pm.response.to.have.status(200);",
                  "});",
                  "pm.test('loggedOut=true in response', function () {",
                  "    pm.expect(pm.response.json().loggedOut).to.be.true;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{prog_logout_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/auth/logout",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "auth",
                "logout"
              ]
            },
            "description": "**For non-browser clients only** (server-side, CLI, native mobile without WebView).\nSame invalidation as the redirect logout, but via an HTTP POST. The `Clear-Site-Data` header is set in the response, but browsers silently ignore it on cross-site responses — so this variant does NOT reliably clear the hosted-login cookie in browser apps."
          },
          "response": []
        },
        {
          "name": "12.6 Verify — session token dead after programmatic logout",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Session token rejected after programmatic logout (401 or 410)', function () {",
                  "    pm.expect([401, 410]).to.include(pm.response.code);",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{prog_logout_session_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            }
          },
          "response": []
        }
      ],
      "description": "Demonstrates both logout variants:\n\n- **Redirect logout** (`GET /api/v1/auth/logout?...`) — the correct method for browser apps. Performs a top-level navigation so `Clear-Site-Data` fires in a first-party context, clearing the hosted-login cookie.\n- **Programmatic logout** (`POST /api/v1/auth/logout`) — for non-browser clients (server-side, native mobile). Does not reliably clear cross-site cookies.\n\nNeither operation destroys the session or its data. To permanently delete session data, use `POST /api/v1/sessions/revoke`."
    },
    {
      "name": "13 — Session revoke (by session token)",
      "item": [
        {
          "name": "13.1 Seed — create session for revoke test",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.environment.set('revoke_target_token', j.sessionToken);",
                  "pm.test('Revoke-target session created', function () {",
                  "    pm.expect(j.sessionToken).to.be.a('string').and.not.empty;",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{firebase_owner_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n    \"workspaceName\": \"demo-workspace\",\n    \"app_id\": \"app-1\"\n}"
            },
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            }
          },
          "response": []
        },
        {
          "name": "13.2 POST /sessions/revoke — revoke via SESSION JWT",
          "event": [
            {
              "listen": "test",
              "script": {
                "exec": [
                  "var j = pm.response.json();",
                  "pm.test('Session revoked (200)', function () { pm.response.to.have.status(200); });",
                  "pm.test('revoked=true', function () { pm.expect(j.revoked).to.be.true; });",
                  "pm.test('sessionId present', function () { pm.expect(j.sessionId).to.be.a('number'); });"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{revoke_target_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "POST",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions/revoke",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions",
                "revoke"
              ]
            },
            "description": "Revokes the session identified by the Bearer SESSION JWT. Returns `{ revoked: true, sessionId: <n> }`.\n\nAfter revocation: the session row is tombstoned, the payload is nulled, and the token returns 410 permanently. Do NOT use this for normal sign-out — use logout instead."
          },
          "response": []
        },
        {
          "name": "13.3 Verify — token returns 410 after revoke",
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "exec": [
                  "pm.variables.set('skip_200_check', true);"
                ],
                "type": "text/javascript"
              }
            },
            {
              "listen": "test",
              "script": {
                "exec": [
                  "pm.test('Revoked session token returns 410', function () {",
                  "    pm.expect(pm.response.code).to.equal(410);",
                  "});",
                  "pm.test('Error body correct', function () {",
                  "    var body = pm.response.json();",
                  "    pm.expect(body.status).to.equal(410);",
                  "    pm.expect(body.error).to.eql('Session revoked');",
                  "});"
                ],
                "type": "text/javascript"
              }
            }
          ],
          "request": {
            "auth": {
              "type": "bearer",
              "bearer": [
                {
                  "key": "token",
                  "value": "{{revoke_target_token}}",
                  "type": "string"
                }
              ]
            },
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{protocol}}://{{address}}/api/v1/sessions",
              "protocol": "{{protocol}}",
              "host": [
                "{{address}}"
              ],
              "path": [
                "api",
                "v1",
                "sessions"
              ]
            },
            "description": "The 410 response confirms revocation. Importantly: **410 ≠ data loss for unexpired tokens**. However, after an explicit revoke the session data IS permanently destroyed."
          },
          "response": []
        }
      ],
      "description": "Tests `POST /api/v1/sessions/revoke` — the session-token-scoped revoke endpoint. Unlike `POST /sessions/revoke/{sessionId}` (which uses a USER JWT), this endpoint is called with a SESSION JWT and is the correct choice for automation clients that only hold a session token."
    }
  ],
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Global pre-request hook — nothing here by default."
        ]
      }
    },
    {
      "listen": "test",
      "script": {
        "type": "text/javascript",
        "exec": [
          "// Global test hook — runs after every request.",
          "// Asserts HTTP 200 unless skip_200_check is set in the request's pre-request script.",
          "var skipStatusCheck = pm.variables.get('skip_200_check');",
          "if (!skipStatusCheck) {",
          "    pm.test('Response code is 200', function () {",
          "        pm.response.to.have.status(200);",
          "    });",
          "}",
          "// Always unset so it does not bleed into the next request.",
          "pm.variables.unset('skip_200_check');",
          "",
          "// Log every response for easier debugging.",
          "console.log(",
          "    '[' + pm.info.requestName + '] ' +",
          "    pm.response.code + ' ' + pm.response.status",
          ");",
          "if (pm.response.code >= 400) {",
          "    console.log('Response body: ' + pm.response.text());",
          "}"
        ]
      }
    }
  ],
  "variable": [
    {
      "key": "protocol",
      "value": "https",
      "description": "http or https. Override to http for local development."
    },
    {
      "key": "address",
      "value": "www.secure-flows.com",
      "description": "Host (and optional port) of the secureFlows instance. Override for local: localhost:8080"
    }
  ]
}