@cboxdk/id-js 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/README.md +121 -4
  3. package/RELEASE-CANDIDATE.md +11 -0
  4. package/dist/api-keys-BE3R3q_A.d.ts +105 -0
  5. package/dist/api-keys-C8-Q4eFA.d.cts +105 -0
  6. package/dist/chunk-EVZ6RB3K.js +98 -0
  7. package/dist/chunk-EVZ6RB3K.js.map +1 -0
  8. package/dist/{chunk-CXGKOJRJ.js → chunk-YGLMLIKL.js} +144 -9
  9. package/dist/chunk-YGLMLIKL.js.map +1 -0
  10. package/dist/{client-BxUHm5hX.d.cts → client-BT-jOm0p.d.cts} +126 -2
  11. package/dist/{client-BSZASrMo.d.ts → client-xlEJalu8.d.ts} +126 -2
  12. package/dist/decisions-C84Fzah4.d.ts +23 -0
  13. package/dist/decisions-DNd9UXwi.d.cts +23 -0
  14. package/dist/index.cjs +338 -28
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +84 -6
  17. package/dist/index.d.ts +84 -6
  18. package/dist/index.js +87 -15
  19. package/dist/index.js.map +1 -1
  20. package/dist/management.cjs +1115 -62
  21. package/dist/management.cjs.map +1 -1
  22. package/dist/management.d.cts +3618 -135
  23. package/dist/management.d.ts +3618 -135
  24. package/dist/management.js +1115 -63
  25. package/dist/management.js.map +1 -1
  26. package/dist/nextjs.cjs +136 -5
  27. package/dist/nextjs.cjs.map +1 -1
  28. package/dist/nextjs.d.cts +4 -3
  29. package/dist/nextjs.d.ts +4 -3
  30. package/dist/nextjs.js +1 -1
  31. package/dist/server.cjs +103 -9
  32. package/dist/server.cjs.map +1 -1
  33. package/dist/server.d.cts +3 -105
  34. package/dist/server.d.ts +3 -105
  35. package/dist/server.js +1 -0
  36. package/dist/{types-BYZldock.d.cts → types-CC1bz68x.d.cts} +30 -1
  37. package/dist/{types-BYZldock.d.ts → types-CC1bz68x.d.ts} +30 -1
  38. package/package.json +8 -3
  39. package/dist/chunk-CXGKOJRJ.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,185 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@cboxdk/id-js` are recorded here. Earlier releases are described
4
+ in their [GitHub releases](https://github.com/cboxdk/id-js/releases).
5
+
6
+ ## [0.20.0] - 2026-11-07
7
+
8
+ ### Upgrade
9
+
10
+ Workspace team.remove requires its body with roles_version. Team writes use roles arrays and current CAS versions. Re-read state on conflicts. Live permission checks bind subject, organization and app; access-token role projections alone do not authorize sensitive actions.
11
+
12
+ ### Added
13
+
14
+ - `checkPermissions()` and `PermissionDecision` for current server authorization, bound to the exact subject, organization and app. Decisions are never cached; malformed, redirected, oversized or unavailable responses fail closed.
15
+ - Updated management clients for multiple app roles, independent role and environment-access versions, scoped invitations and nullable member email.
16
+
17
+ ### Changed
18
+
19
+ - Manifest publication requires a complete acknowledgement for the submitted client, version and checksum, including valid publication counts. Publishing definitions never assigns access.
20
+ - Workspace member removal now requires `roles_version` and removes native ID app roles only. Role edits require `roles` and `roles_version`; environment access uses its separate version.
21
+
22
+ ## [0.19.0] - 2026-10-10
23
+
24
+ Needs laravel-id 1.24 / the Cbox ID release with feature flags, fine-grained
25
+ authorization, Pipes and SMS as a second factor. Against an older instance the new claim
26
+ reads as absent and the new endpoints answer 404.
27
+
28
+ ### Added
29
+
30
+ - Feature flags: `featureFlags(source)` and `hasFeature(source, key)` read the
31
+ `feature_flags` claim from a signed-in user or a verified claim set, and
32
+ `CboxUser.featureFlags` carries it (`null` when the scope was not requested, `[]` when
33
+ nothing is on). `FEATURE_FLAGS_SCOPE` is the scope to request.
34
+ - Management API regenerated from the current specs: `env.featureFlags.*` (including
35
+ `evaluate({ user_id, organization_id })`), `env.fga.*` (check, `checkBatch`, tuples
36
+ write/delete/list, resources and subjects lists, schema get/update/validate, with
37
+ `consistency_token` on every read), `env.pipes.*` (pipe setup, grants, connections),
38
+ `env.radar.*`, `env.signin.sms.*`, the environment sign-in methods (`env.signin.social.*`
39
+ update/enable/disable/inherit/offered, `environment_wide` / `organization_id` on social
40
+ set, the auth policy's method fields), `env.branding.appearance.set()` taking the logo and
41
+ favicon as base64 data URIs, `env.users.mfa.sms.remove()`, the HRIS directory
42
+ routes (`env.directories.*`), and on the account plane `me.mfa.sms.remove()` and
43
+ `me.pipes.disconnect()`. `fgaTuple()` writes a tuple in the notation the batch check takes.
44
+ - Pipes: `VaultClient.leasePipeToken(provider, { userId?, purpose })` leases a fresh token
45
+ for a person's connected account. Refusals are typed: `PipeNotConnectedError` and
46
+ `PipeReauthorizationRequiredError` carry `connectUrl` (and `connectUrlWith({ clientId,
47
+ returnTo })`), `PipeTemporarilyUnavailableError` carries `retryAfter`,
48
+ `PipeLeaseDeniedError` is the 403. `client.pipeConnectUrl(provider, returnTo)` and
49
+ `pipeConnectUrl()` build the hosted connect page.
50
+ - `CboxIdFrontend.config({ organization })`: the sign-in document for one organization (by
51
+ id or slug), whose `social` list is exactly what that organization's hosted page offers.
52
+ Each organization is cached on its own. `FrontendConfig.methods` says whether passkeys and
53
+ magic links are switched on.
54
+ - Embedded sign-in: `mfa_required` lists the `factors` the person can answer with,
55
+ `CboxIdFrontend.sendSmsCode(mfaToken)` texts the code, and `submitSecondFactor()` accepts
56
+ `'sms'`.
57
+
58
+ ### Changed
59
+
60
+ - The generator reads an action's own `202 Accepted` body (documented as `oneOf` it and the
61
+ approval body), so `env.directories.sync()` now returns the directory.
62
+
63
+ ## [0.18.0] - 2026-10-08
64
+
65
+ ### Added
66
+
67
+ - `verifyStandardWebhook()`: verifies deliveries from an endpoint on the `standard_webhooks`
68
+ signature scheme (`webhook-id` / `webhook-timestamp` / `webhook-signature`, any valid `v1`
69
+ signature, 5-minute window). Takes a `whsec_…` secret or an endpoint's 64-hex Cbox secret.
70
+ Tested against the Standard Webhooks specification's vector.
71
+
72
+ - `@cboxdk/id-js/management`: typed clients for Cbox ID's management planes, generated from
73
+ the OpenAPI documents the server publishes. `EnvironmentClient` (an environment's own host,
74
+ `cbid_env_…` key or delegated token), `WorkspaceClient` (`cbid_ws_…` key), `PlatformClient`
75
+ (operator token) and `AccountClient` (a person's own token). Methods are named after the
76
+ server's actions (`env.apps.secrets.rotate(id, body)`, `workspace.environments.create(body)`),
77
+ with typed bodies, queries and responses.
78
+ - Every write sends an `Idempotency-Key` (a fresh UUID unless you pass one) and is retried
79
+ with the same key on network failures, `5xx`, `429` and `409 idempotency_in_progress`,
80
+ respecting `Retry-After`. `replayed` reports `Idempotent-Replayed`.
81
+ - Approvals: a `202 approval_required` calls `onApprovalRequired` (show the binding code),
82
+ polls the approval and repeats the request with `Cbox-Approval` and the same key.
83
+ `ApprovalDeniedError` and `ApprovalExpiredError` when it is not approved;
84
+ `{ approval: 'return' }` hands back the pending approval with a `resume()`.
85
+ - `CboxIdApiError` (`status`, `error`, `message`, `errors`, `requestId` from the envelope's
86
+ `request_id` or `X-Request-Id`, `retryAfter`) and `ManagementNetworkError` (with the
87
+ `idempotencyKey` to repeat safely).
88
+ - `environment` on `EnvironmentClient`: with a person's root access token and the platform
89
+ root as `baseUrl`, it sends `Cbox-Environment` so one token can drive any environment of
90
+ the workspace.
91
+ - Audit Logs helpers: `AuditLogger` (buffers events and sends batches of up to 100, each
92
+ with its own Idempotency-Key, on size, interval and `flush()`), `exportAuditLogs()`
93
+ (creates an export and polls it until ready), and `verifyAuditChain()` /
94
+ `verifyAuditLogChain()` with `canonicalJson()` and `auditEventHash()`, which recompute an
95
+ organization's hash chain exactly as the server does.
96
+ - `…All` async iterators on every paged list (cursor and page-number paging).
97
+ - DPoP-bound access tokens: `createDPoPSigner()` and `generateDPoPKeyPair()` (ES256 on
98
+ WebCrypto, with `DPoP-Nonce` challenge handling).
99
+ - Operation tables (`environmentOperations`, …) with each action's method, path, scope, danger
100
+ (from the spec's `x-scope` / `x-danger`) and whether it can be held for approval.
101
+ - `npm run generate` regenerates the clients from the vendored specs in `openapi/`, and
102
+ `--fetch <plane>=<host>` refreshes a spec from a running server first. The test suite fails
103
+ when the two disagree.
104
+
105
+ ## [0.17.0] - 2026-09-24
106
+
107
+ Organization selection and support sessions. Needs a Cbox ID instance that understands the
108
+ `organization` / `organization_hint` authorize parameters and emits `org_role` and `act`
109
+ (laravel-id 1.19). Against an older instance the new fields stay empty, and a switch fails
110
+ at the callback instead of silently landing in the old organization (see below).
111
+
112
+ ### Added
113
+
114
+ - `createAuthorizationRequest()` accepts `organization` (bind the sign-in to one
115
+ organization), `organizationHint` (preselect it in the hosted picker), and the prompts
116
+ `select_organization` and `create_organization`.
117
+ - `client.switchOrganization(id)` and, on the Next.js adapter,
118
+ `cboxId.switchOrganization(id)`: a new authorization bound to another organization.
119
+ - The organization a sign-in was bound to is echoed on the `AuthorizationRequest` and
120
+ verified at the callback (`stored.organization`): tokens for any other organization are
121
+ refused. The Next.js adapter carries it in a `cbox_id_organization` cookie, and clears
122
+ that cookie on a plain sign-in so an abandoned switch cannot leak its binding into it.
123
+ - `CboxUser` gains typed `organization` (`{ id, name, role }` from `org`, `org_name`,
124
+ `org_role`), `roles`, `permissions` and `actor` (the RFC 8693 `act` claim).
125
+ - Claim helpers that work on a `CboxUser` or on a claim set you verified yourself:
126
+ `organization()`, `actor()`, `isSupportSession()`, `roles()`, `permissions()`,
127
+ `hasRole()`, `hasPermission()`.
128
+ - `client.apiKeysUrl({ clientId?, returnTo?, organization? })` (and `cboxId.apiKeysUrl()`
129
+ on the Next.js adapter): the hosted page where a person creates and revokes API keys for
130
+ your API, `/account/api-keys`, preselected to this app.
131
+ - `ApiKeyVerifier` in a new server-only entry, `@cboxdk/id-js/server`, and
132
+ `cboxId.verifyApiKey(key)` on the Next.js adapter: verifies a customer API key with
133
+ `POST /oauth/api-keys/verify` (HTTP Basic client auth) and returns the typed answer
134
+ `{ active, key_id, sub, org, org_role, permissions, client_id, expires_at }`. It refuses
135
+ an answer for any other `client_id`, treats a key past `expires_at` as inactive, refuses
136
+ to run in a browser, and has an optional cache of active answers (`cacheTtlMs`, at most
137
+ 60 s, never past the key's expiry). Not exported from the main entry.
138
+ - `CboxUser.sessionId` and `sessionId()`: the ID Token's `sid`, the sign-in session a
139
+ back-channel logout token names.
140
+ - Staff roles: `tenantAssignable: false` on a `RoleDefinition` publishes the role as
141
+ staff-only (`tenant_assignable: false`), which Cbox ID never offers or accepts on an
142
+ organization's own admin pages.
143
+ - Self-serve permissions: `tenantAssignable: true` on a `PermissionDefinition` publishes it
144
+ as one an organization's administrators may put in their own custom roles
145
+ (`tenant_assignable: true`). Permissions stay internal by default.
146
+ - Exported wire types `ManifestPermission` and `ManifestRole`.
147
+ - Exported types `AuthorizationPrompt`, `AuthorizationRequestOptions`,
148
+ `OrganizationRole`, `CboxActiveOrganization`, `CboxActor`, `ClaimSource`,
149
+ `CboxOrganization`, and `SignInOptions` from `@cboxdk/id-js/nextjs`.
150
+
151
+ ### Fixed
152
+
153
+ - `CboxIdConfig.accountPath` was documented as defaulting to `/settings`, which on Cbox ID
154
+ is the organization's settings page for its administrators. `profileUrl()` has actually
155
+ defaulted to the person's own account area, `/account`, since 0.8.0; the documentation
156
+ now says so. If you set `accountPath: '/settings'` because of it, remove it: members who
157
+ are not organization admins are redirected away from that page and lose `return_to`.
158
+
159
+ ### Changed
160
+
161
+ - An error returned to the callback (`?error=access_denied`) now sets
162
+ `AuthenticationError.error` and `.errorDescription`, as the back-channel errors already
163
+ did. A refused organization switch is `access_denied`, and an app answers it by switching
164
+ back, not by signing the person out — which it could not tell apart before.
165
+ - `prompt` is typed as `AuthorizationPrompt | AuthorizationPrompt[]` instead of `string`,
166
+ and `prompt: 'none'` combined with another value is refused (OIDC Core §3.1.2.1).
167
+ **Breaking for TypeScript callers that pass a `string` variable**; narrow it to
168
+ `AuthorizationPrompt`.
169
+ - `organization` together with `prompt: 'select_organization'` or `'create_organization'`,
170
+ or an empty `organization` / `organizationHint`, throws `ConfigurationError` before any
171
+ redirect.
172
+ - The manifest `version` now covers both `tenant_assignable` flags, byte-for-byte as the
173
+ PHP reference hashes them: present only in their non-default state, so a catalog that
174
+ never sets them keeps the version it had. The shared cross-SDK fixture gained the
175
+ `staff_role` and `self_serve_permission` cases.
176
+ - `AuthzManifest.permissions` / `.roles` are typed as the wire shapes (`ManifestPermission`,
177
+ `ManifestRole`, snake_case flags) rather than as the declaration types.
178
+ - `defineAuthz()` / `buildManifest()` now refuse a key that is not a lowercase
179
+ `feature:action` slug, and a `tenantAssignable` that is not a boolean, with a
180
+ `ConfigurationError` — both were refused by the server on push anyway.
181
+ - A role that lists the same permission twice is sent and hashed with it once, as the
182
+ server stores it; the repeat used to give a `version` no server computes.
183
+ - `CboxUser` has five new required fields. Code that builds a `CboxUser` by hand (test
184
+ fixtures) must add `organization: null, roles: [], permissions: [], actor: null,
185
+ sessionId: null`.
package/README.md CHANGED
@@ -189,12 +189,37 @@ access token's payload on a resource server:
189
189
  ```ts
190
190
  import { organization, hasPermission, hasRole, isSupportSession } from '@cboxdk/id-js';
191
191
 
192
- if (!hasPermission(payload, 'invoices:create')) return forbidden();
192
+ const showCreateHint = hasPermission(payload, 'invoices:create');
193
193
  if (organization(user)?.role === 'owner') showBilling();
194
194
  ```
195
195
 
196
196
  Matching is exact — `invoices:*` does not grant `invoices:delete`. An `org_role` this SDK
197
197
  version does not recognise reads as `null`, never as a tier it would have to guess.
198
+ These are token snapshots for presentation. Protected operations require the fresh
199
+ `checkPermissions` decision described below; hiding a control is not authorization.
200
+
201
+ ### Feature flags
202
+
203
+ Request the `feature_flags` scope (and give the app that scope on its Scopes tab) and the
204
+ tokens and UserInfo carry the keys of every flag that is on for the person, in the
205
+ organization they signed in to:
206
+
207
+ ```ts
208
+ import { CboxIdClient, FEATURE_FLAGS_SCOPE, hasFeature } from '@cboxdk/id-js';
209
+
210
+ const client = new CboxIdClient({ ...config, scopes: ['openid', 'profile', 'email', FEATURE_FLAGS_SCOPE] });
211
+
212
+ user.featureFlags; // ['acme-beta', 'new-dashboard'] | null
213
+ if (hasFeature(user, 'new-dashboard')) showNewDashboard();
214
+ hasFeature(accessTokenPayload, 'billing.v2'); // works on a verified claim set too
215
+ ```
216
+
217
+ `null` means the claim is absent — the scope was not requested — and `[]` means nothing is
218
+ on. Either way `hasFeature()` is `false`: a missing scope turns every feature off, never on.
219
+ A token carries the flags as they were when it was issued; the next refresh picks up a
220
+ change. To ask without a token in hand (a job, a webhook handler), use
221
+ `env.featureFlags.evaluate({ user_id, organization_id })` from the
222
+ [Management API](#management-api).
198
223
 
199
224
  ### Support sessions
200
225
 
@@ -266,7 +291,10 @@ const frontend = new CboxIdFrontend({
266
291
  })
267
292
 
268
293
  const config = await frontend.config()
269
- // → endpoints, social buttons, and the customer's theme
294
+ // → endpoints, social buttons, which methods are on, and the customer's theme
295
+
296
+ // The buttons one organization's hosted page shows (by id or slug):
297
+ const acme = await frontend.config({ organization: 'acme' })
270
298
 
271
299
  const { user } = await frontend.session(accessToken)
272
300
  // → { id, email, name } or null
@@ -355,6 +383,22 @@ if (result.status === 'mfa_required' || result.status === 'otp_required') {
355
383
  }
356
384
  ```
357
385
 
386
+ `mfa_required` also lists the `factors` the person can answer with — `'totp'`, `'sms'`,
387
+ `'recovery_code'` (kinds only, never the number; absent from instances older than
388
+ laravel-id 1.24). For a texted code, send it when the person asks, then submit it as
389
+ `'sms'`:
390
+
391
+ ```ts
392
+ if (result.status === 'mfa_required' && result.factors?.includes('sms')) {
393
+ // On "Text me a code" — never on page load: each send spends one of the token's five attempts.
394
+ const sent = await frontend.sendSmsCode(result.mfaToken)
395
+ if (sent.status === 'sent') showCodeField(`We texted ${sent.to}`) // '+45 ******78'
396
+ if (sent.status === 'rate_limited') showRetry(sent.retryAfter)
397
+ // later:
398
+ const done = await frontend.submitSecondFactor(result.mfaToken, code, 'sms')
399
+ }
400
+ ```
401
+
358
402
  ### Passkeys
359
403
 
360
404
  ```ts
@@ -546,12 +590,58 @@ const lease = await agent.lease(secret.id, 'call openai');
546
590
 
547
591
  A lease with no live grant is refused — the vault is deny-by-default.
548
592
 
593
+ ### Pipes: a person's own GitHub, Google, Slack… token
594
+
595
+ When a person has connected their account at a provider, lease a fresh access token for it
596
+ with a `vault.lease` token. Cbox ID refreshes it first when it is about to expire:
597
+
598
+ ```ts
599
+ import { PipeNotConnectedError, PipeReauthorizationRequiredError, PipeTemporarilyUnavailableError } from '@cboxdk/id-js';
600
+
601
+ const vault = client.vault(await client.machineToken({ scopes: ['vault.lease'] }));
602
+
603
+ try {
604
+ const { accessToken } = await vault.leasePipeToken('github', { userId, purpose: 'list-repos' });
605
+ // call https://api.github.com/user/repos with it, then drop it
606
+ } catch (e) {
607
+ if (e instanceof PipeNotConnectedError || e instanceof PipeReauthorizationRequiredError) {
608
+ return redirect(e.connectUrlWith({ clientId, returnTo: 'https://app.acme.com/settings' }));
609
+ }
610
+ if (e instanceof PipeTemporarilyUnavailableError) return retryLater(e.retryAfter);
611
+ throw e; // PipeLeaseDeniedError: the app is not granted this pipe
612
+ }
613
+ ```
614
+
615
+ With a token issued for the person (they signed in to your app), leave `userId` out. To
616
+ send somebody to connect before any lease, `client.pipeConnectUrl('github', returnTo)` is
617
+ the hosted connect page, preselected to your app; they come back with
618
+ `?provider=github&status=connected` (or `cancelled`, `failed`).
619
+
549
620
  ## Roles & permissions (federated RBAC)
550
621
 
551
622
  Declare your app's authorization **roles** and **permissions** in code, then push them
552
623
  to Cbox ID on deploy. Your app owns what a role *means*; Cbox ID owns identity and who
553
- *holds* each role — assignments arrive back in the token's `roles` / `permissions`
554
- claims for you to enforce. Requires the app's client to hold the `apps.manifest` scope.
624
+ *holds* each role. Publication requires the client's `apps.manifest` scope and a
625
+ server acknowledgement matching the client, version and full catalog checksum.
626
+ It does not assign roles. Expansions of existing roles await explicit activation.
627
+
628
+ Token role and permission claims are snapshots for presentation. Authorize protected
629
+ server requests and queued work using a fresh decision, then enforce your own row
630
+ ownership/organization rules. Shared organization Admin is not app Admin.
631
+
632
+ ```ts
633
+ import { checkPermissions } from '@cboxdk/id-js/server';
634
+
635
+ const decision = await checkPermissions(
636
+ { issuer, clientId }, accessToken, authenticatedSubject, selectedOrganization,
637
+ ['invoices:create'],
638
+ );
639
+ if (!decision.allowed()) throw new Error('Forbidden');
640
+ ```
641
+
642
+ Derive the subject from verified authentication and authorize the selected organization;
643
+ do not accept either as an unchecked form value. The check has no positive cache,
644
+ refuses redirects and rejects mismatched, incomplete or unavailable decisions.
555
645
 
556
646
  ```ts
557
647
  import { defineAuthz, publishManifest } from '@cboxdk/id-js';
@@ -749,6 +839,33 @@ for await (const org of env.organizations.listAll({ status: 'active' })) {
749
839
  }
750
840
  ```
751
841
 
842
+ ### Fine-grained authorization
843
+
844
+ `env.fga.*` writes relationship tuples and asks checks. Every write answers with a
845
+ `consistency_token`; pass it to a check that must see the write:
846
+
847
+ ```ts
848
+ import { fgaTuple } from '@cboxdk/id-js/management';
849
+
850
+ const { data: written } = await env.fga.tuples.write({
851
+ tuples: [{ resource_type: 'document', resource_id: 'leave', relation: 'viewer', subject: { type: 'user', id: 'alice' } }],
852
+ });
853
+
854
+ const { data } = await env.fga.check({
855
+ resource_type: 'document', resource_id: 'leave', relation: 'viewer',
856
+ subject_type: 'user', subject_id: 'alice',
857
+ consistency_token: written.consistency_token,
858
+ });
859
+
860
+ // Up to 100 at once, in the tuple notation, answered in order:
861
+ await env.fga.checkBatch({ checks: ['document:leave#viewer@user:alice', 'document:readme#editor@user:alice'] });
862
+ ```
863
+
864
+ `env.fga.tuples.delete()`, `env.fga.resources.list()` (which documents can alice view),
865
+ `env.fga.subjects.list()` (who can view this one), and `env.fga.schema.get()` /
866
+ `.update()` / `.validate()` complete it. `fgaTuple()` writes the notation from the same
867
+ object `tuples.write` takes.
868
+
752
869
  ### Audit Logs
753
870
 
754
871
  Your app records what its users did, per organization (your customer), and Cbox ID keeps
@@ -0,0 +1,11 @@
1
+ # id-js 0.20.0
2
+
3
+ Status: prepared local candidate. No public tag, registry publication or deployment has occurred.
4
+
5
+ Add request-bound live permission decisions and catalog checksums; regenerate app catalog, role-definition activation, invitations and native management APIs.
6
+
7
+ ## Upgrade requirements
8
+
9
+ Workspace team.remove requires its body with roles_version. Team writes use roles arrays and current CAS versions. Re-read state on conflicts. Live permission checks bind subject, organization and app; access-token role projections alone do not authorize sensitive actions.
10
+
11
+ Dependency installation against public registries remains contingent on the approved release cascade. Local artifact checks prove the enclosed contents only; they do not claim public indexing.
@@ -0,0 +1,105 @@
1
+ import { O as OrganizationRole } from './types-CC1bz68x.js';
2
+
3
+ /**
4
+ * A customer API key Cbox ID vouches for: live, bound to YOUR app, and held by an active
5
+ * member of `org`.
6
+ *
7
+ * The field names are the endpoint's own, so the claim helpers read it like a token —
8
+ * `hasPermission(answer, 'invoices:create')`, `organization(answer)?.role`. (A type
9
+ * alias rather than an interface for that reason: only an alias is assignable to the
10
+ * helpers' record type.)
11
+ */
12
+ type ActiveApiKey = {
13
+ active: true;
14
+ /** The key's id — log this, never the key. */
15
+ key_id: string;
16
+ /** The person who holds the key (their subject id). */
17
+ sub: string;
18
+ /** The organization the key acts in. */
19
+ org: string;
20
+ /** The holder's membership tier in `org`; null for a tier this SDK does not know. */
21
+ org_role: OrganizationRole | null;
22
+ /**
23
+ * What the key may do, already re-capped by Cbox ID: the key's own list intersected
24
+ * with what the holder holds for your app right now. A demoted holder's key loses the
25
+ * permission on the next verification, not at the next reissue.
26
+ */
27
+ permissions: string[];
28
+ /** Your app's client id — always, or the answer is refused. */
29
+ client_id: string;
30
+ /** ISO 8601 UTC expiry, or null for a key that does not expire. */
31
+ expires_at: string | null;
32
+ };
33
+ /**
34
+ * Every refusal: unknown, malformed, revoked, expired, another app's key, a holder who
35
+ * left. Cbox ID never says which, so the endpoint cannot be used to probe keys — and
36
+ * neither does this.
37
+ */
38
+ interface InactiveApiKey {
39
+ active: false;
40
+ }
41
+ /** What {@link ApiKeyVerifier.verifyApiKey} returns. Narrow on `active`. */
42
+ type ApiKeyVerification = ActiveApiKey | InactiveApiKey;
43
+ /** Configuration for an {@link ApiKeyVerifier}. */
44
+ interface ApiKeyVerifierConfig {
45
+ /** Base URL (issuer) of the Cbox ID instance. */
46
+ issuer: string;
47
+ /** Your app's OAuth client id — the app the keys were issued for. */
48
+ clientId: string;
49
+ /** Your app's client secret. Verification is a confidential-client call. */
50
+ clientSecret: string;
51
+ /** Timeout (ms) for the verification request. Defaults to `10000`. */
52
+ timeoutMs?: number;
53
+ /**
54
+ * Cache an ACTIVE answer for this long (ms), so a burst of requests with one key costs
55
+ * one round-trip. Off (`0`) by default and at most {@link MAX_API_KEY_CACHE_TTL_MS}:
56
+ * while an answer is cached, a revoked key keeps working, and a demoted holder keeps
57
+ * the permissions they lost. Never cached past the key's `expires_at`. Refusals are
58
+ * never cached, so a key issued a moment ago works on its first request.
59
+ */
60
+ cacheTtlMs?: number;
61
+ }
62
+ /** The longest {@link ApiKeyVerifierConfig.cacheTtlMs} allowed: one minute. */
63
+ declare const MAX_API_KEY_CACHE_TTL_MS = 60000;
64
+ /**
65
+ * Verifies customer API keys presented to your API: `POST {issuer}/oauth/api-keys/verify`,
66
+ * authenticated with your app's own client credentials.
67
+ *
68
+ * SERVER-SIDE ONLY. It holds your client secret, so it lives in the
69
+ * `@cboxdk/id-js/server` entry and the Next.js adapter, never in the main entry that a
70
+ * browser bundle pulls in — and it refuses to be constructed in a browser at all.
71
+ *
72
+ * ```ts
73
+ * import { ApiKeyVerifier } from '@cboxdk/id-js/server';
74
+ * import { hasPermission } from '@cboxdk/id-js';
75
+ *
76
+ * const keys = new ApiKeyVerifier({ issuer, clientId, clientSecret, cacheTtlMs: 10_000 });
77
+ *
78
+ * const answer = await keys.verifyApiKey(request.headers.get('x-api-key') ?? '');
79
+ * if (!answer.active) return unauthorized();
80
+ * if (!hasPermission(answer, 'invoices:create')) return forbidden();
81
+ * ```
82
+ */
83
+ declare class ApiKeyVerifier {
84
+ private readonly config;
85
+ private readonly endpoint;
86
+ private readonly timeoutMs;
87
+ private readonly cacheTtlMs;
88
+ private readonly cache;
89
+ constructor(config: ApiKeyVerifierConfig);
90
+ /**
91
+ * Ask Cbox ID whether `key` is good for this app, and what it may do.
92
+ *
93
+ * Resolves `{ active: false }` for every kind of bad key, without a request for an empty
94
+ * one. Throws `AuthenticationError` when the call itself fails (wrong client
95
+ * credentials, an unreachable instance) or when the answer cannot be trusted — one that
96
+ * names another app's `client_id`, or does not have the promised shape. Both mean "do
97
+ * not let this request in"; the difference is that a throw is something to look at.
98
+ */
99
+ verifyApiKey(key: string): Promise<ApiKeyVerification>;
100
+ private request;
101
+ private parse;
102
+ private remember;
103
+ }
104
+
105
+ export { type ActiveApiKey as A, type InactiveApiKey as I, MAX_API_KEY_CACHE_TTL_MS as M, type ApiKeyVerification as a, ApiKeyVerifier as b, type ApiKeyVerifierConfig as c };
@@ -0,0 +1,105 @@
1
+ import { O as OrganizationRole } from './types-CC1bz68x.cjs';
2
+
3
+ /**
4
+ * A customer API key Cbox ID vouches for: live, bound to YOUR app, and held by an active
5
+ * member of `org`.
6
+ *
7
+ * The field names are the endpoint's own, so the claim helpers read it like a token —
8
+ * `hasPermission(answer, 'invoices:create')`, `organization(answer)?.role`. (A type
9
+ * alias rather than an interface for that reason: only an alias is assignable to the
10
+ * helpers' record type.)
11
+ */
12
+ type ActiveApiKey = {
13
+ active: true;
14
+ /** The key's id — log this, never the key. */
15
+ key_id: string;
16
+ /** The person who holds the key (their subject id). */
17
+ sub: string;
18
+ /** The organization the key acts in. */
19
+ org: string;
20
+ /** The holder's membership tier in `org`; null for a tier this SDK does not know. */
21
+ org_role: OrganizationRole | null;
22
+ /**
23
+ * What the key may do, already re-capped by Cbox ID: the key's own list intersected
24
+ * with what the holder holds for your app right now. A demoted holder's key loses the
25
+ * permission on the next verification, not at the next reissue.
26
+ */
27
+ permissions: string[];
28
+ /** Your app's client id — always, or the answer is refused. */
29
+ client_id: string;
30
+ /** ISO 8601 UTC expiry, or null for a key that does not expire. */
31
+ expires_at: string | null;
32
+ };
33
+ /**
34
+ * Every refusal: unknown, malformed, revoked, expired, another app's key, a holder who
35
+ * left. Cbox ID never says which, so the endpoint cannot be used to probe keys — and
36
+ * neither does this.
37
+ */
38
+ interface InactiveApiKey {
39
+ active: false;
40
+ }
41
+ /** What {@link ApiKeyVerifier.verifyApiKey} returns. Narrow on `active`. */
42
+ type ApiKeyVerification = ActiveApiKey | InactiveApiKey;
43
+ /** Configuration for an {@link ApiKeyVerifier}. */
44
+ interface ApiKeyVerifierConfig {
45
+ /** Base URL (issuer) of the Cbox ID instance. */
46
+ issuer: string;
47
+ /** Your app's OAuth client id — the app the keys were issued for. */
48
+ clientId: string;
49
+ /** Your app's client secret. Verification is a confidential-client call. */
50
+ clientSecret: string;
51
+ /** Timeout (ms) for the verification request. Defaults to `10000`. */
52
+ timeoutMs?: number;
53
+ /**
54
+ * Cache an ACTIVE answer for this long (ms), so a burst of requests with one key costs
55
+ * one round-trip. Off (`0`) by default and at most {@link MAX_API_KEY_CACHE_TTL_MS}:
56
+ * while an answer is cached, a revoked key keeps working, and a demoted holder keeps
57
+ * the permissions they lost. Never cached past the key's `expires_at`. Refusals are
58
+ * never cached, so a key issued a moment ago works on its first request.
59
+ */
60
+ cacheTtlMs?: number;
61
+ }
62
+ /** The longest {@link ApiKeyVerifierConfig.cacheTtlMs} allowed: one minute. */
63
+ declare const MAX_API_KEY_CACHE_TTL_MS = 60000;
64
+ /**
65
+ * Verifies customer API keys presented to your API: `POST {issuer}/oauth/api-keys/verify`,
66
+ * authenticated with your app's own client credentials.
67
+ *
68
+ * SERVER-SIDE ONLY. It holds your client secret, so it lives in the
69
+ * `@cboxdk/id-js/server` entry and the Next.js adapter, never in the main entry that a
70
+ * browser bundle pulls in — and it refuses to be constructed in a browser at all.
71
+ *
72
+ * ```ts
73
+ * import { ApiKeyVerifier } from '@cboxdk/id-js/server';
74
+ * import { hasPermission } from '@cboxdk/id-js';
75
+ *
76
+ * const keys = new ApiKeyVerifier({ issuer, clientId, clientSecret, cacheTtlMs: 10_000 });
77
+ *
78
+ * const answer = await keys.verifyApiKey(request.headers.get('x-api-key') ?? '');
79
+ * if (!answer.active) return unauthorized();
80
+ * if (!hasPermission(answer, 'invoices:create')) return forbidden();
81
+ * ```
82
+ */
83
+ declare class ApiKeyVerifier {
84
+ private readonly config;
85
+ private readonly endpoint;
86
+ private readonly timeoutMs;
87
+ private readonly cacheTtlMs;
88
+ private readonly cache;
89
+ constructor(config: ApiKeyVerifierConfig);
90
+ /**
91
+ * Ask Cbox ID whether `key` is good for this app, and what it may do.
92
+ *
93
+ * Resolves `{ active: false }` for every kind of bad key, without a request for an empty
94
+ * one. Throws `AuthenticationError` when the call itself fails (wrong client
95
+ * credentials, an unreachable instance) or when the answer cannot be trusted — one that
96
+ * names another app's `client_id`, or does not have the promised shape. Both mean "do
97
+ * not let this request in"; the difference is that a throw is something to look at.
98
+ */
99
+ verifyApiKey(key: string): Promise<ApiKeyVerification>;
100
+ private request;
101
+ private parse;
102
+ private remember;
103
+ }
104
+
105
+ export { type ActiveApiKey as A, type InactiveApiKey as I, MAX_API_KEY_CACHE_TTL_MS as M, type ApiKeyVerification as a, ApiKeyVerifier as b, type ApiKeyVerifierConfig as c };