@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.
- package/CHANGELOG.md +330 -0
- package/dist/app/client.d.ts +109 -21
- package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +223 -211
- package/dist/app/errors.d.ts +2 -2
- package/dist/app/index.d.ts +4 -4
- package/dist/app/manifest.d.ts +1 -1
- package/dist/app/vocabulary.generated.d.ts +13 -10
- package/dist/app-contract.js +27 -31
- package/dist/app.js +80 -23
- package/dist/auth/access.d.ts +106 -0
- package/dist/auth/grant-table.generated.d.ts +13 -0
- package/dist/auth/grants.d.ts +38 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/operations.d.ts +6 -1
- package/dist/auth/ops.d.ts +1 -0
- package/dist/auth.js +321 -7
- package/dist/client/platform-client.d.ts +1 -1
- package/dist/client.js +28 -1
- package/dist/codegen/index.js +1 -1
- package/dist/data/builtins.d.ts +57 -42
- package/dist/data/document.d.ts +14 -0
- package/dist/data/index.d.ts +2 -2
- package/dist/data/transport.d.ts +27 -20
- package/dist/data.js +91 -13
- package/dist/index.js +24 -0
- package/dist/next.js +47 -5
- package/dist/placement/index.d.ts +26 -11
- package/dist/placement.js +219 -2
- package/dist/push.js +55 -6
- package/dist/react/FoirProvider.d.ts +40 -1
- package/dist/react/feedback-notes.d.ts +1 -1
- package/dist/react/index.d.ts +5 -4
- package/dist/react.js +391 -8
- package/dist/server/events-client.d.ts +7 -7
- package/dist/server/index.d.ts +1 -1
- package/dist/server/require-scoped-token.d.ts +1 -1
- package/dist/server/server-client.d.ts +1 -1
- package/dist/server/verify-scoped-token.d.ts +18 -23
- package/dist/server.js +228 -14
- package/dist/transport/foir-http.d.ts +44 -5
- package/dist/transport/key-kind.d.ts +40 -0
- package/package.json +6 -5
package/dist/data/index.d.ts
CHANGED
|
@@ -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,
|
|
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';
|
package/dist/data/transport.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
9
|
-
*
|
|
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
|
-
/**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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:
|
|
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
|
-
/**
|
|
142
|
-
|
|
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 {};
|