@foir/sdk 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.
package/dist/server.js CHANGED
@@ -72,6 +72,208 @@ async function verifyWebhookSignature(payload, signature, secret) {
72
72
  return mismatch === 0;
73
73
  }
74
74
 
75
+ // src/auth/grant-table.generated.ts
76
+ var GRANT_TABLE = [
77
+ ["records:read", true, ["own", "named", "all"], []],
78
+ ["records:create", true, [], []],
79
+ ["records:write", true, ["own", "named", "all"], []],
80
+ ["records:delete", true, ["own", "named", "all"], []],
81
+ ["records:publish", true, ["own", "all"], []],
82
+ ["records:attribute", true, [], []],
83
+ ["search:read", true, [], []],
84
+ ["embeddings:read", false, [], []],
85
+ ["embeddings:write", false, [], []],
86
+ ["schemas:read", false, [], []],
87
+ ["drafts:read", false, [], []],
88
+ ["files:read", false, ["own", "all"], []],
89
+ ["files:write", false, ["own", "all"], []],
90
+ ["files:delete", false, ["own", "all"], []],
91
+ ["notifications:read", false, ["own", "all"], []],
92
+ ["notifications:write", false, ["own", "named", "all"], []],
93
+ ["notifications:consent", false, [], []],
94
+ ["pushcredentials:read", false, [], []],
95
+ ["pushcredentials:write", false, [], []],
96
+ ["notes:read", false, [], []],
97
+ ["notes:write", false, [], []],
98
+ ["notes:create", false, [], []],
99
+ ["approvals:read", false, [], []],
100
+ ["approvals:write", false, [], []],
101
+ ["rooms:read", true, [], []],
102
+ ["rooms:write", true, [], []],
103
+ ["presence:read", true, [], []],
104
+ ["operations:read", false, [], []],
105
+ ["operations:execute", true, [], []],
106
+ ["operations:write", false, [], []],
107
+ ["operations:complete", false, [], []],
108
+ ["status:write", false, [], []],
109
+ ["appconfig:read", false, [], []],
110
+ ["schedules:read", false, [], []],
111
+ ["schedules:write", false, [], []],
112
+ ["hooks:read", false, [], []],
113
+ ["hooks:write", false, [], []],
114
+ ["relyingparties:read", false, [], []],
115
+ ["relyingparties:write", false, [], []],
116
+ ["domains:read", false, [], []],
117
+ ["domains:write", false, [], []],
118
+ ["secrets:read", false, [], []],
119
+ ["secrets:list", false, [], []],
120
+ ["secrets:put", false, [], []],
121
+ ["secrets:delete", false, [], []],
122
+ ["appsecrets:read", false, [], []],
123
+ ["appsecrets:list", false, [], []],
124
+ ["appsecrets:put", false, [], []],
125
+ ["appsecrets:delete", false, [], []],
126
+ ["opsecrets:read", false, [], []],
127
+ ["opsecrets:write", false, [], []],
128
+ ["settings:read", false, [], []],
129
+ ["settings:write", false, [], []],
130
+ ["designtokens:read", false, [], []],
131
+ ["designtokens:write", false, [], []],
132
+ ["models:read", false, [], []],
133
+ ["models:write", false, [], []],
134
+ ["configs:read", false, [], []],
135
+ ["configs:write", false, [], []],
136
+ ["apps:read", false, [], []],
137
+ ["apps:write", false, [], []],
138
+ ["events:read", false, [], []],
139
+ ["targeting:debug", false, [], []],
140
+ ["visitors:forward", false, [], []],
141
+ ["apikeys:read", false, [], []],
142
+ ["apikeys:write", false, [], []],
143
+ ["organizations:read", false, [], []],
144
+ ["organizations:write", false, [], []],
145
+ ["organizations:create", false, [], []],
146
+ ["organizations:delete", false, [], []],
147
+ ["organizations:reach", false, [], []],
148
+ ["members:read", false, [], []],
149
+ ["members:write", false, [], []],
150
+ ["roles:read", false, [], []],
151
+ ["roles:write", false, [], []],
152
+ ["projects:read", false, [], []],
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"]],
155
+ ["projects:create", false, [], []],
156
+ ["projects:delete", false, [], []],
157
+ ["tenants:read", false, [], []],
158
+ ["tenants:write", false, [], []],
159
+ ["tenants:delete", false, [], []],
160
+ ["audit:read", false, [], []],
161
+ ["export:run", false, [], []],
162
+ ["billing:read", false, [], []],
163
+ ["billing:manage", false, [], []],
164
+ ["usage:read", false, [], []],
165
+ ["controlkeys:read", false, [], []],
166
+ ["controlkeys:write", false, [], []],
167
+ ["tenanthooks:read", false, [], []],
168
+ ["tenanthooks:write", false, [], []],
169
+ ["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"]],
171
+ ["platform:read", false, [], []],
172
+ ["platform:manage", false, [], ["tenant:manage", "platform:read", "platform:create_workspace", "users:setup"]],
173
+ ["platform:create_workspace", false, [], []],
174
+ ["users:read", false, ["own", "named", "all"], []],
175
+ ["users:write", false, ["own", "all"], []],
176
+ ["users:setup", false, [], []],
177
+ ["users:delete", false, ["own", "all"], []],
178
+ ["users:create", false, [], []],
179
+ ["identities:read", false, [], []],
180
+ ["identities:delete", false, [], []],
181
+ ["usersecrets:read", false, [], []],
182
+ ["usersecrets:list", false, [], []],
183
+ ["usersecrets:put", false, [], []],
184
+ ["usersecrets:delete", false, [], []],
185
+ ["auth:login_with_foir", false, [], []],
186
+ ["signinproviders:read", false, [], []],
187
+ ["signinproviders:write", false, [], []]
188
+ ];
189
+
190
+ // src/auth/grants.ts
191
+ var index;
192
+ function catalogue() {
193
+ if (!index) {
194
+ const byName = new Map(GRANT_TABLE.map((row) => [row[0], row]));
195
+ const longestFirst = [...byName.keys()].sort(
196
+ (a, b) => a.length !== b.length ? b.length - a.length : a < b ? -1 : 1
197
+ );
198
+ index = { byName, longestFirst };
199
+ }
200
+ return index;
201
+ }
202
+ function hasReach(permission, reach) {
203
+ const reaches = permission[2];
204
+ return reaches.length === 0 ? reach === "own" : reaches.includes(reach);
205
+ }
206
+ function parseGrant(s) {
207
+ const { byName, longestFirst } = catalogue();
208
+ for (const name of longestFirst) {
209
+ if (!s.startsWith(name)) continue;
210
+ let rest = s.slice(name.length);
211
+ if (rest !== "" && rest[0] !== "." && rest[0] !== ":") continue;
212
+ const permission = byName.get(name);
213
+ let reach = "own";
214
+ if (rest.startsWith(".")) {
215
+ const after = rest.slice(1);
216
+ const colon = after.indexOf(":");
217
+ const word = colon < 0 ? after : after.slice(0, colon);
218
+ if (word !== "named" && word !== "all") return null;
219
+ if (!hasReach(permission, word)) return null;
220
+ reach = word;
221
+ rest = after.slice(word.length);
222
+ }
223
+ let target = "";
224
+ if (rest.startsWith(":")) {
225
+ target = rest.slice(1);
226
+ if (!permission[1]) return null;
227
+ if (target === "" || /[: \t\n*]/.test(target)) return null;
228
+ }
229
+ return { permission, reach, target };
230
+ }
231
+ return null;
232
+ }
233
+
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
+
75
277
  // src/server/verify-scoped-token.ts
76
278
  var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
77
279
  function getCache() {
@@ -129,11 +331,66 @@ function parseSubject(sub) {
129
331
  }
130
332
  return { tenantId, projectId, appName: sub.slice(second + 1) };
131
333
  }
334
+ function modelsFor(claims, permission) {
335
+ const out = [];
336
+ for (const grant of claims.grants) {
337
+ const parsed = parseGrant(grant);
338
+ if (!parsed || parsed.permission[0] !== permission || parsed.target === "") continue;
339
+ if (!out.includes(parsed.target)) out.push(parsed.target);
340
+ }
341
+ return out;
342
+ }
132
343
  function recordsReadModels(claims) {
133
- return claims.rrm ?? [];
344
+ return modelsFor(claims, "records:read");
134
345
  }
135
346
  function recordsWriteModels(claims) {
136
- return claims.rwm ?? [];
347
+ return modelsFor(claims, "records:write");
348
+ }
349
+ function recordsCreateModels(claims) {
350
+ return modelsFor(claims, "records:create");
351
+ }
352
+ var OLDER_RECORD_LANES = [
353
+ ["records:read", "rrm"],
354
+ ["records:write", "rwm"],
355
+ ["records:delete", "rdm"],
356
+ ["records:publish", "rpm"]
357
+ ];
358
+ function strings(value) {
359
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
360
+ }
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;
137
394
  }
138
395
  async function verifyScopedToken(token, jwksUrl, options) {
139
396
  return verifyWithKeys(token, () => loadJwks(jwksUrl), void 0, options);
@@ -169,12 +426,16 @@ async function verifyWithKeys(token, loadKeys, refetchForUnknownKid, options) {
169
426
  )
170
427
  );
171
428
  if (!ok) throw new Error("signature invalid");
172
- let claims;
429
+ let payload;
173
430
  try {
174
- claims = JSON.parse(new TextDecoder().decode(base64UrlDecode(rawPayload)));
431
+ payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(rawPayload)));
175
432
  } catch {
176
433
  throw new Error("unreadable token payload");
177
434
  }
435
+ const claims = {
436
+ ...payload,
437
+ grants: "grants" in payload ? strings(payload.grants) : grantsOfOlderClaims(payload)
438
+ };
178
439
  const now = Math.floor((options?.now?.() ?? Date.now()) / 1e3);
179
440
  if (claims.iss !== expectedIssuer) throw new Error(`bad iss ${claims.iss}`);
180
441
  if (typeof claims.exp !== "number" || claims.exp < now) {
@@ -1241,7 +1502,7 @@ function describe2(payload) {
1241
1502
 
1242
1503
  // src/server/events-client.ts
1243
1504
  var FOIR_EVENTS_PATH = "/v1/events";
1244
- var FOIR_EVENTS_SCOPE = "events:list.project";
1505
+ var FOIR_EVENTS_SCOPE = "events:read";
1245
1506
  var FOIR_EVENTS_RETENTION_DAYS = 90;
1246
1507
  var FoirEventCursorExpiredError = class extends FoirRequestError {
1247
1508
  /**
@@ -1529,6 +1790,7 @@ export {
1529
1790
  parseContextHeader,
1530
1791
  parseSubject,
1531
1792
  readDispatch,
1793
+ recordsCreateModels,
1532
1794
  recordsReadModels,
1533
1795
  recordsWriteModels,
1534
1796
  requireScopedToken,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foir/sdk",
3
- "version": "0.18.0",
3
+ "version": "0.20.0",
4
4
  "description": "Official Foir SDK — editor iframe client + server-side platform helpers",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -177,18 +177,19 @@
177
177
  "ws": "^8.18.0",
178
178
  "y-websocket": "^3.0.0",
179
179
  "yjs": "^13.6.30",
180
+ "@foir/access-vocab": "0.0.0",
180
181
  "@foir/proto-ts": "0.109.0"
181
182
  },
182
183
  "engines": {
183
184
  "node": ">=18.0.0"
184
185
  },
185
186
  "scripts": {
186
- "build": "node scripts/generate-app-vocabulary.mjs && node scripts/clean-dist.mjs && tsup && tsc -p tsconfig.build.json && node scripts/bundle-app-contract-types.mjs && node scripts/assert-export-types.mjs && node scripts/assert-public-exports.mjs",
187
+ "build": "node scripts/generate-app-vocabulary.mjs && node scripts/generate-grant-table.mjs && node scripts/clean-dist.mjs && tsup && tsc -p tsconfig.build.json && node scripts/bundle-app-contract-types.mjs && node scripts/assert-export-types.mjs && node scripts/assert-public-exports.mjs",
187
188
  "snapshot:exports": "node scripts/assert-public-exports.mjs --update",
188
189
  "dev": "node scripts/dev-watch.mjs",
189
- "check-types": "node scripts/generate-app-vocabulary.mjs && tsc --noEmit",
190
- "test": "node scripts/generate-app-vocabulary.mjs && vitest run",
191
- "test:watch": "node scripts/generate-app-vocabulary.mjs && vitest watch",
190
+ "check-types": "node scripts/generate-app-vocabulary.mjs && node scripts/generate-grant-table.mjs && tsc --noEmit",
191
+ "test": "node scripts/generate-app-vocabulary.mjs && node scripts/generate-grant-table.mjs && vitest run",
192
+ "test:watch": "node scripts/generate-app-vocabulary.mjs && node scripts/generate-grant-table.mjs && vitest watch",
192
193
  "release:patch": "pnpm version patch",
193
194
  "release:minor": "pnpm version minor",
194
195
  "release:major": "pnpm version major"