@foir/sdk 0.19.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.
@@ -44,6 +44,7 @@ export declare const AUTH_OPS: {
44
44
  readonly customerSignupContext: "customerSignupContext";
45
45
  readonly currentUser: "currentUser";
46
46
  readonly myProviderToken: "myProviderToken";
47
+ readonly whoami: "whoami";
47
48
  readonly customerLogin: "customerLogin";
48
49
  readonly customerRegister: "customerRegister";
49
50
  readonly customerRequestOTP: "customerRequestOTP";
package/dist/auth.js CHANGED
@@ -679,7 +679,7 @@ function toVariableValue(value, label) {
679
679
  return value.toISOString();
680
680
  }
681
681
  if (Array.isArray(value)) {
682
- return value.filter((item) => item !== void 0).map((item, index) => toVariableValue(item, `${label}[${index}]`));
682
+ return value.filter((item) => item !== void 0).map((item, index2) => toVariableValue(item, `${label}[${index2}]`));
683
683
  }
684
684
  if (kind === "object") {
685
685
  const out = {};
@@ -1034,6 +1034,9 @@ var AUTH_OPS = {
1034
1034
  // Post-credential self reads.
1035
1035
  currentUser: "currentUser",
1036
1036
  myProviderToken: "myProviderToken",
1037
+ // What the calling credential may do, and for a signed-in customer the roles
1038
+ // they hold. `./access` reads it.
1039
+ whoami: "whoami",
1037
1040
  // Methods.
1038
1041
  customerLogin: "customerLogin",
1039
1042
  customerRegister: "customerRegister",
@@ -1357,6 +1360,264 @@ async function requireAdmin(adapter, options) {
1357
1360
  return user;
1358
1361
  }
1359
1362
 
1363
+ // src/auth/grant-table.generated.ts
1364
+ var GRANT_TABLE = [
1365
+ ["records:read", true, ["own", "named", "all"], []],
1366
+ ["records:create", true, [], []],
1367
+ ["records:write", true, ["own", "named", "all"], []],
1368
+ ["records:delete", true, ["own", "named", "all"], []],
1369
+ ["records:publish", true, ["own", "all"], []],
1370
+ ["records:attribute", true, [], []],
1371
+ ["search:read", true, [], []],
1372
+ ["embeddings:read", false, [], []],
1373
+ ["embeddings:write", false, [], []],
1374
+ ["schemas:read", false, [], []],
1375
+ ["drafts:read", false, [], []],
1376
+ ["files:read", false, ["own", "all"], []],
1377
+ ["files:write", false, ["own", "all"], []],
1378
+ ["files:delete", false, ["own", "all"], []],
1379
+ ["notifications:read", false, ["own", "all"], []],
1380
+ ["notifications:write", false, ["own", "named", "all"], []],
1381
+ ["notifications:consent", false, [], []],
1382
+ ["pushcredentials:read", false, [], []],
1383
+ ["pushcredentials:write", false, [], []],
1384
+ ["notes:read", false, [], []],
1385
+ ["notes:write", false, [], []],
1386
+ ["notes:create", false, [], []],
1387
+ ["approvals:read", false, [], []],
1388
+ ["approvals:write", false, [], []],
1389
+ ["rooms:read", true, [], []],
1390
+ ["rooms:write", true, [], []],
1391
+ ["presence:read", true, [], []],
1392
+ ["operations:read", false, [], []],
1393
+ ["operations:execute", true, [], []],
1394
+ ["operations:write", false, [], []],
1395
+ ["operations:complete", false, [], []],
1396
+ ["status:write", false, [], []],
1397
+ ["appconfig:read", false, [], []],
1398
+ ["schedules:read", false, [], []],
1399
+ ["schedules:write", false, [], []],
1400
+ ["hooks:read", false, [], []],
1401
+ ["hooks:write", false, [], []],
1402
+ ["relyingparties:read", false, [], []],
1403
+ ["relyingparties:write", false, [], []],
1404
+ ["domains:read", false, [], []],
1405
+ ["domains:write", false, [], []],
1406
+ ["secrets:read", false, [], []],
1407
+ ["secrets:list", false, [], []],
1408
+ ["secrets:put", false, [], []],
1409
+ ["secrets:delete", false, [], []],
1410
+ ["appsecrets:read", false, [], []],
1411
+ ["appsecrets:list", false, [], []],
1412
+ ["appsecrets:put", false, [], []],
1413
+ ["appsecrets:delete", false, [], []],
1414
+ ["opsecrets:read", false, [], []],
1415
+ ["opsecrets:write", false, [], []],
1416
+ ["settings:read", false, [], []],
1417
+ ["settings:write", false, [], []],
1418
+ ["designtokens:read", false, [], []],
1419
+ ["designtokens:write", false, [], []],
1420
+ ["models:read", false, [], []],
1421
+ ["models:write", false, [], []],
1422
+ ["configs:read", false, [], []],
1423
+ ["configs:write", false, [], []],
1424
+ ["apps:read", false, [], []],
1425
+ ["apps:write", false, [], []],
1426
+ ["events:read", false, [], []],
1427
+ ["targeting:debug", false, [], []],
1428
+ ["visitors:forward", false, [], []],
1429
+ ["apikeys:read", false, [], []],
1430
+ ["apikeys:write", false, [], []],
1431
+ ["organizations:read", false, [], []],
1432
+ ["organizations:write", false, [], []],
1433
+ ["organizations:create", false, [], []],
1434
+ ["organizations:delete", false, [], []],
1435
+ ["organizations:reach", false, [], []],
1436
+ ["members:read", false, [], []],
1437
+ ["members:write", false, [], []],
1438
+ ["roles:read", false, [], []],
1439
+ ["roles:write", false, [], []],
1440
+ ["projects:read", false, [], []],
1441
+ ["projects:write", false, [], []],
1442
+ ["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"]],
1443
+ ["projects:create", false, [], []],
1444
+ ["projects:delete", false, [], []],
1445
+ ["tenants:read", false, [], []],
1446
+ ["tenants:write", false, [], []],
1447
+ ["tenants:delete", false, [], []],
1448
+ ["audit:read", false, [], []],
1449
+ ["export:run", false, [], []],
1450
+ ["billing:read", false, [], []],
1451
+ ["billing:manage", false, [], []],
1452
+ ["usage:read", false, [], []],
1453
+ ["controlkeys:read", false, [], []],
1454
+ ["controlkeys:write", false, [], []],
1455
+ ["tenanthooks:read", false, [], []],
1456
+ ["tenanthooks:write", false, [], []],
1457
+ ["apps:install", false, [], []],
1458
+ ["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"]],
1459
+ ["platform:read", false, [], []],
1460
+ ["platform:manage", false, [], ["tenant:manage", "platform:read", "platform:create_workspace", "users:setup"]],
1461
+ ["platform:create_workspace", false, [], []],
1462
+ ["users:read", false, ["own", "named", "all"], []],
1463
+ ["users:write", false, ["own", "all"], []],
1464
+ ["users:setup", false, [], []],
1465
+ ["users:delete", false, ["own", "all"], []],
1466
+ ["users:create", false, [], []],
1467
+ ["identities:read", false, [], []],
1468
+ ["identities:delete", false, [], []],
1469
+ ["usersecrets:read", false, [], []],
1470
+ ["usersecrets:list", false, [], []],
1471
+ ["usersecrets:put", false, [], []],
1472
+ ["usersecrets:delete", false, [], []],
1473
+ ["auth:login_with_foir", false, [], []],
1474
+ ["signinproviders:read", false, [], []],
1475
+ ["signinproviders:write", false, [], []]
1476
+ ];
1477
+
1478
+ // src/auth/grants.ts
1479
+ var WILDCARD = "*";
1480
+ var RESOURCE_WILDCARD = ":*";
1481
+ var index;
1482
+ function catalogue() {
1483
+ if (!index) {
1484
+ const byName = new Map(GRANT_TABLE.map((row) => [row[0], row]));
1485
+ const longestFirst = [...byName.keys()].sort(
1486
+ (a, b) => a.length !== b.length ? b.length - a.length : a < b ? -1 : 1
1487
+ );
1488
+ index = { byName, longestFirst };
1489
+ }
1490
+ return index;
1491
+ }
1492
+ var reachRank = { own: 1, named: 2, all: 3 };
1493
+ function hasReach(permission, reach) {
1494
+ const reaches = permission[2];
1495
+ return reaches.length === 0 ? reach === "own" : reaches.includes(reach);
1496
+ }
1497
+ function resourceOf(name) {
1498
+ const i = name.indexOf(":");
1499
+ return i < 0 ? name : name.slice(0, i);
1500
+ }
1501
+ function parseGrant(s) {
1502
+ const { byName, longestFirst } = catalogue();
1503
+ for (const name of longestFirst) {
1504
+ if (!s.startsWith(name)) continue;
1505
+ let rest = s.slice(name.length);
1506
+ if (rest !== "" && rest[0] !== "." && rest[0] !== ":") continue;
1507
+ const permission = byName.get(name);
1508
+ let reach = "own";
1509
+ if (rest.startsWith(".")) {
1510
+ const after = rest.slice(1);
1511
+ const colon = after.indexOf(":");
1512
+ const word = colon < 0 ? after : after.slice(0, colon);
1513
+ if (word !== "named" && word !== "all") return null;
1514
+ if (!hasReach(permission, word)) return null;
1515
+ reach = word;
1516
+ rest = after.slice(word.length);
1517
+ }
1518
+ let target = "";
1519
+ if (rest.startsWith(":")) {
1520
+ target = rest.slice(1);
1521
+ if (!permission[1]) return null;
1522
+ if (target === "" || /[: \t\n*]/.test(target)) return null;
1523
+ }
1524
+ return { permission, reach, target };
1525
+ }
1526
+ return null;
1527
+ }
1528
+ function covers(held, required) {
1529
+ return held.permission === required.permission && reachRank[held.reach] >= reachRank[required.reach] && (held.target === "" || held.target === required.target);
1530
+ }
1531
+ function resourceWildcard(held) {
1532
+ if (!held.endsWith(RESOURCE_WILDCARD)) return null;
1533
+ const resource = held.slice(0, -RESOURCE_WILDCARD.length);
1534
+ return resource.includes(":") ? null : resource;
1535
+ }
1536
+ function holdsAt(held, required, depth) {
1537
+ for (const h of held) {
1538
+ if (h === WILDCARD) return true;
1539
+ const resource = resourceWildcard(h);
1540
+ if (resource !== null) {
1541
+ if (resource === resourceOf(required.permission[0])) return true;
1542
+ continue;
1543
+ }
1544
+ const grant = parseGrant(h);
1545
+ if (grant === null) continue;
1546
+ if (covers(grant, required)) return true;
1547
+ const implies = grant.permission[3];
1548
+ if (depth < 4 && implies.length > 0 && holdsAt(implies, required, depth + 1)) return true;
1549
+ }
1550
+ return false;
1551
+ }
1552
+ function holds(held, required) {
1553
+ const grant = parseGrant(required);
1554
+ return grant !== null && holdsAt(held, grant, 0);
1555
+ }
1556
+
1557
+ // src/auth/access.ts
1558
+ function accessFrom(who) {
1559
+ const customer = who?.credential?.customer;
1560
+ if (!customer?.id) return null;
1561
+ return {
1562
+ customerId: customer.id,
1563
+ roles: Array.isArray(customer.roles) ? [...customer.roles] : null,
1564
+ scopes: [...who?.authority?.effective ?? []]
1565
+ };
1566
+ }
1567
+ async function customerAccess(env, options) {
1568
+ return accessFrom(await authOp(env, AUTH_OPS.whoami, void 0, options));
1569
+ }
1570
+ async function getSessionAccess(adapter, env) {
1571
+ if (!await getSessionToken(adapter)) return null;
1572
+ return accessFrom(await authedOp(adapter, env, AUTH_OPS.whoami));
1573
+ }
1574
+ function hasRole(access, key) {
1575
+ return access?.roles?.includes(key) ?? false;
1576
+ }
1577
+ function hasScope(access, scope) {
1578
+ return access ? holds(access.scopes, scope) : false;
1579
+ }
1580
+ var FoirAccessDeniedError = class extends Error {
1581
+ code = "FOIR_ACCESS_DENIED";
1582
+ reason;
1583
+ /** The role that was required, when one was. */
1584
+ role;
1585
+ /** The scope that was required, when one was. */
1586
+ scope;
1587
+ constructor(reason, required) {
1588
+ super(denialMessage(reason, required));
1589
+ this.name = "FoirAccessDeniedError";
1590
+ this.reason = reason;
1591
+ this.role = required.role;
1592
+ this.scope = required.scope;
1593
+ }
1594
+ };
1595
+ function denialMessage(reason, required) {
1596
+ switch (reason) {
1597
+ case "signed_out":
1598
+ return "[foir] nobody is signed in";
1599
+ case "role_not_held":
1600
+ return `[foir] the signed-in customer does not hold the role "${required.role}"`;
1601
+ case "roles_unknown":
1602
+ return `[foir] the platform did not say which roles the signed-in customer holds, so "${required.role}" could not be checked`;
1603
+ case "scope_not_held":
1604
+ return `[foir] the signed-in customer's scopes do not cover "${required.scope}"`;
1605
+ }
1606
+ }
1607
+ async function requireRole(adapter, env, key) {
1608
+ const access = await getSessionAccess(adapter, env);
1609
+ if (!access) throw new FoirAccessDeniedError("signed_out", { role: key });
1610
+ if (access.roles === null) throw new FoirAccessDeniedError("roles_unknown", { role: key });
1611
+ if (!access.roles.includes(key)) throw new FoirAccessDeniedError("role_not_held", { role: key });
1612
+ return access;
1613
+ }
1614
+ async function requireScope(adapter, env, scope) {
1615
+ const access = await getSessionAccess(adapter, env);
1616
+ if (!access) throw new FoirAccessDeniedError("signed_out", { scope });
1617
+ if (!holds(access.scopes, scope)) throw new FoirAccessDeniedError("scope_not_held", { scope });
1618
+ return access;
1619
+ }
1620
+
1360
1621
  // src/auth/hosted.ts
1361
1622
  var DEFAULT_COOKIE_NAME = "foir_customer_session";
1362
1623
  var DEFAULT_SESSION_PATH = "/session/token";
@@ -1567,6 +1828,7 @@ function trimSlash(s) {
1567
1828
  }
1568
1829
  export {
1569
1830
  AUTH_OPS,
1831
+ FoirAccessDeniedError,
1570
1832
  FoirAuthDowngradedError,
1571
1833
  FoirSessionExpiredError,
1572
1834
  authedOp,
@@ -1574,6 +1836,7 @@ export {
1574
1836
  clearSession,
1575
1837
  createFoirSessionClient,
1576
1838
  currentUser,
1839
+ customerAccess,
1577
1840
  customerJoinWaitlist,
1578
1841
  customerLogin,
1579
1842
  customerLoginOTP,
@@ -1594,8 +1857,11 @@ export {
1594
1857
  getAuthConfig,
1595
1858
  getAuthProviders,
1596
1859
  getDefaultAuthProvider,
1860
+ getSessionAccess,
1597
1861
  getSessionCustomer,
1598
1862
  getSessionToken,
1863
+ hasRole,
1864
+ hasScope,
1599
1865
  hostedLoginUrl,
1600
1866
  hostedLogoutUrl,
1601
1867
  loginWithOTP,
@@ -1608,6 +1874,8 @@ export {
1608
1874
  registerWithPassword,
1609
1875
  requireAdmin,
1610
1876
  requireAuth,
1877
+ requireRole,
1878
+ requireScope,
1611
1879
  resetPasswordAndSignIn,
1612
1880
  saveSession,
1613
1881
  selectOrganization,
@@ -520,7 +520,7 @@ export interface FoirCustomerNote {
520
520
  * What a note surface has to know about the project before it renders anything
521
521
  * — `foir.ns.notes.noteConfig`.
522
522
  *
523
- * Charged `notes:write` rather than `notes:read`, and refused to a caller with
523
+ * Charged `notes:create` rather than `notes:read`, and refused to a caller with
524
524
  * no customer session, because the only caller it is useful to is one that can
525
525
  * submit. So a composer can read it as its single readiness probe: an answer
526
526
  * means this visitor may write, and a refusal names which half is missing.
@@ -551,7 +551,54 @@ export interface FoirMyNotesArgs {
551
551
  * internal ones.
552
552
  */
553
553
  export type FoirMyNotesResult = FoirConnection<FoirCustomerNote>;
554
- /** `createMyNote` arguments. */
554
+ /**
555
+ * `writeNote` arguments: the one note create, for a signed-in customer and for
556
+ * a visitor alike. It asks `notes:create` of both.
557
+ *
558
+ * A signed-in customer's note is theirs and comes back in `myNotes`. A
559
+ * visitor's, through a key that gives `notes:create` to whoever comes through
560
+ * it, on a project that takes visitors' notes, is nobody's: ask for its `id`
561
+ * and `createdAt` and nothing else, because nothing reads it back.
562
+ *
563
+ * Served by a data API that has the field. `<FoirFeedback>` still writes
564
+ * through `createMyNote`, the same create under its older name, which every
565
+ * data API serves.
566
+ */
567
+ export interface FoirWriteNoteArgs {
568
+ /** A visitor may write about a record and nothing else. */
569
+ subjectType: FoirNoteSubjectType;
570
+ subjectId: string;
571
+ /** Required when `subjectType` is `'record'`, refused otherwise. */
572
+ subjectModelKey?: string;
573
+ body: string;
574
+ /** Reply within an existing thread. Refused where nobody is signed in. */
575
+ parentNoteId?: string;
576
+ /**
577
+ * Files to submit with this note, already uploaded through
578
+ * `createFileUpload` / `confirmFileUpload`. Refused where nobody is signed in.
579
+ *
580
+ * A file this caller cannot read is REFUSED and the note is not written. That
581
+ * is not a rough edge to code around: a create that accepted the note and
582
+ * dropped the attachment would leave the author believing they sent a
583
+ * screenshot nobody has.
584
+ */
585
+ attachmentFileIds?: string[];
586
+ /**
587
+ * Where the operators may reply to a visitor. Refused from a signed-in
588
+ * customer, whose note has an author.
589
+ *
590
+ * Captured as typed and NEVER verified: it is not treated as proof of who
591
+ * wrote the note, and nothing is sent to it to confirm it. Do not build a
592
+ * flow that assumes the address is reachable.
593
+ */
594
+ contactEmail?: string;
595
+ }
596
+ /**
597
+ * `createMyNote` arguments.
598
+ *
599
+ * @deprecated `createMyNote` is the older name of `writeNote` for a signed-in
600
+ * customer. Use `writeNote` and {@link FoirWriteNoteArgs}.
601
+ */
555
602
  export interface FoirCreateMyNoteArgs {
556
603
  subjectType: FoirNoteSubjectType;
557
604
  subjectId: string;
@@ -581,6 +628,10 @@ export interface FoirCreateMyNoteArgs {
581
628
  *
582
629
  * WRITE-ONLY. Nothing reads an anonymous note back on the public plane, which
583
630
  * is why the result is a receipt rather than the note.
631
+ *
632
+ * @deprecated `submitAnonymousNote` is the older name of `writeNote` for a
633
+ * visitor. Use `writeNote` and {@link FoirWriteNoteArgs}, asking for `id` and
634
+ * `createdAt`.
584
635
  */
585
636
  export interface FoirSubmitAnonymousNoteArgs {
586
637
  /**
@@ -606,7 +657,12 @@ export interface FoirSubmitAnonymousNoteArgs {
606
657
  */
607
658
  contactEmail?: string;
608
659
  }
609
- /** What `submitAnonymousNote` returns. */
660
+ /**
661
+ * What `submitAnonymousNote` returns.
662
+ *
663
+ * @deprecated With the field it answers. `writeNote` answers a visitor with
664
+ * the note's `id` and `createdAt`.
665
+ */
610
666
  export interface FoirAnonymousNoteReceipt {
611
667
  /**
612
668
  * A reference for this submission, to quote when following it up.
@@ -59,7 +59,7 @@ export { planFoirModelCall, planFoirNamespaceCall } from './document';
59
59
  export type { FoirCall, FoirDocumentPlan, FoirNamespaceCall, FoirNamespaceSelections, FoirRecordPath, FoirSelectionModel, FoirSelectionNamespace, FoirSelections, FoirSelectionSource, FoirSelectionUnions, } from './document';
60
60
  export { createFoirSubjectRegistry, recordFoirSubjects } from './subjects';
61
61
  export type { FoirSubject, FoirSubjectEntry, FoirSubjectOptions, FoirSubjectOrigin, FoirSubjectRegistry, } from './subjects';
62
- export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirSubmitAnonymousNoteArgs, FoirAnonymousNoteReceipt, FoirCustomerNote, FoirCustomerNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirNoteConfig, FoirNoteSubjectType, FoirPlatformType, FoirRecordOwner, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Passage, Prose, ProseSpan, RecordAncestor, Reference, Richtext, Video, } from './builtins';
62
+ export type { CollabInfo, FileValue, FoirAccessGrant, FoirAccessLevel, FoirAccessSource, FoirAccessor, FoirCreateMyNoteArgs, FoirWriteNoteArgs, FoirSubmitAnonymousNoteArgs, FoirAnonymousNoteReceipt, FoirCustomerNote, FoirCustomerNoteAttachment, FoirDegraded, FoirDepthBelow, FoirDepthLevel, FoirDeviceType, FoirGranteeKind, FoirJsonValue, FoirMyAccess, FoirMyNotesArgs, FoirMyNotesResult, FoirNoteConfig, FoirNoteSubjectType, FoirPlatformType, FoirRecordOwner, FoirResolveContext, FoirResourceAccess, FoirUnexpandedRecord, FoirUpdateManyPayload, FoirVisibility, GeoPoint, Image, ImageFormat, ImageVariant, ImageVariantSet, MediaValue, Passage, Prose, ProseSpan, RecordAncestor, Reference, Richtext, Video, } from './builtins';
63
63
  /**
64
64
  * Pre-rename spellings. The `<Name>Block` wrappers and the `<Name>Value`
65
65
  * split are the same thing as the types above — an item IS its value — so each
@@ -155,10 +155,10 @@ export type FoirOperationGeneratedModule<S extends FoirSchemaDefinition = FoirSc
155
155
  * const foir = createFoirClient({ apiUrl, dispatch, generated });
156
156
  * ```
157
157
  *
158
- * The endpoint calls Foir AS the operation, with the capabilities the operation
158
+ * The endpoint calls Foir AS the operation, with the grants the operation
159
159
  * declared, and holds no key of its own. api-public accepts the token as a
160
160
  * caller in its own right: it builds the schema overlay from the token's
161
- * capabilities, and it reads the organization and the customer from the
161
+ * grants, and it reads the organization and the customer from the
162
162
  * token's claims. So nothing a key client configures beside its key
163
163
  * (`bearerToken`, `orgId`, `channel`) means anything here, and none of it is
164
164
  * accepted.
package/dist/next.js CHANGED
@@ -921,6 +921,9 @@ var AUTH_OPS = {
921
921
  // Post-credential self reads.
922
922
  currentUser: "currentUser",
923
923
  myProviderToken: "myProviderToken",
924
+ // What the calling credential may do, and for a signed-in customer the roles
925
+ // they hold. `./access` reads it.
926
+ whoami: "whoami",
924
927
  // Methods.
925
928
  customerLogin: "customerLogin",
926
929
  customerRegister: "customerRegister",
@@ -5,8 +5,9 @@
5
5
  * reaches a cross-site frame, so the console hands the page a short-lived token
6
6
  * instead, by message, to your placement's origin only. The token is the
7
7
  * operator looking at the page, acting through your app at the level your app
8
- * is installed at, carrying the scopes your placement declares that this
9
- * operator holds. Your page runs no login of its own.
8
+ * is installed at, carrying what your placement declares and this operator
9
+ * holds, as grants in the permission catalogue's names. Your page runs no
10
+ * login of its own.
10
11
  *
11
12
  * ```ts
12
13
  * import { connectPlacement, FOIR_CONSOLE_ORIGINS } from '@foir/sdk/placement';
@@ -15,7 +16,7 @@
15
16
  * apiUrl: 'https://api.foir.dev',
16
17
  * consoleOrigins: FOIR_CONSOLE_ORIGINS.production,
17
18
  * });
18
- * if (session.can('records:write:preorder')) showEditButtons();
19
+ * if (session.can('records:write.all:preorder')) showEditButtons();
19
20
  * const data = await session.request('{ preorders { items { id } } }');
20
21
  * ```
21
22
  *
@@ -52,14 +53,38 @@ export interface PlacementGrant {
52
53
  expiresAt: string;
53
54
  /** The organisation the page acts at: your app's install. Empty for the project. */
54
55
  orgId: string;
55
- /** What the token carries. */
56
+ /**
57
+ * What the token carries: the grants your placement declares that this
58
+ * operator holds, in the permission catalogue's names. A grant the operator
59
+ * holds part of is carried as that part, so ask with `can` and not by
60
+ * looking for a spelling here.
61
+ */
62
+ grants: string[];
63
+ /** Each grant your placement declares that the token does not carry whole, and why. */
64
+ withheldGrants: {
65
+ grant: string;
66
+ reason: string;
67
+ }[];
68
+ /**
69
+ * @deprecated Read `grants`. The same list under the name it had before a
70
+ * placement declared grants.
71
+ */
56
72
  grantedScopes: string[];
57
- /** What your placement declares that this operator does not hold, and why. */
73
+ /**
74
+ * @deprecated Read `withheldGrants`. The same list under the name it had
75
+ * before a placement declared grants.
76
+ */
58
77
  withheld: {
59
78
  scope: string;
60
79
  reason: string;
61
80
  }[];
62
81
  }
82
+ /**
83
+ * What a console sends with a token. A console of the release before sends
84
+ * only `grantedScopes` and `withheld`, and the session reads `grants` from
85
+ * those.
86
+ */
87
+ type PlacementGrantMessage = Pick<PlacementGrant, 'accessToken' | 'expiresAt' | 'orgId'> & Partial<Pick<PlacementGrant, 'grants' | 'withheldGrants' | 'grantedScopes' | 'withheld'>>;
63
88
  /** How the console looks, for an embedded page to match. */
64
89
  export interface PlacementAppearance {
65
90
  /**
@@ -71,7 +96,7 @@ export interface PlacementAppearance {
71
96
  /** A message the console sends an embedded page. */
72
97
  export type PlacementHostMessage = {
73
98
  type: typeof PLACEMENT_MESSAGE.token;
74
- grant: PlacementGrant;
99
+ grant: PlacementGrantMessage;
75
100
  } | {
76
101
  type: typeof PLACEMENT_MESSAGE.refused;
77
102
  reason: string;
@@ -138,8 +163,14 @@ export interface PlacementSession {
138
163
  * older than this). Kept current as the operator changes it.
139
164
  */
140
165
  readonly appearance: PlacementAppearance | null;
141
- /** Whether the token carries this scope, to hide or grey out what the operator cannot do. */
142
- can(scope: string): boolean;
166
+ /**
167
+ * Whether the token covers this grant, to hide or grey out what the operator
168
+ * cannot do. It answers as Foir's API would: a grant for every model covers
169
+ * the same grant for one, and one for every owner's rows covers a person's
170
+ * own. `can('records:read.all:order')` is true for a token that carries
171
+ * `records:read.all`.
172
+ */
173
+ can(grant: string): boolean;
143
174
  /** A GraphQL request to Foir's API as the operator, through your app. */
144
175
  request<TResult = unknown>(document: string | {
145
176
  toString(): string;
@@ -156,3 +187,4 @@ export interface PlacementSession {
156
187
  * console gives no token, or when none arrives in time.
157
188
  */
158
189
  export declare function connectPlacement(options: ConnectPlacementOptions): Promise<PlacementSession>;
190
+ export {};