@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.
- package/CHANGELOG.md +135 -1
- package/README.md +9 -9
- package/dist/app/client.d.ts +90 -84
- package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +14 -13
- package/dist/app/dispatch.d.ts +5 -5
- package/dist/app/errors.d.ts +11 -11
- package/dist/app/index.d.ts +4 -4
- package/dist/app/naming.d.ts +1 -1
- package/dist/app/ticket.d.ts +2 -2
- package/dist/app/vocabulary.generated.d.ts +2 -2
- package/dist/app-contract.js +3 -3
- package/dist/app.js +92 -90
- package/dist/auth/access.d.ts +19 -19
- package/dist/auth/auth.d.ts +216 -114
- package/dist/auth/hosted.d.ts +14 -14
- package/dist/auth/index.d.ts +7 -7
- package/dist/auth/operations.d.ts +128 -32
- package/dist/auth/ops.d.ts +39 -21
- package/dist/auth/session-client.d.ts +3 -3
- package/dist/auth-session.js +3 -3
- package/dist/auth.js +291 -146
- package/dist/client/platform-client.d.ts +1 -1
- package/dist/client.js +32 -5
- package/dist/codegen/index.js +1 -1
- package/dist/data/builtins.d.ts +41 -82
- package/dist/data/client.d.ts +2 -2
- package/dist/data/decimal.d.ts +1 -1
- package/dist/data/document.d.ts +14 -0
- package/dist/data/index.d.ts +2 -2
- package/dist/data/manifest.d.ts +1 -1
- package/dist/data/schema.d.ts +1 -1
- package/dist/data/transport.d.ts +33 -26
- package/dist/data/ws-transport.d.ts +1 -1
- package/dist/data.js +97 -19
- package/dist/graphql/content-client.d.ts +13 -13
- package/dist/index.js +24 -0
- package/dist/live/drift.d.ts +1 -1
- package/dist/media/screen-capture.d.ts +1 -1
- package/dist/next/index.d.ts +2 -2
- package/dist/next/session.d.ts +10 -10
- package/dist/next.js +121 -61
- package/dist/placement/index.d.ts +15 -32
- package/dist/placement.js +18 -20
- package/dist/push.js +113 -46
- package/dist/react/FoirFeedback.d.ts +2 -2
- package/dist/react/FoirProvider.d.ts +23 -23
- package/dist/react/SignIn.d.ts +4 -4
- package/dist/react/UserButton.d.ts +1 -1
- package/dist/react/feedback-notes.d.ts +4 -4
- package/dist/react/index.d.ts +3 -3
- package/dist/react.js +168 -105
- package/dist/realtime/index.d.ts +1 -1
- package/dist/server/callback-client.d.ts +4 -4
- package/dist/server/events-client.d.ts +18 -18
- package/dist/server/operation-types.d.ts +8 -8
- package/dist/server/require-scoped-token.d.ts +3 -3
- package/dist/server/server-client.d.ts +1 -1
- package/dist/server/types.d.ts +1 -1
- package/dist/server/verify-scoped-token.d.ts +4 -28
- package/dist/server.js +67 -112
- package/dist/transport/foir-http.d.ts +57 -18
- package/dist/transport/key-kind.d.ts +40 -0
- package/dist/types/index.d.ts +2 -2
- package/package.json +3 -3
- package/dist/server/older-claims.generated.d.ts +0 -8
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
* ```
|
|
19
19
|
*
|
|
20
20
|
* ---------------------------------------------------------------------------
|
|
21
|
-
*
|
|
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
|
|
26
|
-
*
|
|
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
|
|
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
|
|
55
|
-
* bearer resolves the
|
|
56
|
-
* from every
|
|
57
|
-
* shopper's token to
|
|
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
|
|
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
|
-
*
|
|
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. `"
|
|
94
|
+
/** e.g. `"user"`, `"api_key"`, `"system"`. */
|
|
95
95
|
kind?: string;
|
|
96
|
-
/** e.g. `"
|
|
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
|
|
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
|
|
203
|
+
* An admin key holding `events:read`.
|
|
204
204
|
*
|
|
205
|
-
* A
|
|
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
|
|
211
|
-
* flips api-public's resolved authority to the
|
|
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
|
-
*
|
|
151
|
-
*
|
|
150
|
+
* Who started the run. Always present, and the empty string when nobody
|
|
151
|
+
* did, as for a hook's run.
|
|
152
152
|
*/
|
|
153
|
-
|
|
153
|
+
actorId: string;
|
|
154
154
|
/**
|
|
155
|
-
* The
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
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
|
-
|
|
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
|
|
124
|
-
*
|
|
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
|
-
|
|
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:
|
|
28
|
+
* apiKey: process.env.FOIR_ADMIN_KEY!, // foir_adm_...
|
|
29
29
|
* });
|
|
30
30
|
*
|
|
31
31
|
* const { data } = await client.query('query { models { items { key } } }');
|
package/dist/server/types.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface ServerClientOptions {
|
|
|
16
16
|
configKey?: string;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
-
* Where an editor iframe should appear in the
|
|
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
|
|
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
|
|
43
|
-
*
|
|
44
|
-
*
|
|
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", "
|
|
77
|
+
["records:read", true, ["own", "all"], []],
|
|
78
78
|
["records:create", true, [], []],
|
|
79
|
-
["records:write", true, ["own", "
|
|
80
|
-
["records:delete", true, ["own", "
|
|
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:
|
|
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", "
|
|
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"
|
|
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 !== "" ? {
|
|
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, "
|
|
645
|
+
requireString(context, "actorId", "body.context", true);
|
|
728
646
|
requireString(context, "timestamp", "body.context");
|
|
729
|
-
if (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
|
|
929
|
-
{ graphQLErrors: [{ message: "
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
1495
|
+
`[foir] createEventsClient: apiKey is empty. Pass an admin key holding ${FOIR_EVENTS_SCOPE}.`
|
|
1542
1496
|
);
|
|
1543
1497
|
}
|
|
1544
|
-
|
|
1498
|
+
const kind = foirKeyKind(key);
|
|
1499
|
+
if (kind === "publishable" || kind === "server") {
|
|
1545
1500
|
throw new FoirRequestError(
|
|
1546
|
-
`[foir] createEventsClient: the event log is
|
|
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 (
|
|
1504
|
+
if (kind === void 0) {
|
|
1550
1505
|
throw new FoirRequestError(
|
|
1551
|
-
`[foir] createEventsClient: apiKey must be an
|
|
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
|
|
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
|
}
|