@foir/sdk 0.20.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 +51 -0
- package/dist/auth.js +59 -13
- 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 +15 -56
- package/dist/data/document.d.ts +14 -0
- package/dist/data/index.d.ts +2 -2
- package/dist/data/transport.d.ts +25 -18
- package/dist/data.js +91 -13
- package/dist/index.js +24 -0
- package/dist/next.js +44 -5
- package/dist/placement/index.d.ts +2 -19
- package/dist/placement.js +11 -16
- package/dist/push.js +52 -6
- package/dist/react.js +51 -12
- package/dist/server/events-client.d.ts +3 -3
- package/dist/server/server-client.d.ts +1 -1
- package/dist/server/verify-scoped-token.d.ts +1 -25
- package/dist/server.js +55 -103
- package/dist/transport/foir-http.d.ts +44 -5
- package/dist/transport/key-kind.d.ts +40 -0
- package/package.json +1 -1
- package/dist/server/older-claims.generated.d.ts +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -131,14 +131,65 @@ Changes on `dev` that have not been published to npm.
|
|
|
131
131
|
release that carries `writeNote`. `foir.ns.notes` answers any field the
|
|
132
132
|
platform serves, so the call itself needs no new SDK.
|
|
133
133
|
|
|
134
|
+
- **A server says where its visitor is** (`@foir/sdk/data`). `visitor` takes
|
|
135
|
+
an object as well as an address: `{ address, country, latitude, longitude }`,
|
|
136
|
+
or a function that yields one for each request. The address is sent as
|
|
137
|
+
`X-Foir-Visitor-IP`, as before. The country and the coordinates are attached
|
|
138
|
+
to every read, in its `contexts`, so location targeting is decided by where
|
|
139
|
+
the visitor is and not by where your server is. Coordinates are sent at two
|
|
140
|
+
decimal places, and latitude and longitude go together or not at all. The
|
|
141
|
+
visitor is resolved once for a request, so the header and the read name the
|
|
142
|
+
same one. A string is still the address alone. New type: `FoirVisitor`, and
|
|
143
|
+
`country` and `location` on `FoirResolveContext`. It needs a key holding
|
|
144
|
+
`visitors:forward`, and the platform release that takes `contexts.country`
|
|
145
|
+
and `contexts.location`: an earlier platform refuses a read that names them.
|
|
146
|
+
|
|
134
147
|
### Changed
|
|
135
148
|
|
|
149
|
+
- **Breaking: an app client refuses an admin key** (`@foir/sdk/data`).
|
|
150
|
+
`createFoirClient` given a key that begins `foir_adm_` throws before any
|
|
151
|
+
request. An admin key is a machine's own: nobody signs in through it, and an
|
|
152
|
+
app never holds one. Build the client from a publishable key, or from a
|
|
153
|
+
server key on the server that renders your pages. A machine that acts for
|
|
154
|
+
itself uses `createServerClient` from `@foir/sdk/server`. A key minted
|
|
155
|
+
before a key had a kind (`sk_`) is not refused here.
|
|
156
|
+
- **A key's kind is read from its prefix in one place.** A key is publishable
|
|
157
|
+
(`foir_pub_`), server (`foir_srv_`) or admin (`foir_adm_`), and a key minted
|
|
158
|
+
earlier is `pk_`, `sk_` or `tc_`. Only a publishable key, in either format,
|
|
159
|
+
is named in a URL and takes the shared cache lane. Earlier versions looked
|
|
160
|
+
for the letters `pk_` alone, so a `foir_pub_` key was sent to the origin and
|
|
161
|
+
missed the shared cache.
|
|
162
|
+
- **`visitor` takes a server key** (`@foir/sdk/data`). A client that names the
|
|
163
|
+
visitor it calls for is built from a server key (`foir_srv_`), or from a
|
|
164
|
+
secret key minted before a key had a kind. Earlier versions looked for the
|
|
165
|
+
letters `sk_` alone and refused a server key. A publishable key is still
|
|
166
|
+
refused `visitor`, in either format.
|
|
167
|
+
- **The events client takes an admin key** (`@foir/sdk/server`).
|
|
168
|
+
`createEventsClient` refuses a publishable key and a server key by name, and
|
|
169
|
+
takes an admin key (`foir_adm_`) or a secret key minted earlier.
|
|
136
170
|
- **`FOIR_EVENTS_SCOPE` is `events:read`** (`@foir/sdk/server`). It was
|
|
137
171
|
`events:list.project`, the name the event log's permission had when a key
|
|
138
172
|
was written with scopes. A key is made with the permission catalogue's
|
|
139
173
|
names, where it is `events:read`, and that is what a refusal names. A key
|
|
140
174
|
minted under the older name holds `events:read` and needs no change.
|
|
141
175
|
|
|
176
|
+
### Removed
|
|
177
|
+
|
|
178
|
+
- **Breaking: `ScopedTokenClaims` has no `cap`, `rrm` or `rwm`**
|
|
179
|
+
(`@foir/sdk/server`). The platform no longer writes the claims a token
|
|
180
|
+
carried before it said its grants. Read `grants`, or `recordsReadModels`,
|
|
181
|
+
`recordsWriteModels` and `recordsCreateModels`.
|
|
182
|
+
- **Breaking: `PlacementGrant` has no `grantedScopes` or `withheld`**
|
|
183
|
+
(`@foir/sdk/placement`). A console sends what a page's token carries as
|
|
184
|
+
`grants` and what it was refused as `withheldGrants`, and no longer sends
|
|
185
|
+
the same two lists under their older names. Read `session.grant.grants`, or
|
|
186
|
+
ask `session.can`.
|
|
187
|
+
- **Breaking: `verifyScopedToken` refuses a token that does not say its
|
|
188
|
+
grants** (`@foir/sdk/server`), with `the token says no grants`, as the
|
|
189
|
+
platform refuses it. It used to read such a token as what its older claims
|
|
190
|
+
were worth. `requireScopedToken` answers it 401 like any token that does not
|
|
191
|
+
verify.
|
|
192
|
+
|
|
142
193
|
### Deprecated
|
|
143
194
|
|
|
144
195
|
- **`FoirCreateMyNoteArgs`, `FoirSubmitAnonymousNoteArgs` and
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
// src/graphql/foir-request.ts
|
|
2
2
|
import { print as print2 } from "graphql";
|
|
3
3
|
|
|
4
|
+
// src/transport/key-kind.ts
|
|
5
|
+
var KEY_PREFIXES = [
|
|
6
|
+
["foir_pub_", "publishable"],
|
|
7
|
+
["foir_srv_", "server"],
|
|
8
|
+
["foir_adm_", "admin"],
|
|
9
|
+
["pk_", "publishable"],
|
|
10
|
+
["sk_", "older"],
|
|
11
|
+
["tc_", "older"]
|
|
12
|
+
];
|
|
13
|
+
var FOIR_CREDENTIALS_DOC = "https://docs.foir.dev/credentials";
|
|
14
|
+
function foirKeyKind(apiKey) {
|
|
15
|
+
if (typeof apiKey !== "string") return void 0;
|
|
16
|
+
for (const [prefix, kind] of KEY_PREFIXES) {
|
|
17
|
+
if (apiKey.startsWith(prefix)) return kind;
|
|
18
|
+
}
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
function adminKeyRefusal(client) {
|
|
22
|
+
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}`;
|
|
23
|
+
}
|
|
24
|
+
function isPublishableFoirKey(apiKey) {
|
|
25
|
+
return foirKeyKind(apiKey) === "publishable";
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
// src/graphql/cdn-route.ts
|
|
5
29
|
import { print } from "graphql";
|
|
6
30
|
|
|
@@ -125,6 +149,28 @@ function refuseCredentialHeaders(extra) {
|
|
|
125
149
|
}
|
|
126
150
|
}
|
|
127
151
|
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
152
|
+
function sentCoordinate(value) {
|
|
153
|
+
return Math.round(value * 100) / 100;
|
|
154
|
+
}
|
|
155
|
+
async function resolveFoirVisitor(source) {
|
|
156
|
+
const value = typeof source === "function" ? await source() : source;
|
|
157
|
+
if (value === null || value === void 0) return { place: {} };
|
|
158
|
+
if (typeof value === "string") {
|
|
159
|
+
const address2 = value.trim();
|
|
160
|
+
return { address: address2 || void 0, place: {} };
|
|
161
|
+
}
|
|
162
|
+
const address = value.address?.trim();
|
|
163
|
+
const country = value.country?.trim();
|
|
164
|
+
const place = {};
|
|
165
|
+
if (country) place.country = country;
|
|
166
|
+
if (Number.isFinite(value.latitude) && Number.isFinite(value.longitude)) {
|
|
167
|
+
place.location = {
|
|
168
|
+
latitude: sentCoordinate(value.latitude),
|
|
169
|
+
longitude: sentCoordinate(value.longitude)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return { address: address || void 0, place };
|
|
173
|
+
}
|
|
128
174
|
function buildFoirHeaders(input) {
|
|
129
175
|
const headers = {
|
|
130
176
|
"Content-Type": "application/json"
|
|
@@ -277,7 +323,7 @@ function isCdnEligible(env, documentText, bearerToken, orgId) {
|
|
|
277
323
|
if (orgId) return false;
|
|
278
324
|
if (isFoirPreview(env)) return false;
|
|
279
325
|
const apiKey = resolveFoirApiKey(env);
|
|
280
|
-
if (!apiKey
|
|
326
|
+
if (!isPublishableFoirKey(apiKey)) return false;
|
|
281
327
|
return !/\bmutation\b/.test(documentText);
|
|
282
328
|
}
|
|
283
329
|
|
|
@@ -290,6 +336,9 @@ async function foirRequest(env, document, variables, options) {
|
|
|
290
336
|
"[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
|
|
291
337
|
);
|
|
292
338
|
}
|
|
339
|
+
if (foirKeyKind(apiKey) === "admin") {
|
|
340
|
+
throw new FoirRequestError(adminKeyRefusal("foirRequest"));
|
|
341
|
+
}
|
|
293
342
|
const headers = buildFoirHeaders({
|
|
294
343
|
apiKey,
|
|
295
344
|
draftChannel: isFoirPreview(env),
|
|
@@ -852,10 +901,7 @@ function foirDataEndpoint(apiUrl, path) {
|
|
|
852
901
|
return foirApiEndpoint(apiUrl, path);
|
|
853
902
|
}
|
|
854
903
|
async function resolveVisitorAddress(config) {
|
|
855
|
-
|
|
856
|
-
const value = typeof source === "function" ? await source() : source;
|
|
857
|
-
const address = value?.trim();
|
|
858
|
-
return address ? address : void 0;
|
|
904
|
+
return (await resolveFoirVisitor(config.visitor)).address;
|
|
859
905
|
}
|
|
860
906
|
async function resolveBearerToken(config) {
|
|
861
907
|
const source = config.bearerToken;
|
|
@@ -896,7 +942,7 @@ async function resolveFoirAuth(config) {
|
|
|
896
942
|
};
|
|
897
943
|
}
|
|
898
944
|
function foirRoute(input) {
|
|
899
|
-
if (!input.apiKey
|
|
945
|
+
if (!isPublishableFoirKey(input.apiKey)) return { lane: "origin", reason: "key" };
|
|
900
946
|
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
901
947
|
if (input.orgId || input.orgUrl) return { lane: "origin", reason: "org" };
|
|
902
948
|
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
@@ -1362,10 +1408,10 @@ async function requireAdmin(adapter, options) {
|
|
|
1362
1408
|
|
|
1363
1409
|
// src/auth/grant-table.generated.ts
|
|
1364
1410
|
var GRANT_TABLE = [
|
|
1365
|
-
["records:read", true, ["own", "
|
|
1411
|
+
["records:read", true, ["own", "all"], []],
|
|
1366
1412
|
["records:create", true, [], []],
|
|
1367
|
-
["records:write", true, ["own", "
|
|
1368
|
-
["records:delete", true, ["own", "
|
|
1413
|
+
["records:write", true, ["own", "all"], []],
|
|
1414
|
+
["records:delete", true, ["own", "all"], []],
|
|
1369
1415
|
["records:publish", true, ["own", "all"], []],
|
|
1370
1416
|
["records:attribute", true, [], []],
|
|
1371
1417
|
["search:read", true, [], []],
|
|
@@ -1381,8 +1427,8 @@ var GRANT_TABLE = [
|
|
|
1381
1427
|
["notifications:consent", false, [], []],
|
|
1382
1428
|
["pushcredentials:read", false, [], []],
|
|
1383
1429
|
["pushcredentials:write", false, [], []],
|
|
1384
|
-
["notes:read", false, [], []],
|
|
1385
|
-
["notes:write", false, [], []],
|
|
1430
|
+
["notes:read", false, ["own", "all"], []],
|
|
1431
|
+
["notes:write", false, ["own", "all"], []],
|
|
1386
1432
|
["notes:create", false, [], []],
|
|
1387
1433
|
["approvals:read", false, [], []],
|
|
1388
1434
|
["approvals:write", false, [], []],
|
|
@@ -1439,7 +1485,7 @@ var GRANT_TABLE = [
|
|
|
1439
1485
|
["roles:write", false, [], []],
|
|
1440
1486
|
["projects:read", false, [], []],
|
|
1441
1487
|
["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"]],
|
|
1488
|
+
["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.all", "notes:write.all", "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", "audit:read"]],
|
|
1443
1489
|
["projects:create", false, [], []],
|
|
1444
1490
|
["projects:delete", false, [], []],
|
|
1445
1491
|
["tenants:read", false, [], []],
|
|
@@ -1455,7 +1501,7 @@ var GRANT_TABLE = [
|
|
|
1455
1501
|
["tenanthooks:read", false, [], []],
|
|
1456
1502
|
["tenanthooks:write", false, [], []],
|
|
1457
1503
|
["apps:install", false, [], []],
|
|
1458
|
-
["tenant:manage", false, [], ["project:manage", "projects:create", "projects:delete", "tenants:read", "tenants:write", "tenants:delete", "
|
|
1504
|
+
["tenant:manage", false, [], ["project:manage", "projects:create", "projects:delete", "tenants:read", "tenants:write", "tenants:delete", "export:run", "billing:read", "billing:manage", "usage:read", "controlkeys:read", "controlkeys:write", "tenanthooks:read", "tenanthooks:write", "apps:install"]],
|
|
1459
1505
|
["platform:read", false, [], []],
|
|
1460
1506
|
["platform:manage", false, [], ["tenant:manage", "platform:read", "platform:create_workspace", "users:setup"]],
|
|
1461
1507
|
["platform:create_workspace", false, [], []],
|
|
@@ -17,7 +17,7 @@ export declare class PlatformClientError extends Error {
|
|
|
17
17
|
* ```ts
|
|
18
18
|
* const client = createPlatformClient({
|
|
19
19
|
* apiUrl: 'https://api.example.com/graphql',
|
|
20
|
-
* apiKey: '
|
|
20
|
+
* apiKey: 'foir_pub_...', // runs in the browser: a publishable key only
|
|
21
21
|
* });
|
|
22
22
|
*
|
|
23
23
|
* const { data } = await client.query(
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
// src/graphql/foir-request.ts
|
|
2
2
|
import { print as print2 } from "graphql";
|
|
3
3
|
|
|
4
|
+
// src/transport/key-kind.ts
|
|
5
|
+
var KEY_PREFIXES = [
|
|
6
|
+
["foir_pub_", "publishable"],
|
|
7
|
+
["foir_srv_", "server"],
|
|
8
|
+
["foir_adm_", "admin"],
|
|
9
|
+
["pk_", "publishable"],
|
|
10
|
+
["sk_", "older"],
|
|
11
|
+
["tc_", "older"]
|
|
12
|
+
];
|
|
13
|
+
var FOIR_CREDENTIALS_DOC = "https://docs.foir.dev/credentials";
|
|
14
|
+
function foirKeyKind(apiKey) {
|
|
15
|
+
if (typeof apiKey !== "string") return void 0;
|
|
16
|
+
for (const [prefix, kind] of KEY_PREFIXES) {
|
|
17
|
+
if (apiKey.startsWith(prefix)) return kind;
|
|
18
|
+
}
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
function adminKeyRefusal(client) {
|
|
22
|
+
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}`;
|
|
23
|
+
}
|
|
24
|
+
function isPublishableFoirKey(apiKey) {
|
|
25
|
+
return foirKeyKind(apiKey) === "publishable";
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
// src/graphql/cdn-route.ts
|
|
5
29
|
import { print } from "graphql";
|
|
6
30
|
|
|
@@ -282,7 +306,7 @@ function isCdnEligible(env, documentText, bearerToken, orgId) {
|
|
|
282
306
|
if (orgId) return false;
|
|
283
307
|
if (isFoirPreview(env)) return false;
|
|
284
308
|
const apiKey = resolveFoirApiKey(env);
|
|
285
|
-
if (!apiKey
|
|
309
|
+
if (!isPublishableFoirKey(apiKey)) return false;
|
|
286
310
|
return !/\bmutation\b/.test(documentText);
|
|
287
311
|
}
|
|
288
312
|
|
|
@@ -329,6 +353,9 @@ async function foirRequest(env, document, variables, options) {
|
|
|
329
353
|
"[foir] PLATFORM_API_URL or PLATFORM_API_KEY is not set \u2014 add them to .env"
|
|
330
354
|
);
|
|
331
355
|
}
|
|
356
|
+
if (foirKeyKind(apiKey) === "admin") {
|
|
357
|
+
throw new FoirRequestError(adminKeyRefusal("foirRequest"));
|
|
358
|
+
}
|
|
332
359
|
const headers = buildFoirHeaders({
|
|
333
360
|
apiKey,
|
|
334
361
|
draftChannel: isFoirPreview(env),
|
package/dist/codegen/index.js
CHANGED
|
@@ -15,7 +15,7 @@ function schemaEntry(apiUrl, key) {
|
|
|
15
15
|
headers: {
|
|
16
16
|
"x-api-key": key ?? "",
|
|
17
17
|
// Read the draft channel so freshly pushed changes are visible
|
|
18
|
-
// (
|
|
18
|
+
// (ignored by an app's key that does not hold drafts:read).
|
|
19
19
|
"X-Foir-Schema-Channel": "draft"
|
|
20
20
|
}
|
|
21
21
|
}
|
package/dist/data/builtins.d.ts
CHANGED
|
@@ -618,62 +618,6 @@ export interface FoirCreateMyNoteArgs {
|
|
|
618
618
|
*/
|
|
619
619
|
attachmentFileIds?: string[];
|
|
620
620
|
}
|
|
621
|
-
/**
|
|
622
|
-
* `submitAnonymousNote` arguments — feedback from a visitor with no account.
|
|
623
|
-
*
|
|
624
|
-
* Present on the project's schema only while the project has opted in
|
|
625
|
-
* (Notes settings → "Accept anonymous notes"). The feedback widget does NOT
|
|
626
|
-
* use this: a signed-out visitor there is still asked to sign in, so this
|
|
627
|
-
* mutation is the only path a visitor's note can take.
|
|
628
|
-
*
|
|
629
|
-
* WRITE-ONLY. Nothing reads an anonymous note back on the public plane, which
|
|
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`.
|
|
635
|
-
*/
|
|
636
|
-
export interface FoirSubmitAnonymousNoteArgs {
|
|
637
|
-
/**
|
|
638
|
-
* Must be `'record'` — the only thing a visitor may write a note about.
|
|
639
|
-
* Narrower than `FoirCreateMyNoteArgs`, where a customer may also address a
|
|
640
|
-
* file or a model.
|
|
641
|
-
*/
|
|
642
|
-
subjectType: 'record';
|
|
643
|
-
subjectId: string;
|
|
644
|
-
/**
|
|
645
|
-
* The record's model key. REQUIRED here, unlike on `createMyNote` where it is
|
|
646
|
-
* conditional: the subject is always a record on this lane, so the condition
|
|
647
|
-
* is always met.
|
|
648
|
-
*/
|
|
649
|
-
subjectModelKey: string;
|
|
650
|
-
body: string;
|
|
651
|
-
/**
|
|
652
|
-
* Optional. Where the operators may reply.
|
|
653
|
-
*
|
|
654
|
-
* Captured as typed and NEVER verified — it is not treated as proof of who
|
|
655
|
-
* wrote the note, and nothing is sent to it to confirm it. Do not build a
|
|
656
|
-
* flow that assumes the address is reachable.
|
|
657
|
-
*/
|
|
658
|
-
contactEmail?: string;
|
|
659
|
-
}
|
|
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
|
-
*/
|
|
666
|
-
export interface FoirAnonymousNoteReceipt {
|
|
667
|
-
/**
|
|
668
|
-
* A reference for this submission, to quote when following it up.
|
|
669
|
-
*
|
|
670
|
-
* NOT a handle: there is no read that takes it. Showing it to the visitor is
|
|
671
|
-
* the whole of its purpose.
|
|
672
|
-
*/
|
|
673
|
-
reference: string;
|
|
674
|
-
/** When the platform recorded the submission. */
|
|
675
|
-
submittedAt: string;
|
|
676
|
-
}
|
|
677
621
|
export type FoirDeviceType = 'DESKTOP' | 'MOBILE' | 'TABLET';
|
|
678
622
|
export type FoirPlatformType = 'IOS' | 'ANDROID' | 'WEB';
|
|
679
623
|
/**
|
|
@@ -694,4 +638,19 @@ export interface FoirResolveContext {
|
|
|
694
638
|
locale?: string | null;
|
|
695
639
|
platform?: FoirPlatformType | null;
|
|
696
640
|
preview?: boolean | null;
|
|
641
|
+
/**
|
|
642
|
+
* The country of the visitor a server is calling for, as an ISO 3166-1
|
|
643
|
+
* alpha-2 code. Only a key holding `visitors:forward` may say it, and a
|
|
644
|
+
* client given a `visitor` attaches it to every read, so there is rarely a
|
|
645
|
+
* reason to write it on one.
|
|
646
|
+
*/
|
|
647
|
+
country?: string | null;
|
|
648
|
+
/**
|
|
649
|
+
* Where the visitor a server is calling for is, in decimal degrees. Only a
|
|
650
|
+
* key holding `visitors:forward` may say it.
|
|
651
|
+
*/
|
|
652
|
+
location?: {
|
|
653
|
+
latitude: number;
|
|
654
|
+
longitude: number;
|
|
655
|
+
} | null;
|
|
697
656
|
}
|
package/dist/data/document.d.ts
CHANGED
|
@@ -51,6 +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 FoirVisitorPlace } from '../transport/foir-http';
|
|
54
55
|
import type { FoirManifestOp, FoirManifestOpPayload, FoirManifestRelation, FoirManifestSelection, FoirManifestSelectionField, FoirManifestSelectionUnion } from './manifest';
|
|
55
56
|
/**
|
|
56
57
|
* One model, as the printer needs it.
|
|
@@ -260,6 +261,19 @@ export declare function planFoirModelCall(source: FoirSelectionSource, call: Foi
|
|
|
260
261
|
* choose, since only the caller knows which calls are the same one.
|
|
261
262
|
*/
|
|
262
263
|
export declare function withCreateIdempotencyKey(source: FoirSelectionSource, model: string, op: string, args: Record<string, unknown>): Record<string, unknown>;
|
|
264
|
+
/**
|
|
265
|
+
* A read's arguments with where its stated visitor is, when the op takes
|
|
266
|
+
* `contexts` and the client was given a visitor with a place.
|
|
267
|
+
*
|
|
268
|
+
* It is attached here, to every read, and not left to each call: a page
|
|
269
|
+
* renders with many reads, and one that forgot would be answered as a visitor
|
|
270
|
+
* nobody could place while the read beside it was not.
|
|
271
|
+
*
|
|
272
|
+
* What a call says of the place itself wins, part by part: a caller that
|
|
273
|
+
* passes `contexts.country` for one read has said something more exact than
|
|
274
|
+
* the client's visitor.
|
|
275
|
+
*/
|
|
276
|
+
export declare function withStatedVisitor(op: FoirManifestOp | undefined, args: Record<string, unknown>, place: FoirVisitorPlace): Record<string, unknown>;
|
|
263
277
|
/**
|
|
264
278
|
* Turn a namespace call into a document.
|
|
265
279
|
*
|
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, FoirWriteNoteArgs,
|
|
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
|
@@ -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;
|