@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.
@@ -12,6 +12,8 @@
12
12
  */
13
13
  export { customerLogin, customerRegister, customerRequestOTP, customerLoginOTP, customerRefreshToken, customerLoginWithProvider, customerProviderCallback, customerProviderVerifyOTP, getAuthProviders, getAuthConfig, getDefaultAuthProvider, customerSignupContext, customerJoinWaitlist, customerRequestPasswordReset, customerResetPassword, customerUpdatePassword, customerVerifyEmail, currentUser, myProviderToken, customerLogout, deleteMyAccount, myOrganizations, selectOrganization, registerOrganization, saveSession, clearSession, getSessionToken, getSessionCustomer, loginWithPassword, registerWithPassword, loginWithOTP, resetPasswordAndSignIn, refreshSession, refreshSessionOnce, signOut, authedRequest, authedOp, requireAuth, requireAdmin, } from './auth';
14
14
  export type { FoirAuthTokens, FoirCustomer, FoirUser, SessionAdapter, RequireAuthOptions, RequireAdminOptions, } from './auth';
15
+ export { customerAccess, getSessionAccess, hasRole, hasScope, requireRole, requireScope, FoirAccessDeniedError, } from './access';
16
+ export type { FoirAccess, FoirAccessDenial } from './access';
15
17
  export { FoirAuthDowngradedError, FoirSessionExpiredError } from '../graphql/foir-request';
16
18
  export { AUTH_OPS } from './ops';
17
19
  export type { AuthOpName, AuthOpOptions } from './ops';
@@ -212,7 +212,12 @@ export interface MyOrganizationRole {
212
212
  id: string;
213
213
  key: string;
214
214
  name: string;
215
- permissions: string[];
215
+ /**
216
+ * What the role gives, as grants in the permission catalogue's names
217
+ * (`records:read:order`, `members:write`). Each stands for itself: changing
218
+ * a record and creating one are two grants.
219
+ */
220
+ grants: string[];
216
221
  }
217
222
  /**
218
223
  * Where the caller now stands, as `selectMyOrganization` returns it.
@@ -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.
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Whether the surface being served can still answer what a generated module
3
+ * prints.
4
+ *
5
+ * A generated module records the surface version it came from, and that version
6
+ * is a hash over everything `foir types` reads. So it moves when a model gains
7
+ * a field, when a namespace gains an op, and when the platform adds a mutation
8
+ * to every model at once. None of those changes a document this client prints:
9
+ * the document names the fields and arguments the module knew about, and the
10
+ * server still has every one of them. Treating a moved version as a reason to
11
+ * refuse calls made every deployed client stop working the moment the surface
12
+ * grew, with nothing in its own project having changed.
13
+ *
14
+ * So the two are kept apart. A moved version is DRIFT, and it is reported. A
15
+ * call is refused only for an INCOMPATIBILITY: something that call prints, or
16
+ * sends, that the server no longer serves the way the module describes. The
17
+ * client holds both sides of that question once the manifest has arrived, since
18
+ * the generated trees and the served manifest are the same shape, and this file
19
+ * compares them.
20
+ *
21
+ * ---------------------------------------------------------------------------
22
+ * What "the same" means
23
+ * ---------------------------------------------------------------------------
24
+ *
25
+ * What `document.ts` reads, because a property the printer never reads cannot
26
+ * change a document:
27
+ *
28
+ * - an op: its root `field`, whether it is a `mutation`, its `args` and
29
+ * `argTypes`, and its `payload` (`kind`, `model`, `union`, `fields`);
30
+ * - a selection field: its `key`, and which of the printer's branches it
31
+ * takes (a relation, a stopping point, a `model` reference, a `union`
32
+ * reference, an object with `fields`, or a leaf);
33
+ * - a relation an `include` resolves: `target`, `targets` and `connection`,
34
+ * and one property the printer does not read, `list`, because it decides
35
+ * whether the same document answers with one record or an array;
36
+ * - a model's `typeName`, which names the fragment a polymorphic relation
37
+ * prints for it;
38
+ * - a union's members, by `typeName`, and each member's fields.
39
+ *
40
+ * `cost`, `nullable`, `deprecated` and the buckets the generator does not emit
41
+ * are not compared. `alias` is not compared either, and that one is a decision:
42
+ * an alias is a name this client gives a field in its OWN document, so the
43
+ * server never sees the manifest's opinion of it. A new union member whose
44
+ * field collides with an existing one makes the server publish aliases for
45
+ * both, and the module's document, which selects only the members it knew,
46
+ * is as valid as it was.
47
+ *
48
+ * The comparison runs one way. Everything the module names must still be
49
+ * served; whatever the server has added is never looked at, with one
50
+ * exception: an argument the served op REQUIRES and the module does not know,
51
+ * since a call that cannot supply it cannot succeed.
52
+ */
53
+ import { type FoirSelectionSource } from './document';
54
+ /** One thing the generated module describes that the server no longer serves that way. */
55
+ export interface FoirIncompatibility {
56
+ /**
57
+ * Where it sits, in the manifest's own keys:
58
+ * `models.order.selection.status`, `models.order.ops.update`,
59
+ * `namespaces.search.ops.searchRecords.args.query`.
60
+ */
61
+ path: string;
62
+ /** What happened to it, as a clause a message can put in brackets. */
63
+ reason: string;
64
+ }
65
+ /** The comparison, held by a client for as long as it lives. */
66
+ export interface FoirSurfaceComparison {
67
+ /** Every incompatible path, sorted. Empty when the surface has only grown. */
68
+ readonly incompatible: readonly string[];
69
+ /** What `foir.<model>.<op>()` depends on that is incompatible. */
70
+ modelCall(model: string, op: string, include: unknown): readonly FoirIncompatibility[];
71
+ /** What `foir.ns.<namespace>.<op>()` depends on that is incompatible. */
72
+ namespaceCall(namespace: string, op: string, include: unknown): readonly FoirIncompatibility[];
73
+ }
74
+ /**
75
+ * Compare a generated module's trees with the manifest being served.
76
+ *
77
+ * Called once, when the manifest arrives, and off the response path. What it
78
+ * returns answers each later call from maps built here, so a call pays for a
79
+ * lookup and, when it has an `include`, a walk of the relations it names.
80
+ *
81
+ * How far a break reaches is decided per call rather than per model. A call
82
+ * depends on its op, on every tree its result prints (the model's own, the
83
+ * unions inside it, and so on down), and on the relations THIS call includes
84
+ * with the trees of their targets. A model whose relation points at a broken
85
+ * model is not refused for it until a call includes that relation, because
86
+ * until then the broken tree is not in the document. Refusing on the whole
87
+ * relation graph would let one field removed from one model stop every call in
88
+ * a project whose models all reach each other, which is most projects.
89
+ *
90
+ * It is not narrowed by `pick` or `omit`. A call that omits the one field that
91
+ * is gone is still refused: resolving a caller's paths against two trees, with
92
+ * aliases and union members in play, is the printer's work done a second time,
93
+ * and regenerating is the remedy either way.
94
+ */
95
+ export declare function compareFoirSurfaces(generated: FoirSelectionSource, served: FoirSelectionSource): FoirSurfaceComparison;
@@ -51,7 +51,7 @@
51
51
  * inferring a literal would reinstate the defect above under a condition no
52
52
  * caller can see.
53
53
  */
54
- import type { FoirManifestOp, FoirManifestRelation, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionUnion } from './manifest';
54
+ import type { FoirManifestOp, FoirManifestOpPayload, FoirManifestRelation, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionUnion } from './manifest';
55
55
  /**
56
56
  * One model, as the printer needs it.
57
57
  *
@@ -145,6 +145,16 @@ export interface FoirRecordPath {
145
145
  /** Model key whose published `selection` is printed at that position. */
146
146
  modelKey: string;
147
147
  }
148
+ /**
149
+ * Every record position in a payload, in the order the tree declares them.
150
+ *
151
+ * ONE walk, and everything that needs to know where the records are reads it:
152
+ * `planCall` takes the model SET from it to decide where `pick`/`omit`/`include`
153
+ * land, and the plan carries the positions so the transport's caller can find
154
+ * the records in the response. A second walk would be correct on the day it was
155
+ * written and wrong on the day the payload format grew a case.
156
+ */
157
+ export declare function payloadRecordPaths(payload: FoirManifestOpPayload): readonly FoirRecordPath[];
148
158
  /** One printed operation, and what the transport needs to route it. */
149
159
  export interface FoirDocumentPlan {
150
160
  /** The document text. Also the thing that gets hashed for APQ. */
@@ -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