@foir/sdk 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +330 -0
  2. package/dist/app/client.d.ts +109 -21
  3. package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +223 -211
  4. package/dist/app/errors.d.ts +2 -2
  5. package/dist/app/index.d.ts +4 -4
  6. package/dist/app/manifest.d.ts +1 -1
  7. package/dist/app/vocabulary.generated.d.ts +13 -10
  8. package/dist/app-contract.js +27 -31
  9. package/dist/app.js +80 -23
  10. package/dist/auth/access.d.ts +106 -0
  11. package/dist/auth/grant-table.generated.d.ts +13 -0
  12. package/dist/auth/grants.d.ts +38 -0
  13. package/dist/auth/index.d.ts +2 -0
  14. package/dist/auth/operations.d.ts +6 -1
  15. package/dist/auth/ops.d.ts +1 -0
  16. package/dist/auth.js +321 -7
  17. package/dist/client/platform-client.d.ts +1 -1
  18. package/dist/client.js +28 -1
  19. package/dist/codegen/index.js +1 -1
  20. package/dist/data/builtins.d.ts +57 -42
  21. package/dist/data/document.d.ts +14 -0
  22. package/dist/data/index.d.ts +2 -2
  23. package/dist/data/transport.d.ts +27 -20
  24. package/dist/data.js +91 -13
  25. package/dist/index.js +24 -0
  26. package/dist/next.js +47 -5
  27. package/dist/placement/index.d.ts +26 -11
  28. package/dist/placement.js +219 -2
  29. package/dist/push.js +55 -6
  30. package/dist/react/FoirProvider.d.ts +40 -1
  31. package/dist/react/feedback-notes.d.ts +1 -1
  32. package/dist/react/index.d.ts +5 -4
  33. package/dist/react.js +391 -8
  34. package/dist/server/events-client.d.ts +7 -7
  35. package/dist/server/index.d.ts +1 -1
  36. package/dist/server/require-scoped-token.d.ts +1 -1
  37. package/dist/server/server-client.d.ts +1 -1
  38. package/dist/server/verify-scoped-token.d.ts +18 -23
  39. package/dist/server.js +228 -14
  40. package/dist/transport/foir-http.d.ts +44 -5
  41. package/dist/transport/key-kind.d.ts +40 -0
  42. package/package.json +6 -5
@@ -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, 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
@@ -82,4 +82,4 @@ export type { FoirInclude, FoirModelOperation, FoirNamespaceOperation, FoirOmitt
82
82
  export { FoirDecimal, decimal } from './decimal';
83
83
  export type { FoirDecimalDivOptions, FoirDecimalInput, FoirDecimalRounding, } from './decimal';
84
84
  export { FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, FOIR_VISITOR_IP_HEADER, } from '../transport/foir-http';
85
- export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, FoirVisitorSource, } from '../transport/foir-http';
85
+ export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, FoirVisitor, FoirVisitorSource, } from '../transport/foir-http';
@@ -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.
@@ -200,9 +200,9 @@ export interface FoirKeyClientConfig<S extends FoirSchemaDefinition = FoirSchema
200
200
  dispatch?: never;
201
201
  /**
202
202
  * Read the draft schema channel (`X-Foir-Schema-Channel: draft`). Needs a key
203
- * holding `drafts:read`: a preview environment's own `pk_`, or an `sk_`. A
204
- * public key without it is served the published copy; a secret key without
205
- * it is refused. Defaults to `'published'`.
203
+ * holding `drafts:read`: a preview environment's own key. A publishable or
204
+ * a server key without it is served the published copy. Defaults to
205
+ * `'published'`.
206
206
  */
207
207
  channel?: 'published' | 'draft';
208
208
  /**
@@ -259,21 +259,28 @@ export interface FoirKeyClientConfig<S extends FoirSchemaDefinition = FoirSchema
259
259
  */
260
260
  orgId?: string;
261
261
  /**
262
- * The address of the visitor a server rendering pages is calling for, or a
263
- * function resolving it for each request, sent as `X-Foir-Visitor-IP`. The
264
- * public API's per-visitor limit then applies to that visitor rather than to
265
- * your server, which otherwise counts as one visitor for every page it
266
- * renders.
267
- *
268
- * Give it the address your host guarantees: the one its edge sets and a
269
- * visitor cannot. A header a visitor can write lets them name a fresh address
270
- * per request, which is exactly what the limit exists to stop. The docs list
271
- * the value for each common host.
272
- *
273
- * A secret key only, and one holding `visitors:forward`: the API refuses the
274
- * header, naming the permission, from any other. A client built from a public
275
- * key is refused `visitor` here, because a browser already calls as its own
276
- * visitor and a public key cannot hold the permission.
262
+ * The visitor a server rendering pages is calling for, or a function
263
+ * resolving them for each request: their address, and where they are.
264
+ *
265
+ * The address is sent as `X-Foir-Visitor-IP`. The public API's per-visitor
266
+ * limit then applies to that visitor rather than to your server, which
267
+ * otherwise counts as one visitor for every page it renders. A string is the
268
+ * address alone.
269
+ *
270
+ * The country and the coordinates are attached to every read, in its
271
+ * `contexts`, so that location targeting is decided by where the visitor is.
272
+ * Through a key holding `visitors:forward` the API never uses where the
273
+ * caller is: a read that says no place is a visitor nobody could place.
274
+ *
275
+ * Give it what your host guarantees: the values its edge sets and a visitor
276
+ * cannot. A header a visitor can write lets them name a fresh address per
277
+ * request, which is exactly what the limit exists to stop. The docs list
278
+ * the values for each common host.
279
+ *
280
+ * A server key only, and one holding `visitors:forward`: the API refuses the
281
+ * header, naming the permission, from any other. A client built from a
282
+ * publishable key is refused `visitor` here, because a browser already calls
283
+ * as its own visitor and a publishable key cannot hold the permission.
277
284
  */
278
285
  visitor?: FoirVisitorSource;
279
286
  /**
package/dist/data.js CHANGED
@@ -98,6 +98,28 @@ function refuseCredentialHeaders(extra) {
98
98
  }
99
99
  }
100
100
  var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
101
+ function sentCoordinate(value) {
102
+ return Math.round(value * 100) / 100;
103
+ }
104
+ async function resolveFoirVisitor(source) {
105
+ const value = typeof source === "function" ? await source() : source;
106
+ if (value === null || value === void 0) return { place: {} };
107
+ if (typeof value === "string") {
108
+ const address2 = value.trim();
109
+ return { address: address2 || void 0, place: {} };
110
+ }
111
+ const address = value.address?.trim();
112
+ const country = value.country?.trim();
113
+ const place = {};
114
+ if (country) place.country = country;
115
+ if (Number.isFinite(value.latitude) && Number.isFinite(value.longitude)) {
116
+ place.location = {
117
+ latitude: sentCoordinate(value.latitude),
118
+ longitude: sentCoordinate(value.longitude)
119
+ };
120
+ }
121
+ return { address: address || void 0, place };
122
+ }
101
123
  function buildFoirHeaders(input) {
102
124
  const headers = {
103
125
  "Content-Type": "application/json"
@@ -214,6 +236,30 @@ function readFoirEnvelope(json, options) {
214
236
  return json.data;
215
237
  }
216
238
 
239
+ // src/transport/key-kind.ts
240
+ var KEY_PREFIXES = [
241
+ ["foir_pub_", "publishable"],
242
+ ["foir_srv_", "server"],
243
+ ["foir_adm_", "admin"],
244
+ ["pk_", "publishable"],
245
+ ["sk_", "older"],
246
+ ["tc_", "older"]
247
+ ];
248
+ var FOIR_CREDENTIALS_DOC = "https://docs.foir.dev/credentials";
249
+ function foirKeyKind(apiKey) {
250
+ if (typeof apiKey !== "string") return void 0;
251
+ for (const [prefix, kind] of KEY_PREFIXES) {
252
+ if (apiKey.startsWith(prefix)) return kind;
253
+ }
254
+ return void 0;
255
+ }
256
+ function adminKeyRefusal(client) {
257
+ return `[foir] ${client} was given an admin key. An admin key is a machine's own: nobody signs in through it, and an app never holds one. Build this client from your publishable key, or from your server key on the server that renders your pages. A machine that acts for itself uses createServerClient from @foir/sdk/server. See ${FOIR_CREDENTIALS_DOC}`;
258
+ }
259
+ function isPublishableFoirKey(apiKey) {
260
+ return foirKeyKind(apiKey) === "publishable";
261
+ }
262
+
217
263
  // src/transport/persisted-query.ts
218
264
  var FOIR_CDN_GRAPHQL_PATH = "/cdn/graphql";
219
265
  function persistedQueryExtension(hash) {
@@ -257,10 +303,7 @@ function foirDataEndpoint(apiUrl, path) {
257
303
  return foirApiEndpoint(apiUrl, path);
258
304
  }
259
305
  async function resolveVisitorAddress(config) {
260
- const source = config.visitor;
261
- const value = typeof source === "function" ? await source() : source;
262
- const address = value?.trim();
263
- return address ? address : void 0;
306
+ return (await resolveFoirVisitor(config.visitor)).address;
264
307
  }
265
308
  async function resolveBearerToken(config) {
266
309
  const source = config.bearerToken;
@@ -309,9 +352,13 @@ function assertFoirCredential(config) {
309
352
  "[foir] createFoirClient needs both apiUrl and apiKey (or, inside an operation endpoint, apiUrl and dispatch)"
310
353
  );
311
354
  }
312
- if (loose.visitor !== void 0 && !loose.apiKey.startsWith("sk_")) {
355
+ const kind = foirKeyKind(loose.apiKey);
356
+ if (kind === "admin") {
357
+ throw new FoirRequestError(adminKeyRefusal("createFoirClient"));
358
+ }
359
+ if (loose.visitor !== void 0 && kind !== "server" && kind !== "older") {
313
360
  throw new FoirRequestError(
314
- "[foir] createFoirClient: `visitor` names the visitor a server calls for, and only a secret key holding visitors:forward may send it. A public key already calls as its visitor, and cannot hold the permission. Drop `visitor`, or build this client from your server's secret key."
361
+ `[foir] createFoirClient: \`visitor\` names the visitor a server calls for, and only a server key holding visitors:forward may send it. A publishable key already calls as its visitor, and cannot hold the permission. Drop \`visitor\`, or build this client from your server key on the server that renders your pages. See ${FOIR_CREDENTIALS_DOC}`
315
362
  );
316
363
  }
317
364
  if (operation !== void 0) {
@@ -373,7 +420,7 @@ function contextOperation(context) {
373
420
  return void 0;
374
421
  }
375
422
  function foirRoute(input) {
376
- if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
423
+ if (!isPublishableFoirKey(input.apiKey)) return { lane: "origin", reason: "key" };
377
424
  if (input.bearerToken) return { lane: "origin", reason: "bearer" };
378
425
  if (input.orgId || input.orgUrl) return { lane: "origin", reason: "org" };
379
426
  if (input.mutation) return { lane: "origin", reason: "operation" };
@@ -894,13 +941,19 @@ function planFoirModelCall(source, call) {
894
941
  }
895
942
  return planCall(source, opDef, `${call.model}.${call.op}`, operationName(model.typeName, call.op), call);
896
943
  }
897
- var CREATING_OPS = /* @__PURE__ */ new Set(["create", "submit"]);
944
+ var CREATING_OPS = /* @__PURE__ */ new Set(["create"]);
898
945
  function withCreateIdempotencyKey(source, model, op, args) {
899
946
  if (!CREATING_OPS.has(op) || args.idempotencyKey !== void 0) return args;
900
947
  const argTypes = source.models[model]?.ops?.[op]?.argTypes;
901
948
  if (!argTypes || !("idempotencyKey" in argTypes)) return args;
902
949
  return { ...args, idempotencyKey: globalThis.crypto.randomUUID() };
903
950
  }
951
+ function withStatedVisitor(op, args, place) {
952
+ if (place.country === void 0 && place.location === void 0) return args;
953
+ if (!op?.argTypes || !("contexts" in op.argTypes)) return args;
954
+ const said = args.contexts ?? {};
955
+ return { ...args, contexts: { ...place, ...said } };
956
+ }
904
957
  function planFoirNamespaceCall(source, call) {
905
958
  const namespace = source.namespaces?.[call.namespace];
906
959
  if (!namespace) {
@@ -2260,20 +2313,43 @@ function createFoirClient(clientConfig) {
2260
2313
  detectedDrift
2261
2314
  );
2262
2315
  };
2316
+ const visitorOfThisRequest = async () => {
2317
+ if (config.dispatch !== void 0 || config.visitor === void 0) {
2318
+ return { config, place: {} };
2319
+ }
2320
+ const { address, place } = await resolveFoirVisitor(config.visitor);
2321
+ return {
2322
+ config: {
2323
+ ...config,
2324
+ visitor: {
2325
+ address,
2326
+ country: place.country,
2327
+ latitude: place.location?.latitude,
2328
+ longitude: place.location?.longitude
2329
+ }
2330
+ },
2331
+ place
2332
+ };
2333
+ };
2263
2334
  const runModel = async (model, op, query) => {
2264
2335
  assertStillServed(`${model}.${op}`, (served) => served.modelCall(model, op, query?.include));
2265
2336
  const { args, include, omit, pick, depth } = splitQuery(query);
2266
2337
  const source = await selectionSource();
2338
+ const visitor = await visitorOfThisRequest();
2267
2339
  const plan = planFoirModelCall(source, {
2268
2340
  model,
2269
2341
  op,
2270
- args: withCreateIdempotencyKey(source, model, op, args),
2342
+ args: withStatedVisitor(
2343
+ source.models[model]?.ops?.[op],
2344
+ withCreateIdempotencyKey(source, model, op, args),
2345
+ visitor.place
2346
+ ),
2271
2347
  include,
2272
2348
  omit,
2273
2349
  pick,
2274
2350
  depth
2275
2351
  });
2276
- const result = await executeFoirDocument(config, plan);
2352
+ const result = await executeFoirDocument(visitor.config, plan);
2277
2353
  recordFoirSubjects(config.subjects, plan, result);
2278
2354
  maybeCheckDrift();
2279
2355
  return result;
@@ -2284,16 +2360,18 @@ function createFoirClient(clientConfig) {
2284
2360
  (served) => served.namespaceCall(namespace, op, query?.include)
2285
2361
  );
2286
2362
  const { args, include, omit, pick, depth } = splitQuery(query);
2287
- const plan = planFoirNamespaceCall(await selectionSource(), {
2363
+ const source = await selectionSource();
2364
+ const visitor = await visitorOfThisRequest();
2365
+ const plan = planFoirNamespaceCall(source, {
2288
2366
  namespace,
2289
2367
  op,
2290
- args,
2368
+ args: withStatedVisitor(source.namespaces?.[namespace]?.ops?.[op], args, visitor.place),
2291
2369
  include,
2292
2370
  omit,
2293
2371
  pick,
2294
2372
  depth
2295
2373
  });
2296
- const result = await executeFoirDocument(config, plan);
2374
+ const result = await executeFoirDocument(visitor.config, plan);
2297
2375
  recordFoirSubjects(config.subjects, plan, result);
2298
2376
  maybeCheckDrift();
2299
2377
  return result;
package/dist/index.js CHANGED
@@ -94,6 +94,27 @@ function isValidHostMessage(data) {
94
94
  return true;
95
95
  }
96
96
 
97
+ // src/transport/key-kind.ts
98
+ var KEY_PREFIXES = [
99
+ ["foir_pub_", "publishable"],
100
+ ["foir_srv_", "server"],
101
+ ["foir_adm_", "admin"],
102
+ ["pk_", "publishable"],
103
+ ["sk_", "older"],
104
+ ["tc_", "older"]
105
+ ];
106
+ var FOIR_CREDENTIALS_DOC = "https://docs.foir.dev/credentials";
107
+ function foirKeyKind(apiKey) {
108
+ if (typeof apiKey !== "string") return void 0;
109
+ for (const [prefix, kind] of KEY_PREFIXES) {
110
+ if (apiKey.startsWith(prefix)) return kind;
111
+ }
112
+ return void 0;
113
+ }
114
+ function adminKeyRefusal(client) {
115
+ return `[foir] ${client} was given an admin key. An admin key is a machine's own: nobody signs in through it, and an app never holds one. Build this client from your publishable key, or from your server key on the server that renders your pages. A machine that acts for itself uses createServerClient from @foir/sdk/server. See ${FOIR_CREDENTIALS_DOC}`;
116
+ }
117
+
97
118
  // src/client/platform-client.ts
98
119
  var PlatformClientError = class extends Error {
99
120
  constructor(message, status, response) {
@@ -107,6 +128,9 @@ var PlatformClientError = class extends Error {
107
128
  };
108
129
  function createPlatformClient(options) {
109
130
  const { apiUrl, apiKey } = options;
131
+ if (foirKeyKind(apiKey) === "admin") {
132
+ throw new PlatformClientError(adminKeyRefusal("createPlatformClient"));
133
+ }
110
134
  return {
111
135
  async query(query, variables) {
112
136
  const res = await fetch(apiUrl, {
package/dist/next.js CHANGED
@@ -20,6 +20,26 @@ function createFoirCookieAdapter(store, opts = {}) {
20
20
  // src/graphql/foir-request.ts
21
21
  import { print as print2 } from "graphql";
22
22
 
23
+ // src/transport/key-kind.ts
24
+ var KEY_PREFIXES = [
25
+ ["foir_pub_", "publishable"],
26
+ ["foir_srv_", "server"],
27
+ ["foir_adm_", "admin"],
28
+ ["pk_", "publishable"],
29
+ ["sk_", "older"],
30
+ ["tc_", "older"]
31
+ ];
32
+ function foirKeyKind(apiKey) {
33
+ if (typeof apiKey !== "string") return void 0;
34
+ for (const [prefix, kind] of KEY_PREFIXES) {
35
+ if (apiKey.startsWith(prefix)) return kind;
36
+ }
37
+ return void 0;
38
+ }
39
+ function isPublishableFoirKey(apiKey) {
40
+ return foirKeyKind(apiKey) === "publishable";
41
+ }
42
+
23
43
  // src/graphql/cdn-route.ts
24
44
  import { print } from "graphql";
25
45
 
@@ -123,6 +143,28 @@ function refuseCredentialHeaders(extra) {
123
143
  }
124
144
  }
125
145
  var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
146
+ function sentCoordinate(value) {
147
+ return Math.round(value * 100) / 100;
148
+ }
149
+ async function resolveFoirVisitor(source) {
150
+ const value = typeof source === "function" ? await source() : source;
151
+ if (value === null || value === void 0) return { place: {} };
152
+ if (typeof value === "string") {
153
+ const address2 = value.trim();
154
+ return { address: address2 || void 0, place: {} };
155
+ }
156
+ const address = value.address?.trim();
157
+ const country = value.country?.trim();
158
+ const place = {};
159
+ if (country) place.country = country;
160
+ if (Number.isFinite(value.latitude) && Number.isFinite(value.longitude)) {
161
+ place.location = {
162
+ latitude: sentCoordinate(value.latitude),
163
+ longitude: sentCoordinate(value.longitude)
164
+ };
165
+ }
166
+ return { address: address || void 0, place };
167
+ }
126
168
  function buildFoirHeaders(input) {
127
169
  const headers = {
128
170
  "Content-Type": "application/json"
@@ -739,10 +781,7 @@ function foirDataEndpoint(apiUrl, path) {
739
781
  return foirApiEndpoint(apiUrl, path);
740
782
  }
741
783
  async function resolveVisitorAddress(config) {
742
- const source = config.visitor;
743
- const value = typeof source === "function" ? await source() : source;
744
- const address = value?.trim();
745
- return address ? address : void 0;
784
+ return (await resolveFoirVisitor(config.visitor)).address;
746
785
  }
747
786
  async function resolveBearerToken(config) {
748
787
  const source = config.bearerToken;
@@ -783,7 +822,7 @@ async function resolveFoirAuth(config) {
783
822
  };
784
823
  }
785
824
  function foirRoute(input) {
786
- if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
825
+ if (!isPublishableFoirKey(input.apiKey)) return { lane: "origin", reason: "key" };
787
826
  if (input.bearerToken) return { lane: "origin", reason: "bearer" };
788
827
  if (input.orgId || input.orgUrl) return { lane: "origin", reason: "org" };
789
828
  if (input.mutation) return { lane: "origin", reason: "operation" };
@@ -921,6 +960,9 @@ var AUTH_OPS = {
921
960
  // Post-credential self reads.
922
961
  currentUser: "currentUser",
923
962
  myProviderToken: "myProviderToken",
963
+ // What the calling credential may do, and for a signed-in customer the roles
964
+ // they hold. `./access` reads it.
965
+ whoami: "whoami",
924
966
  // Methods.
925
967
  customerLogin: "customerLogin",
926
968
  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,21 @@ 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
- grantedScopes: string[];
57
- /** What your placement declares that this operator does not hold, and why. */
58
- withheld: {
59
- scope: string;
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;
60
66
  reason: string;
61
67
  }[];
62
68
  }
69
+ /** What a console sends with a token. A list it does not send is an empty one. */
70
+ type PlacementGrantMessage = Pick<PlacementGrant, 'accessToken' | 'expiresAt' | 'orgId'> & Partial<Pick<PlacementGrant, 'grants' | 'withheldGrants'>>;
63
71
  /** How the console looks, for an embedded page to match. */
64
72
  export interface PlacementAppearance {
65
73
  /**
@@ -71,7 +79,7 @@ export interface PlacementAppearance {
71
79
  /** A message the console sends an embedded page. */
72
80
  export type PlacementHostMessage = {
73
81
  type: typeof PLACEMENT_MESSAGE.token;
74
- grant: PlacementGrant;
82
+ grant: PlacementGrantMessage;
75
83
  } | {
76
84
  type: typeof PLACEMENT_MESSAGE.refused;
77
85
  reason: string;
@@ -138,8 +146,14 @@ export interface PlacementSession {
138
146
  * older than this). Kept current as the operator changes it.
139
147
  */
140
148
  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;
149
+ /**
150
+ * Whether the token covers this grant, to hide or grey out what the operator
151
+ * cannot do. It answers as Foir's API would: a grant for every model covers
152
+ * the same grant for one, and one for every owner's rows covers a person's
153
+ * own. `can('records:read.all:order')` is true for a token that carries
154
+ * `records:read.all`.
155
+ */
156
+ can(grant: string): boolean;
143
157
  /** A GraphQL request to Foir's API as the operator, through your app. */
144
158
  request<TResult = unknown>(document: string | {
145
159
  toString(): string;
@@ -156,3 +170,4 @@ export interface PlacementSession {
156
170
  * console gives no token, or when none arrives in time.
157
171
  */
158
172
  export declare function connectPlacement(options: ConnectPlacementOptions): Promise<PlacementSession>;
173
+ export {};