@foir/sdk 0.20.0 → 0.22.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 (65) hide show
  1. package/CHANGELOG.md +135 -1
  2. package/README.md +9 -9
  3. package/dist/app/client.d.ts +90 -84
  4. package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +14 -13
  5. package/dist/app/dispatch.d.ts +5 -5
  6. package/dist/app/errors.d.ts +11 -11
  7. package/dist/app/index.d.ts +4 -4
  8. package/dist/app/naming.d.ts +1 -1
  9. package/dist/app/ticket.d.ts +2 -2
  10. package/dist/app/vocabulary.generated.d.ts +2 -2
  11. package/dist/app-contract.js +3 -3
  12. package/dist/app.js +92 -90
  13. package/dist/auth/access.d.ts +19 -19
  14. package/dist/auth/auth.d.ts +216 -114
  15. package/dist/auth/hosted.d.ts +14 -14
  16. package/dist/auth/index.d.ts +7 -7
  17. package/dist/auth/operations.d.ts +128 -32
  18. package/dist/auth/ops.d.ts +39 -21
  19. package/dist/auth/session-client.d.ts +3 -3
  20. package/dist/auth-session.js +3 -3
  21. package/dist/auth.js +291 -146
  22. package/dist/client/platform-client.d.ts +1 -1
  23. package/dist/client.js +32 -5
  24. package/dist/codegen/index.js +1 -1
  25. package/dist/data/builtins.d.ts +41 -82
  26. package/dist/data/client.d.ts +2 -2
  27. package/dist/data/decimal.d.ts +1 -1
  28. package/dist/data/document.d.ts +14 -0
  29. package/dist/data/index.d.ts +2 -2
  30. package/dist/data/manifest.d.ts +1 -1
  31. package/dist/data/schema.d.ts +1 -1
  32. package/dist/data/transport.d.ts +33 -26
  33. package/dist/data/ws-transport.d.ts +1 -1
  34. package/dist/data.js +97 -19
  35. package/dist/graphql/content-client.d.ts +13 -13
  36. package/dist/index.js +24 -0
  37. package/dist/live/drift.d.ts +1 -1
  38. package/dist/media/screen-capture.d.ts +1 -1
  39. package/dist/next/index.d.ts +2 -2
  40. package/dist/next/session.d.ts +10 -10
  41. package/dist/next.js +121 -61
  42. package/dist/placement/index.d.ts +15 -32
  43. package/dist/placement.js +18 -20
  44. package/dist/push.js +113 -46
  45. package/dist/react/FoirFeedback.d.ts +2 -2
  46. package/dist/react/FoirProvider.d.ts +23 -23
  47. package/dist/react/SignIn.d.ts +4 -4
  48. package/dist/react/UserButton.d.ts +1 -1
  49. package/dist/react/feedback-notes.d.ts +4 -4
  50. package/dist/react/index.d.ts +3 -3
  51. package/dist/react.js +168 -105
  52. package/dist/realtime/index.d.ts +1 -1
  53. package/dist/server/callback-client.d.ts +4 -4
  54. package/dist/server/events-client.d.ts +18 -18
  55. package/dist/server/operation-types.d.ts +8 -8
  56. package/dist/server/require-scoped-token.d.ts +3 -3
  57. package/dist/server/server-client.d.ts +1 -1
  58. package/dist/server/types.d.ts +1 -1
  59. package/dist/server/verify-scoped-token.d.ts +4 -28
  60. package/dist/server.js +67 -112
  61. package/dist/transport/foir-http.d.ts +57 -18
  62. package/dist/transport/key-kind.d.ts +40 -0
  63. package/dist/types/index.d.ts +2 -2
  64. package/package.json +3 -3
  65. package/dist/server/older-claims.generated.d.ts +0 -8
@@ -18,12 +18,12 @@
18
18
  * ```
19
19
  *
20
20
  * ---------------------------------------------------------------------------
21
- * OPERATOR SURFACE. This is not a customer-facing capability.
21
+ * PROJECT SURFACE. Your app's users do not reach it.
22
22
  * ---------------------------------------------------------------------------
23
23
  *
24
24
  * DECIDED 2026-08-04. The event log is reachable by an `sk_` api key
25
- * and by an admin session. It is NOT reachable by a customer token, there is no
26
- * customer lane for it on api-public, and a `pk_` key can never be minted
25
+ * and by a console session. It is NOT reachable by a user token, there is no
26
+ * user lane for it on api-public, and a `pk_` key can never be minted
27
27
  * holding `events:read` at all.
28
28
  *
29
29
  * The reason is a predicate asymmetry rather than a policy preference.
@@ -31,7 +31,7 @@
31
31
  * owner, admin_owner, access_mode, grantee, comp_parent, read_borrow and
32
32
  * slice_parent terms on top of it. A containment-only read over the log
33
33
  * therefore describes rows whose underlying records the same caller could not
34
- * read — and every row carries `actor`, naming which OTHER customer acted. In a
34
+ * read — and every row carries `actor`, naming which OTHER user acted. In a
35
35
  * project with owner confinement that is a per-record activity feed of other
36
36
  * people's actions.
37
37
  *
@@ -51,13 +51,13 @@
51
51
  * built the client.
52
52
  *
53
53
  * 3. **There is no `bearerToken` option, and an `Authorization` header is
54
- * refused.** On api-public a request presenting `sk_` plus a customer
55
- * bearer resolves the CUSTOMER as the authority, and `events` is stripped
56
- * from every customer authority upstream — so attaching a signed-in
57
- * shopper's token to an operator read turns a working call into a 403 that
54
+ * refused.** On api-public a request presenting `sk_` plus a user
55
+ * bearer resolves the USER as the authority, and `events` is stripped
56
+ * from every user authority upstream — so attaching a signed-in
57
+ * shopper's token to a project read turns a working call into a 403 that
58
58
  * reads like a scope problem. Refusing it here says what actually happened.
59
59
  *
60
- * Widening this later is easy. Teaching customers a capability they do not have
60
+ * Widening this later is easy. Teaching users a capability they do not have
61
61
  * is a support burden that cannot be retracted.
62
62
  *
63
63
  * ---------------------------------------------------------------------------
@@ -79,7 +79,7 @@ export declare const FOIR_EVENTS_PATH = "/v1/events";
79
79
  /**
80
80
  * The scope an `sk_` key must hold. Never mintable on a `pk_` — `events` is
81
81
  * absent from the platform's `publicSafeScopeResources` — and never present in a
82
- * customer's resolved authority.
82
+ * user's resolved authority.
83
83
  */
84
84
  export declare const FOIR_EVENTS_SCOPE = "events:read";
85
85
  /**
@@ -91,9 +91,9 @@ export declare const FOIR_EVENTS_SCOPE = "events:read";
91
91
  export declare const FOIR_EVENTS_RETENTION_DAYS = 90;
92
92
  /** Who caused an event. `null` when the emit site attributed nobody. */
93
93
  export interface FoirEventActor {
94
- /** e.g. `"customer"`, `"admin"`, `"api_key"`, `"system"`. */
94
+ /** e.g. `"user"`, `"api_key"`, `"system"`. */
95
95
  kind?: string;
96
- /** e.g. `"customer:cus_123"`. */
96
+ /** e.g. `"user:usr_123"`. */
97
97
  ref?: string;
98
98
  [key: string]: unknown;
99
99
  }
@@ -119,7 +119,7 @@ export interface FoirEvent {
119
119
  * The event's own flat keys, plus `state` and `stateSchema`.
120
120
  *
121
121
  * `state` is the resource AS OF the event — self-contained, not a pointer to
122
- * current state — for the customer, session, operation and workspace-plane
122
+ * current state — for the user, session, operation and workspace-plane
123
123
  * families. It is null for `record.*` events, deliberately and permanently:
124
124
  * `events.view` is plain tenant/project containment while `record.view`
125
125
  * carries owner, grant, borrow and per-field terms, so a copy of record data
@@ -200,15 +200,15 @@ export interface FoirEventsClientConfig {
200
200
  */
201
201
  apiUrl: string;
202
202
  /**
203
- * An `sk_` secret key holding `events:read`.
203
+ * An admin key holding `events:read`.
204
204
  *
205
- * A `pk_` key is refused here rather than at the server — see the operator
206
- * note at the top of this file.
205
+ * A publishable or a server key is refused here rather than at the server:
206
+ * see the note at the top of this file.
207
207
  */
208
208
  apiKey: string;
209
209
  /**
210
- * Extra headers. An `Authorization` header is refused: a customer bearer
211
- * flips api-public's resolved authority to the customer, and no customer
210
+ * Extra headers. An `Authorization` header is refused: a user bearer
211
+ * flips api-public's resolved authority to the user, and no user
212
212
  * authority can contain `events:read`.
213
213
  */
214
214
  headers?: Readonly<Record<string, string>>;
@@ -147,17 +147,17 @@ export interface OperationContext {
147
147
  /** Project ID */
148
148
  projectId: string;
149
149
  /**
150
- * The principal that started the run. Always present, and the empty string
151
- * when the run has none, as a hook's run does.
150
+ * Who started the run. Always present, and the empty string when nobody
151
+ * did, as for a hook's run.
152
152
  */
153
- userId: string;
153
+ actorId: string;
154
154
  /**
155
- * The customer the run acts for, when it acts for one: the signed-in
156
- * customer who ran it, or the owner of the record whose event fired the
157
- * hook. The dispatch token carries the same customer, so a client built from
158
- * it acts for them too.
155
+ * The user the run acts for, when it acts for one: the signed-in user who
156
+ * ran it, or the owner of the record whose event fired the hook. The
157
+ * dispatch token carries the same user, so a client built from it acts for
158
+ * them too.
159
159
  */
160
- customerId?: string;
160
+ userId?: string;
161
161
  /** When the platform built this body, RFC 3339 in UTC to the second. */
162
162
  timestamp: string;
163
163
  }
@@ -120,12 +120,12 @@ export interface ScopedTokenIngress {
120
120
  */
121
121
  orgId: string | null;
122
122
  /**
123
- * The customer the run acts for, when it acts for one: the signed-in
124
- * customer who ran the operation, or the owner of the record whose event
123
+ * The user the run acts for, when it acts for one: the signed-in
124
+ * user who ran the operation, or the owner of the record whose event
125
125
  * fired it. Verified with the rest of the token, so it is the one to trust
126
126
  * over anything in the input.
127
127
  */
128
- customerId?: string;
128
+ userId?: string;
129
129
  /** When the token stops being accepted. There is no refresh: finish, or report, before it. */
130
130
  expiresAt: Date;
131
131
  /** The token's own id (`jti`). The run's id on a dispatch that carries no other. */
@@ -25,7 +25,7 @@ export declare class ServerClientError extends Error {
25
25
  *
26
26
  * const client = createServerClient({
27
27
  * baseUrl: 'https://api.example.com',
28
- * apiKey: 'sk_project_xxx',
28
+ * apiKey: process.env.FOIR_ADMIN_KEY!, // foir_adm_...
29
29
  * });
30
30
  *
31
31
  * const { data } = await client.query('query { models { items { key } } }');
@@ -16,7 +16,7 @@ export interface ServerClientOptions {
16
16
  configKey?: string;
17
17
  }
18
18
  /**
19
- * Where an editor iframe should appear in the admin record editor.
19
+ * Where an editor iframe should appear in the console's record editor.
20
20
  */
21
21
  export interface EditorPlacement {
22
22
  /** 'main-editor' replaces / augments the content tab; 'sidebar' renders in the sidebar. */
@@ -28,8 +28,7 @@ export interface ScopedTokenClaims {
28
28
  * What the token carries, as grants in the permission catalogue's names:
29
29
  * `records:read:<model>`, `records:write:<model>`,
30
30
  * `records:create:<model>`, `users:create` and the rest. It is the whole of
31
- * what the token may do. A token minted before a token said its grants is
32
- * read as what its older claims were worth, as the platform reads it, so
31
+ * what the token may do. A token that does not say its grants is refused, so
33
32
  * this is always set.
34
33
  */
35
34
  grants: Array<string>;
@@ -39,32 +38,9 @@ export interface ScopedTokenClaims {
39
38
  iat: number;
40
39
  ctx?: Record<string, string>;
41
40
  /**
42
- * The same grants in the names a token said them with before it said its
43
- * grants. They never say more than {@link ScopedTokenClaims.grants}, and
44
- * can say less: a grant the older names had no word for is left out.
45
- *
46
- * @deprecated Read {@link ScopedTokenClaims.grants}. The platform stops
47
- * sending this once no token minted before is live.
48
- */
49
- cap?: Array<string>;
50
- /**
51
- * The models the older `records:read` marker in
52
- * {@link ScopedTokenClaims.cap} was for.
53
- *
54
- * @deprecated Read {@link recordsReadModels}.
55
- */
56
- rrm?: Array<string>;
57
- /**
58
- * The models the older `records:write` marker in
59
- * {@link ScopedTokenClaims.cap} was for.
60
- *
61
- * @deprecated Read {@link recordsWriteModels}.
62
- */
63
- rwm?: Array<string>;
64
- /**
65
- * The customer the token acts for, when it acts for one: the signed-in
66
- * customer who ran the operation, or the owner of the record whose event
67
- * fired it. `requireScopedToken` answers it as `customerId`.
41
+ * The user the token acts for, when it acts for one: the signed-in
42
+ * user who ran the operation, or the owner of the record whose event
43
+ * fired it. `requireScopedToken` answers it as `userId`.
68
44
  */
69
45
  cid?: string;
70
46
  }
package/dist/server.js CHANGED
@@ -74,10 +74,10 @@ async function verifyWebhookSignature(payload, signature, secret) {
74
74
 
75
75
  // src/auth/grant-table.generated.ts
76
76
  var GRANT_TABLE = [
77
- ["records:read", true, ["own", "named", "all"], []],
77
+ ["records:read", true, ["own", "all"], []],
78
78
  ["records:create", true, [], []],
79
- ["records:write", true, ["own", "named", "all"], []],
80
- ["records:delete", true, ["own", "named", "all"], []],
79
+ ["records:write", true, ["own", "all"], []],
80
+ ["records:delete", true, ["own", "all"], []],
81
81
  ["records:publish", true, ["own", "all"], []],
82
82
  ["records:attribute", true, [], []],
83
83
  ["search:read", true, [], []],
@@ -93,8 +93,8 @@ var GRANT_TABLE = [
93
93
  ["notifications:consent", false, [], []],
94
94
  ["pushcredentials:read", false, [], []],
95
95
  ["pushcredentials:write", false, [], []],
96
- ["notes:read", false, [], []],
97
- ["notes:write", false, [], []],
96
+ ["notes:read", false, ["own", "all"], []],
97
+ ["notes:write", false, ["own", "all"], []],
98
98
  ["notes:create", false, [], []],
99
99
  ["approvals:read", false, [], []],
100
100
  ["approvals:write", false, [], []],
@@ -151,7 +151,8 @@ var GRANT_TABLE = [
151
151
  ["roles:write", false, [], []],
152
152
  ["projects:read", false, [], []],
153
153
  ["projects:write", false, [], []],
154
- ["project:manage", false, [], ["records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read", "notes:write", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "auth:login_with_foir", "relyingparties:read", "relyingparties:write", "domains:read", "domains:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write"]],
154
+ ["project:stand", false, [], []],
155
+ ["project:manage", false, [], ["project:stand", "records:read.all", "records:create", "records:write.all", "records:delete.all", "records:publish.all", "records:attribute", "search:read", "embeddings:read", "embeddings:write", "schemas:read", "drafts:read", "files:read.all", "files:write.all", "files:delete.all", "users:read.all", "identities:read", "users:create", "identities:delete", "users:write.all", "users:setup", "users:delete.all", "notifications:read.all", "notifications:write.all", "notifications:consent", "pushcredentials:read", "pushcredentials:write", "notes:read.all", "notes:write.all", "notes:create", "approvals:read", "approvals:write", "rooms:read", "rooms:write", "presence:read", "operations:read", "operations:execute", "operations:write", "appconfig:read", "schedules:read", "schedules:write", "hooks:read", "hooks:write", "auth:login_with_foir", "relyingparties:read", "relyingparties:write", "domains:read", "domains:write", "secrets:read", "secrets:list", "secrets:put", "secrets:delete", "appsecrets:read", "appsecrets:list", "appsecrets:put", "appsecrets:delete", "usersecrets:read", "usersecrets:list", "usersecrets:put", "usersecrets:delete", "opsecrets:read", "opsecrets:write", "settings:read", "settings:write", "designtokens:read", "designtokens:write", "models:read", "models:write", "configs:read", "configs:write", "signinproviders:read", "signinproviders:write", "apps:read", "apps:write", "events:read", "targeting:debug", "visitors:forward", "apikeys:read", "apikeys:write", "organizations:read", "organizations:create", "organizations:delete", "organizations:write", "organizations:reach", "members:read", "members:write", "roles:read", "roles:write", "projects:read", "projects:write", "audit:read"]],
155
156
  ["projects:create", false, [], []],
156
157
  ["projects:delete", false, [], []],
157
158
  ["tenants:read", false, [], []],
@@ -164,12 +165,14 @@ var GRANT_TABLE = [
164
165
  ["usage:read", false, [], []],
165
166
  ["controlkeys:read", false, [], []],
166
167
  ["controlkeys:write", false, [], []],
168
+ ["tenantsigninproviders:read", false, [], []],
169
+ ["tenantsigninproviders:write", false, [], []],
167
170
  ["tenanthooks:read", false, [], []],
168
171
  ["tenanthooks:write", false, [], []],
169
172
  ["apps:install", false, [], []],
170
- ["tenant:manage", false, [], ["project:manage", "projects:create", "projects:delete", "tenants:read", "tenants:write", "tenants:delete", "audit:read", "export:run", "billing:read", "billing:manage", "usage:read", "controlkeys:read", "controlkeys:write", "tenanthooks:read", "tenanthooks:write", "apps:install"]],
173
+ ["tenant:manage", false, [], ["project:manage", "projects:create", "projects:delete", "tenants:read", "tenants:write", "tenants:delete", "export:run", "billing:read", "billing:manage", "usage:read", "controlkeys:read", "controlkeys:write", "tenanthooks:read", "tenanthooks:write", "tenantsigninproviders:read", "tenantsigninproviders:write", "apps:install"]],
171
174
  ["platform:read", false, [], []],
172
- ["platform:manage", false, [], ["tenant:manage", "platform:read", "platform:create_workspace", "users:setup"]],
175
+ ["platform:manage", false, [], ["tenant:manage", "platform:read", "platform:create_workspace"]],
173
176
  ["platform:create_workspace", false, [], []],
174
177
  ["users:read", false, ["own", "named", "all"], []],
175
178
  ["users:write", false, ["own", "all"], []],
@@ -231,49 +234,6 @@ function parseGrant(s) {
231
234
  return null;
232
235
  }
233
236
 
234
- // src/server/older-claims.generated.ts
235
- var OLDER_CLAIM_TARGET = "{target}";
236
- var OLDER_CAPABILITY_GRANTS = {
237
- "config:read": ["appconfig:read"],
238
- "customers:read": ["users:read.all"],
239
- "customers:read:identity": ["identities:read"],
240
- "customers:read:named": ["users:read.named"],
241
- "customers:write": ["users:create"],
242
- "embeddings:read": ["embeddings:read"],
243
- "embeddings:write": ["embeddings:write"],
244
- "files:delete": ["files:delete"],
245
- "files:list": ["files:read"],
246
- "files:read": ["files:read"],
247
- "files:write": ["files:write"],
248
- "notifications:consent": ["notifications:consent"],
249
- "notifications:read": ["notifications:read"],
250
- "notifications:write": ["notifications:write.named"],
251
- "operations:complete": ["operations:complete"],
252
- "records:attribute": ["records:attribute"],
253
- "records:attribute:{target}": ["records:attribute:{target}"],
254
- "records:delete": [],
255
- "records:delete:{target}": ["records:delete:{target}"],
256
- "records:publish": [],
257
- "records:publish:{target}": ["records:publish:{target}"],
258
- "records:read": [],
259
- "records:read:{target}": ["records:read:{target}"],
260
- "records:write": [],
261
- "records:write:{target}": ["records:write.all:{target}", "records:create:{target}"],
262
- "secrets:delete.app": ["appsecrets:delete"],
263
- "secrets:delete.customer": ["usersecrets:delete"],
264
- "secrets:delete.project": ["secrets:delete"],
265
- "secrets:list.app": ["appsecrets:list"],
266
- "secrets:list.customer": ["usersecrets:list"],
267
- "secrets:list.project": ["secrets:list"],
268
- "secrets:put.app": ["appsecrets:put"],
269
- "secrets:put.customer": ["usersecrets:put"],
270
- "secrets:put.project": ["secrets:put"],
271
- "secrets:read.app": ["appsecrets:read"],
272
- "secrets:read.customer": ["usersecrets:read"],
273
- "secrets:read.project": ["secrets:read"],
274
- "status:write": ["status:write"]
275
- };
276
-
277
237
  // src/server/verify-scoped-token.ts
278
238
  var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
279
239
  function getCache() {
@@ -349,49 +309,9 @@ function recordsWriteModels(claims) {
349
309
  function recordsCreateModels(claims) {
350
310
  return modelsFor(claims, "records:create");
351
311
  }
352
- var OLDER_RECORD_LANES = [
353
- ["records:read", "rrm"],
354
- ["records:write", "rwm"],
355
- ["records:delete", "rdm"],
356
- ["records:publish", "rpm"]
357
- ];
358
312
  function strings(value) {
359
313
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
360
314
  }
361
- function worthBefore(capability) {
362
- if (Object.hasOwn(OLDER_CAPABILITY_GRANTS, capability)) return OLDER_CAPABILITY_GRANTS[capability] ?? [];
363
- for (const [form, grants] of Object.entries(OLDER_CAPABILITY_GRANTS)) {
364
- if (!form.endsWith(OLDER_CLAIM_TARGET)) continue;
365
- const prefix = form.slice(0, -OLDER_CLAIM_TARGET.length);
366
- if (capability.startsWith(prefix) && capability.length > prefix.length) {
367
- const model = capability.slice(prefix.length);
368
- return grants.map((grant) => grant.replace(OLDER_CLAIM_TARGET, model));
369
- }
370
- }
371
- return [];
372
- }
373
- var OLDER_WRITE = "records:write";
374
- var OLDER_DELETE = "records:delete";
375
- var OLDER_CREATE = "customers:write";
376
- function grantsOfOlderClaims(payload) {
377
- const cap = strings(payload.cap);
378
- const written = new Set(cap.includes(OLDER_WRITE) ? strings(payload.rwm) : []);
379
- const grants = [];
380
- for (const [marker, claim] of OLDER_RECORD_LANES) {
381
- if (!cap.includes(marker)) continue;
382
- for (const model of strings(payload[claim])) {
383
- if (marker !== OLDER_WRITE && written.has(model)) grants.push(`${marker}.all:${model}`);
384
- else grants.push(...worthBefore(`${marker}:${model}`));
385
- }
386
- }
387
- for (const capability of cap) {
388
- if (!OLDER_RECORD_LANES.some(([marker]) => marker === capability)) grants.push(...worthBefore(capability));
389
- }
390
- if (cap.includes(OLDER_CREATE) && cap.includes(OLDER_DELETE) && strings(payload.rdm).length > 0) {
391
- grants.push("identities:delete");
392
- }
393
- return grants;
394
- }
395
315
  async function verifyScopedToken(token, jwksUrl, options) {
396
316
  return verifyWithKeys(token, () => loadJwks(jwksUrl), void 0, options);
397
317
  }
@@ -432,10 +352,7 @@ async function verifyWithKeys(token, loadKeys, refetchForUnknownKid, options) {
432
352
  } catch {
433
353
  throw new Error("unreadable token payload");
434
354
  }
435
- const claims = {
436
- ...payload,
437
- grants: "grants" in payload ? strings(payload.grants) : grantsOfOlderClaims(payload)
438
- };
355
+ const claims = { ...payload, grants: strings(payload.grants) };
439
356
  const now = Math.floor((options?.now?.() ?? Date.now()) / 1e3);
440
357
  if (claims.iss !== expectedIssuer) throw new Error(`bad iss ${claims.iss}`);
441
358
  if (typeof claims.exp !== "number" || claims.exp < now) {
@@ -444,6 +361,7 @@ async function verifyWithKeys(token, loadKeys, refetchForUnknownKid, options) {
444
361
  if (typeof claims.nbf === "number" && claims.nbf > now + 30) {
445
362
  throw new Error("token not yet valid");
446
363
  }
364
+ if (!("grants" in payload)) throw new Error("the token says no grants");
447
365
  return claims;
448
366
  }
449
367
  function parseContextHeader(raw) {
@@ -662,7 +580,7 @@ async function requireScopedToken(request, options) {
662
580
  // header, so the client presents exactly what this guard accepted.
663
581
  dispatch: { token: () => token, context: rawContext },
664
582
  orgId: typeof level === "string" ? level : null,
665
- ...typeof claims.cid === "string" && claims.cid !== "" ? { customerId: claims.cid } : {},
583
+ ...typeof claims.cid === "string" && claims.cid !== "" ? { userId: claims.cid } : {},
666
584
  expiresAt: new Date(claims.exp * 1e3),
667
585
  tokenId: claims.jti
668
586
  };
@@ -724,9 +642,9 @@ async function readDispatch(request) {
724
642
  if (!isObject(context)) refuse(`body.context is ${describe(context)}, expected an object`);
725
643
  requireString(context, "tenantId", "body.context");
726
644
  requireString(context, "projectId", "body.context");
727
- requireString(context, "userId", "body.context", true);
645
+ requireString(context, "actorId", "body.context", true);
728
646
  requireString(context, "timestamp", "body.context");
729
- if (context.customerId !== void 0) requireString(context, "customerId", "body.context");
647
+ if (context.userId !== void 0) requireString(context, "userId", "body.context");
730
648
  const input = body.input;
731
649
  if (input !== null && !isObject(input)) {
732
650
  refuse(`body.input is ${describe(input)}, expected an object`);
@@ -879,6 +797,28 @@ function refuseCredentialHeaders(extra) {
879
797
  }
880
798
  }
881
799
  var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
800
+ function sentCoordinate(value) {
801
+ return Math.round(value * 100) / 100;
802
+ }
803
+ async function resolveFoirVisitor(source) {
804
+ const value = typeof source === "function" ? await source() : source;
805
+ if (value === null || value === void 0) return { place: {} };
806
+ if (typeof value === "string") {
807
+ const address2 = value.trim();
808
+ return { address: address2 || void 0, place: {} };
809
+ }
810
+ const address = value.address?.trim();
811
+ const country = value.country?.trim();
812
+ const place = {};
813
+ if (country) place.country = country;
814
+ if (Number.isFinite(value.latitude) && Number.isFinite(value.longitude)) {
815
+ place.location = {
816
+ latitude: sentCoordinate(value.latitude),
817
+ longitude: sentCoordinate(value.longitude)
818
+ };
819
+ }
820
+ return { address: address || void 0, place };
821
+ }
882
822
  function buildFoirHeaders(input) {
883
823
  const headers = {
884
824
  "Content-Type": "application/json"
@@ -925,8 +865,8 @@ var FoirAuthDowngradedError = class extends FoirRequestError {
925
865
  downgrade;
926
866
  constructor(downgrade) {
927
867
  super(
928
- `[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}), so the request was served as ${downgrade.servedAs}`,
929
- { graphQLErrors: [{ message: "customer token rejected", extensions: { code: "AUTH_DOWNGRADED" } }] }
868
+ `[foir] your user token was ${downgrade.userToken} (${downgrade.reason}), so the request was served as ${downgrade.servedAs}`,
869
+ { graphQLErrors: [{ message: "user token rejected", extensions: { code: "AUTH_DOWNGRADED" } }] }
930
870
  );
931
871
  this.name = "FoirAuthDowngradedError";
932
872
  this.downgrade = downgrade;
@@ -934,7 +874,7 @@ var FoirAuthDowngradedError = class extends FoirRequestError {
934
874
  };
935
875
  var FoirSessionExpiredError = class extends FoirRequestError {
936
876
  constructor(options) {
937
- super("[foir] the customer session has expired and could not be refreshed: sign in again", {
877
+ super("[foir] the user session has expired and could not be refreshed: sign in again", {
938
878
  cause: options?.cause,
939
879
  graphQLErrors: [{ message: "session expired", extensions: { code: "SESSION_EXPIRED" } }]
940
880
  });
@@ -1096,12 +1036,26 @@ async function hmacSha256Hex(secret, body) {
1096
1036
  return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
1097
1037
  }
1098
1038
 
1039
+ // src/transport/key-kind.ts
1040
+ var KEY_PREFIXES = [
1041
+ ["foir_pub_", "publishable"],
1042
+ ["foir_srv_", "server"],
1043
+ ["foir_adm_", "admin"],
1044
+ ["pk_", "publishable"],
1045
+ ["sk_", "older"],
1046
+ ["tc_", "older"]
1047
+ ];
1048
+ function foirKeyKind(apiKey) {
1049
+ if (typeof apiKey !== "string") return void 0;
1050
+ for (const [prefix, kind] of KEY_PREFIXES) {
1051
+ if (apiKey.startsWith(prefix)) return kind;
1052
+ }
1053
+ return void 0;
1054
+ }
1055
+
1099
1056
  // src/data/transport.ts
1100
1057
  async function resolveVisitorAddress(config) {
1101
- const source = config.visitor;
1102
- const value = typeof source === "function" ? await source() : source;
1103
- const address = value?.trim();
1104
- return address ? address : void 0;
1058
+ return (await resolveFoirVisitor(config.visitor)).address;
1105
1059
  }
1106
1060
  async function resolveBearerToken(config) {
1107
1061
  const source = config.bearerToken;
@@ -1180,7 +1134,7 @@ var FoirSocketTransport = class {
1180
1134
  constructor(config) {
1181
1135
  if (config.dispatch !== void 0) {
1182
1136
  throw new FoirRequestError(
1183
- "[foir] a client built from a dispatch token cannot subscribe. The subscription socket authenticates an api key, an admin token or a customer bearer, and has no path for a dispatch token. Queries and mutations made as the operation go to /graphql and work as they are."
1137
+ "[foir] a client built from a dispatch token cannot subscribe. The subscription socket authenticates an api key, a console token or a user bearer, and has no path for a dispatch token. Queries and mutations made as the operation go to /graphql and work as they are."
1184
1138
  );
1185
1139
  }
1186
1140
  this.config = config;
@@ -1538,17 +1492,18 @@ function assertOperatorKey(apiKey) {
1538
1492
  const key = apiKey?.trim() ?? "";
1539
1493
  if (!key) {
1540
1494
  throw new FoirRequestError(
1541
- `[foir] createEventsClient: apiKey is empty \u2014 pass an sk_ secret key holding ${FOIR_EVENTS_SCOPE}.`
1495
+ `[foir] createEventsClient: apiKey is empty. Pass an admin key holding ${FOIR_EVENTS_SCOPE}.`
1542
1496
  );
1543
1497
  }
1544
- if (key.startsWith("pk_")) {
1498
+ const kind = foirKeyKind(key);
1499
+ if (kind === "publishable" || kind === "server") {
1545
1500
  throw new FoirRequestError(
1546
- `[foir] createEventsClient: the event log is an OPERATOR surface and a public (pk_) key can never hold ${FOIR_EVENTS_SCOPE} \u2014 it is not mintable on a public key. Use an sk_ secret key from a server-side runtime; a pk_ key is embeddable in browser bundles, and the log names which customer acted on records the bundle's reader cannot themselves read.`
1501
+ `[foir] createEventsClient: the event log is a project surface and a ${kind} key can never hold ${FOIR_EVENTS_SCOPE}: it is not mintable on an app's key. Use an admin key from a machine of your own. The log names which user acted on records that whoever reads an app's key cannot themselves read.`
1547
1502
  );
1548
1503
  }
1549
- if (!key.startsWith("sk_")) {
1504
+ if (kind === void 0) {
1550
1505
  throw new FoirRequestError(
1551
- `[foir] createEventsClient: apiKey must be an sk_ secret key, got a key starting "${key.slice(0, 4)}". The event log is reachable by an sk_ api key or an admin session only.`
1506
+ `[foir] createEventsClient: apiKey must be an admin key, got a key starting "${key.slice(0, 4)}". The event log is reachable by an admin key or a console session only.`
1552
1507
  );
1553
1508
  }
1554
1509
  return key;
@@ -1558,7 +1513,7 @@ function assertNoBearer(headers) {
1558
1513
  for (const name of Object.keys(headers)) {
1559
1514
  if (name.toLowerCase() === "authorization") {
1560
1515
  throw new FoirRequestError(
1561
- `[foir] createEventsClient: an Authorization header is not accepted. A customer bearer makes the customer the resolved authority on api-public, and no customer authority can contain ${FOIR_EVENTS_SCOPE}. The sk_ key alone is the credential for this surface.`
1516
+ `[foir] createEventsClient: an Authorization header is not accepted. A user bearer makes the user the resolved authority on api-public, and no user authority can contain ${FOIR_EVENTS_SCOPE}. The sk_ key alone is the credential for this surface.`
1562
1517
  );
1563
1518
  }
1564
1519
  }