@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/server.js
CHANGED
|
@@ -72,6 +72,165 @@ async function verifyWebhookSignature(payload, signature, secret) {
|
|
|
72
72
|
return mismatch === 0;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// src/auth/grant-table.generated.ts
|
|
76
|
+
var GRANT_TABLE = [
|
|
77
|
+
["records:read", true, ["own", "all"], []],
|
|
78
|
+
["records:create", true, [], []],
|
|
79
|
+
["records:write", true, ["own", "all"], []],
|
|
80
|
+
["records:delete", true, ["own", "all"], []],
|
|
81
|
+
["records:publish", true, ["own", "all"], []],
|
|
82
|
+
["records:attribute", true, [], []],
|
|
83
|
+
["search:read", true, [], []],
|
|
84
|
+
["embeddings:read", false, [], []],
|
|
85
|
+
["embeddings:write", false, [], []],
|
|
86
|
+
["schemas:read", false, [], []],
|
|
87
|
+
["drafts:read", false, [], []],
|
|
88
|
+
["files:read", false, ["own", "all"], []],
|
|
89
|
+
["files:write", false, ["own", "all"], []],
|
|
90
|
+
["files:delete", false, ["own", "all"], []],
|
|
91
|
+
["notifications:read", false, ["own", "all"], []],
|
|
92
|
+
["notifications:write", false, ["own", "named", "all"], []],
|
|
93
|
+
["notifications:consent", false, [], []],
|
|
94
|
+
["pushcredentials:read", false, [], []],
|
|
95
|
+
["pushcredentials:write", false, [], []],
|
|
96
|
+
["notes:read", false, ["own", "all"], []],
|
|
97
|
+
["notes:write", false, ["own", "all"], []],
|
|
98
|
+
["notes:create", false, [], []],
|
|
99
|
+
["approvals:read", false, [], []],
|
|
100
|
+
["approvals:write", false, [], []],
|
|
101
|
+
["rooms:read", true, [], []],
|
|
102
|
+
["rooms:write", true, [], []],
|
|
103
|
+
["presence:read", true, [], []],
|
|
104
|
+
["operations:read", false, [], []],
|
|
105
|
+
["operations:execute", true, [], []],
|
|
106
|
+
["operations:write", false, [], []],
|
|
107
|
+
["operations:complete", false, [], []],
|
|
108
|
+
["status:write", false, [], []],
|
|
109
|
+
["appconfig:read", false, [], []],
|
|
110
|
+
["schedules:read", false, [], []],
|
|
111
|
+
["schedules:write", false, [], []],
|
|
112
|
+
["hooks:read", false, [], []],
|
|
113
|
+
["hooks:write", false, [], []],
|
|
114
|
+
["relyingparties:read", false, [], []],
|
|
115
|
+
["relyingparties:write", false, [], []],
|
|
116
|
+
["domains:read", false, [], []],
|
|
117
|
+
["domains:write", false, [], []],
|
|
118
|
+
["secrets:read", false, [], []],
|
|
119
|
+
["secrets:list", false, [], []],
|
|
120
|
+
["secrets:put", false, [], []],
|
|
121
|
+
["secrets:delete", false, [], []],
|
|
122
|
+
["appsecrets:read", false, [], []],
|
|
123
|
+
["appsecrets:list", false, [], []],
|
|
124
|
+
["appsecrets:put", false, [], []],
|
|
125
|
+
["appsecrets:delete", false, [], []],
|
|
126
|
+
["opsecrets:read", false, [], []],
|
|
127
|
+
["opsecrets:write", false, [], []],
|
|
128
|
+
["settings:read", false, [], []],
|
|
129
|
+
["settings:write", false, [], []],
|
|
130
|
+
["designtokens:read", false, [], []],
|
|
131
|
+
["designtokens:write", false, [], []],
|
|
132
|
+
["models:read", false, [], []],
|
|
133
|
+
["models:write", false, [], []],
|
|
134
|
+
["configs:read", false, [], []],
|
|
135
|
+
["configs:write", false, [], []],
|
|
136
|
+
["apps:read", false, [], []],
|
|
137
|
+
["apps:write", false, [], []],
|
|
138
|
+
["events:read", false, [], []],
|
|
139
|
+
["targeting:debug", false, [], []],
|
|
140
|
+
["visitors:forward", false, [], []],
|
|
141
|
+
["apikeys:read", false, [], []],
|
|
142
|
+
["apikeys:write", false, [], []],
|
|
143
|
+
["organizations:read", false, [], []],
|
|
144
|
+
["organizations:write", false, [], []],
|
|
145
|
+
["organizations:create", false, [], []],
|
|
146
|
+
["organizations:delete", false, [], []],
|
|
147
|
+
["organizations:reach", false, [], []],
|
|
148
|
+
["members:read", false, [], []],
|
|
149
|
+
["members:write", false, [], []],
|
|
150
|
+
["roles:read", false, [], []],
|
|
151
|
+
["roles:write", false, [], []],
|
|
152
|
+
["projects:read", false, [], []],
|
|
153
|
+
["projects:write", false, [], []],
|
|
154
|
+
["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"]],
|
|
155
|
+
["projects:create", false, [], []],
|
|
156
|
+
["projects:delete", false, [], []],
|
|
157
|
+
["tenants:read", false, [], []],
|
|
158
|
+
["tenants:write", false, [], []],
|
|
159
|
+
["tenants:delete", false, [], []],
|
|
160
|
+
["audit:read", false, [], []],
|
|
161
|
+
["export:run", false, [], []],
|
|
162
|
+
["billing:read", false, [], []],
|
|
163
|
+
["billing:manage", false, [], []],
|
|
164
|
+
["usage:read", false, [], []],
|
|
165
|
+
["controlkeys:read", false, [], []],
|
|
166
|
+
["controlkeys:write", false, [], []],
|
|
167
|
+
["tenanthooks:read", false, [], []],
|
|
168
|
+
["tenanthooks:write", false, [], []],
|
|
169
|
+
["apps:install", false, [], []],
|
|
170
|
+
["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"]],
|
|
171
|
+
["platform:read", false, [], []],
|
|
172
|
+
["platform:manage", false, [], ["tenant:manage", "platform:read", "platform:create_workspace", "users:setup"]],
|
|
173
|
+
["platform:create_workspace", false, [], []],
|
|
174
|
+
["users:read", false, ["own", "named", "all"], []],
|
|
175
|
+
["users:write", false, ["own", "all"], []],
|
|
176
|
+
["users:setup", false, [], []],
|
|
177
|
+
["users:delete", false, ["own", "all"], []],
|
|
178
|
+
["users:create", false, [], []],
|
|
179
|
+
["identities:read", false, [], []],
|
|
180
|
+
["identities:delete", false, [], []],
|
|
181
|
+
["usersecrets:read", false, [], []],
|
|
182
|
+
["usersecrets:list", false, [], []],
|
|
183
|
+
["usersecrets:put", false, [], []],
|
|
184
|
+
["usersecrets:delete", false, [], []],
|
|
185
|
+
["auth:login_with_foir", false, [], []],
|
|
186
|
+
["signinproviders:read", false, [], []],
|
|
187
|
+
["signinproviders:write", false, [], []]
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
// src/auth/grants.ts
|
|
191
|
+
var index;
|
|
192
|
+
function catalogue() {
|
|
193
|
+
if (!index) {
|
|
194
|
+
const byName = new Map(GRANT_TABLE.map((row) => [row[0], row]));
|
|
195
|
+
const longestFirst = [...byName.keys()].sort(
|
|
196
|
+
(a, b) => a.length !== b.length ? b.length - a.length : a < b ? -1 : 1
|
|
197
|
+
);
|
|
198
|
+
index = { byName, longestFirst };
|
|
199
|
+
}
|
|
200
|
+
return index;
|
|
201
|
+
}
|
|
202
|
+
function hasReach(permission, reach) {
|
|
203
|
+
const reaches = permission[2];
|
|
204
|
+
return reaches.length === 0 ? reach === "own" : reaches.includes(reach);
|
|
205
|
+
}
|
|
206
|
+
function parseGrant(s) {
|
|
207
|
+
const { byName, longestFirst } = catalogue();
|
|
208
|
+
for (const name of longestFirst) {
|
|
209
|
+
if (!s.startsWith(name)) continue;
|
|
210
|
+
let rest = s.slice(name.length);
|
|
211
|
+
if (rest !== "" && rest[0] !== "." && rest[0] !== ":") continue;
|
|
212
|
+
const permission = byName.get(name);
|
|
213
|
+
let reach = "own";
|
|
214
|
+
if (rest.startsWith(".")) {
|
|
215
|
+
const after = rest.slice(1);
|
|
216
|
+
const colon = after.indexOf(":");
|
|
217
|
+
const word = colon < 0 ? after : after.slice(0, colon);
|
|
218
|
+
if (word !== "named" && word !== "all") return null;
|
|
219
|
+
if (!hasReach(permission, word)) return null;
|
|
220
|
+
reach = word;
|
|
221
|
+
rest = after.slice(word.length);
|
|
222
|
+
}
|
|
223
|
+
let target = "";
|
|
224
|
+
if (rest.startsWith(":")) {
|
|
225
|
+
target = rest.slice(1);
|
|
226
|
+
if (!permission[1]) return null;
|
|
227
|
+
if (target === "" || /[: \t\n*]/.test(target)) return null;
|
|
228
|
+
}
|
|
229
|
+
return { permission, reach, target };
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
75
234
|
// src/server/verify-scoped-token.ts
|
|
76
235
|
var JWKS_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
77
236
|
function getCache() {
|
|
@@ -129,11 +288,26 @@ function parseSubject(sub) {
|
|
|
129
288
|
}
|
|
130
289
|
return { tenantId, projectId, appName: sub.slice(second + 1) };
|
|
131
290
|
}
|
|
291
|
+
function modelsFor(claims, permission) {
|
|
292
|
+
const out = [];
|
|
293
|
+
for (const grant of claims.grants) {
|
|
294
|
+
const parsed = parseGrant(grant);
|
|
295
|
+
if (!parsed || parsed.permission[0] !== permission || parsed.target === "") continue;
|
|
296
|
+
if (!out.includes(parsed.target)) out.push(parsed.target);
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
132
300
|
function recordsReadModels(claims) {
|
|
133
|
-
return claims
|
|
301
|
+
return modelsFor(claims, "records:read");
|
|
134
302
|
}
|
|
135
303
|
function recordsWriteModels(claims) {
|
|
136
|
-
return claims
|
|
304
|
+
return modelsFor(claims, "records:write");
|
|
305
|
+
}
|
|
306
|
+
function recordsCreateModels(claims) {
|
|
307
|
+
return modelsFor(claims, "records:create");
|
|
308
|
+
}
|
|
309
|
+
function strings(value) {
|
|
310
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
137
311
|
}
|
|
138
312
|
async function verifyScopedToken(token, jwksUrl, options) {
|
|
139
313
|
return verifyWithKeys(token, () => loadJwks(jwksUrl), void 0, options);
|
|
@@ -169,12 +343,13 @@ async function verifyWithKeys(token, loadKeys, refetchForUnknownKid, options) {
|
|
|
169
343
|
)
|
|
170
344
|
);
|
|
171
345
|
if (!ok) throw new Error("signature invalid");
|
|
172
|
-
let
|
|
346
|
+
let payload;
|
|
173
347
|
try {
|
|
174
|
-
|
|
348
|
+
payload = JSON.parse(new TextDecoder().decode(base64UrlDecode(rawPayload)));
|
|
175
349
|
} catch {
|
|
176
350
|
throw new Error("unreadable token payload");
|
|
177
351
|
}
|
|
352
|
+
const claims = { ...payload, grants: strings(payload.grants) };
|
|
178
353
|
const now = Math.floor((options?.now?.() ?? Date.now()) / 1e3);
|
|
179
354
|
if (claims.iss !== expectedIssuer) throw new Error(`bad iss ${claims.iss}`);
|
|
180
355
|
if (typeof claims.exp !== "number" || claims.exp < now) {
|
|
@@ -183,6 +358,7 @@ async function verifyWithKeys(token, loadKeys, refetchForUnknownKid, options) {
|
|
|
183
358
|
if (typeof claims.nbf === "number" && claims.nbf > now + 30) {
|
|
184
359
|
throw new Error("token not yet valid");
|
|
185
360
|
}
|
|
361
|
+
if (!("grants" in payload)) throw new Error("the token says no grants");
|
|
186
362
|
return claims;
|
|
187
363
|
}
|
|
188
364
|
function parseContextHeader(raw) {
|
|
@@ -618,6 +794,28 @@ function refuseCredentialHeaders(extra) {
|
|
|
618
794
|
}
|
|
619
795
|
}
|
|
620
796
|
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
797
|
+
function sentCoordinate(value) {
|
|
798
|
+
return Math.round(value * 100) / 100;
|
|
799
|
+
}
|
|
800
|
+
async function resolveFoirVisitor(source) {
|
|
801
|
+
const value = typeof source === "function" ? await source() : source;
|
|
802
|
+
if (value === null || value === void 0) return { place: {} };
|
|
803
|
+
if (typeof value === "string") {
|
|
804
|
+
const address2 = value.trim();
|
|
805
|
+
return { address: address2 || void 0, place: {} };
|
|
806
|
+
}
|
|
807
|
+
const address = value.address?.trim();
|
|
808
|
+
const country = value.country?.trim();
|
|
809
|
+
const place = {};
|
|
810
|
+
if (country) place.country = country;
|
|
811
|
+
if (Number.isFinite(value.latitude) && Number.isFinite(value.longitude)) {
|
|
812
|
+
place.location = {
|
|
813
|
+
latitude: sentCoordinate(value.latitude),
|
|
814
|
+
longitude: sentCoordinate(value.longitude)
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
return { address: address || void 0, place };
|
|
818
|
+
}
|
|
621
819
|
function buildFoirHeaders(input) {
|
|
622
820
|
const headers = {
|
|
623
821
|
"Content-Type": "application/json"
|
|
@@ -835,12 +1033,26 @@ async function hmacSha256Hex(secret, body) {
|
|
|
835
1033
|
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
836
1034
|
}
|
|
837
1035
|
|
|
1036
|
+
// src/transport/key-kind.ts
|
|
1037
|
+
var KEY_PREFIXES = [
|
|
1038
|
+
["foir_pub_", "publishable"],
|
|
1039
|
+
["foir_srv_", "server"],
|
|
1040
|
+
["foir_adm_", "admin"],
|
|
1041
|
+
["pk_", "publishable"],
|
|
1042
|
+
["sk_", "older"],
|
|
1043
|
+
["tc_", "older"]
|
|
1044
|
+
];
|
|
1045
|
+
function foirKeyKind(apiKey) {
|
|
1046
|
+
if (typeof apiKey !== "string") return void 0;
|
|
1047
|
+
for (const [prefix, kind] of KEY_PREFIXES) {
|
|
1048
|
+
if (apiKey.startsWith(prefix)) return kind;
|
|
1049
|
+
}
|
|
1050
|
+
return void 0;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
838
1053
|
// src/data/transport.ts
|
|
839
1054
|
async function resolveVisitorAddress(config) {
|
|
840
|
-
|
|
841
|
-
const value = typeof source === "function" ? await source() : source;
|
|
842
|
-
const address = value?.trim();
|
|
843
|
-
return address ? address : void 0;
|
|
1055
|
+
return (await resolveFoirVisitor(config.visitor)).address;
|
|
844
1056
|
}
|
|
845
1057
|
async function resolveBearerToken(config) {
|
|
846
1058
|
const source = config.bearerToken;
|
|
@@ -1241,7 +1453,7 @@ function describe2(payload) {
|
|
|
1241
1453
|
|
|
1242
1454
|
// src/server/events-client.ts
|
|
1243
1455
|
var FOIR_EVENTS_PATH = "/v1/events";
|
|
1244
|
-
var FOIR_EVENTS_SCOPE = "events:
|
|
1456
|
+
var FOIR_EVENTS_SCOPE = "events:read";
|
|
1245
1457
|
var FOIR_EVENTS_RETENTION_DAYS = 90;
|
|
1246
1458
|
var FoirEventCursorExpiredError = class extends FoirRequestError {
|
|
1247
1459
|
/**
|
|
@@ -1277,17 +1489,18 @@ function assertOperatorKey(apiKey) {
|
|
|
1277
1489
|
const key = apiKey?.trim() ?? "";
|
|
1278
1490
|
if (!key) {
|
|
1279
1491
|
throw new FoirRequestError(
|
|
1280
|
-
`[foir] createEventsClient: apiKey is empty
|
|
1492
|
+
`[foir] createEventsClient: apiKey is empty. Pass an admin key holding ${FOIR_EVENTS_SCOPE}.`
|
|
1281
1493
|
);
|
|
1282
1494
|
}
|
|
1283
|
-
|
|
1495
|
+
const kind = foirKeyKind(key);
|
|
1496
|
+
if (kind === "publishable" || kind === "server") {
|
|
1284
1497
|
throw new FoirRequestError(
|
|
1285
|
-
`[foir] createEventsClient: the event log is an OPERATOR surface and a
|
|
1498
|
+
`[foir] createEventsClient: the event log is an OPERATOR surface and a ${kind} key can never hold ${FOIR_EVENTS_SCOPE}: it is not mintable on an app's key. Use an admin key from a machine of your own. The log names which customer acted on records that whoever reads an app's key cannot themselves read.`
|
|
1286
1499
|
);
|
|
1287
1500
|
}
|
|
1288
|
-
if (
|
|
1501
|
+
if (kind === void 0) {
|
|
1289
1502
|
throw new FoirRequestError(
|
|
1290
|
-
`[foir] createEventsClient: apiKey must be an
|
|
1503
|
+
`[foir] createEventsClient: apiKey must be an admin key, got a key starting "${key.slice(0, 4)}". The event log is reachable by an admin key or an admin session only.`
|
|
1291
1504
|
);
|
|
1292
1505
|
}
|
|
1293
1506
|
return key;
|
|
@@ -1529,6 +1742,7 @@ export {
|
|
|
1529
1742
|
parseContextHeader,
|
|
1530
1743
|
parseSubject,
|
|
1531
1744
|
readDispatch,
|
|
1745
|
+
recordsCreateModels,
|
|
1532
1746
|
recordsReadModels,
|
|
1533
1747
|
recordsWriteModels,
|
|
1534
1748
|
requireScopedToken,
|
|
@@ -273,8 +273,8 @@ export interface FoirKeyHeaderInput {
|
|
|
273
273
|
* freshly pushed model changes (new fields, new models) are visible at
|
|
274
274
|
* runtime — otherwise selections on draft-only fields are silently stripped
|
|
275
275
|
* and surface as confusing "fragment never used" errors. The header has no
|
|
276
|
-
* effect under
|
|
277
|
-
* whenever the caller is in preview.
|
|
276
|
+
* effect under an app's key without `drafts:read` (the server ignores it),
|
|
277
|
+
* so it is safe to send whenever the caller is in preview.
|
|
278
278
|
*/
|
|
279
279
|
draftChannel?: boolean;
|
|
280
280
|
bearerToken?: string;
|
|
@@ -343,8 +343,28 @@ export declare function refuseCredentialHeaders(extra: Readonly<Record<string, s
|
|
|
343
343
|
/** The header a server names the visitor it calls for in. */
|
|
344
344
|
export declare const FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
345
345
|
/**
|
|
346
|
-
* The
|
|
347
|
-
*
|
|
346
|
+
* The visitor a server rendering pages is calling for: who they are to the
|
|
347
|
+
* per-visitor limits, and where they are to location targeting.
|
|
348
|
+
*
|
|
349
|
+
* Each part is what your host says of the visitor, and each is optional. A
|
|
350
|
+
* part left out is not said: with no address the request counts as your
|
|
351
|
+
* server's own, and with no country and no coordinates the visitor is one
|
|
352
|
+
* nobody could place. Where your server is never stands in for where the
|
|
353
|
+
* visitor is.
|
|
354
|
+
*/
|
|
355
|
+
export interface FoirVisitor {
|
|
356
|
+
/** The visitor's address, sent as `X-Foir-Visitor-IP`. */
|
|
357
|
+
address?: string | null;
|
|
358
|
+
/** The visitor's country, as an ISO 3166-1 alpha-2 code such as `GB`. */
|
|
359
|
+
country?: string | null;
|
|
360
|
+
/** The visitor's latitude, in decimal degrees. Sent with `longitude`, or not at all. */
|
|
361
|
+
latitude?: number | null;
|
|
362
|
+
/** The visitor's longitude, in decimal degrees. Sent with `latitude`, or not at all. */
|
|
363
|
+
longitude?: number | null;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* The visitor a server rendering pages is calling for, or a function that
|
|
367
|
+
* resolves them for each request. A string is the visitor's address alone.
|
|
348
368
|
*
|
|
349
369
|
* The SDK does not read it off a request, because which request header holds
|
|
350
370
|
* the visitor's real address, and whether a visitor can forge it, depends on
|
|
@@ -352,7 +372,26 @@ export declare const FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
|
352
372
|
* strips; that value, and only that, belongs here. A function that yields
|
|
353
373
|
* nothing sends nothing, and the API counts the request as your server's own.
|
|
354
374
|
*/
|
|
355
|
-
export type FoirVisitorSource = string | (() => string | null | undefined | Promise<string | null | undefined>);
|
|
375
|
+
export type FoirVisitorSource = string | FoirVisitor | (() => string | FoirVisitor | null | undefined | Promise<string | FoirVisitor | null | undefined>);
|
|
376
|
+
/** Where a stated visitor is, as a read's `contexts` carries it. */
|
|
377
|
+
export interface FoirVisitorPlace {
|
|
378
|
+
country?: string;
|
|
379
|
+
location?: {
|
|
380
|
+
latitude: number;
|
|
381
|
+
longitude: number;
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
/** One request's visitor, resolved: the address for the header, and the place for the read. */
|
|
385
|
+
export interface FoirResolvedVisitor {
|
|
386
|
+
address?: string;
|
|
387
|
+
place: FoirVisitorPlace;
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Resolve a `visitor` for one request. The provider is called here, once, and
|
|
391
|
+
* what it yields is used for the header and for the read alike, so the two
|
|
392
|
+
* never describe different visitors.
|
|
393
|
+
*/
|
|
394
|
+
export declare function resolveFoirVisitor(source: FoirVisitorSource | undefined): Promise<FoirResolvedVisitor>;
|
|
356
395
|
export declare function buildFoirHeaders(input: FoirHeaderInput): Record<string, string>;
|
|
357
396
|
/**
|
|
358
397
|
* Build a `/v1/...` URL from whatever the caller put in `apiUrl`.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The kind a key says it is, by its prefix.
|
|
3
|
+
*
|
|
4
|
+
* A key is one of three kinds, chosen by whoever made it. A publishable key
|
|
5
|
+
* sits in a page or an app. A server key is held by the server that renders an
|
|
6
|
+
* app's pages. An admin key is a machine's own: a sync job, a pipeline, a tool.
|
|
7
|
+
* A key says its kind in its first letters, so this is decided without a
|
|
8
|
+
* request.
|
|
9
|
+
*
|
|
10
|
+
* A key minted before a key had a kind says an older type. A `pk_` key is a
|
|
11
|
+
* publishable key. An `sk_` or a `tc_` key says only that it is not public:
|
|
12
|
+
* the platform holds it as an admin key, and it is reported here as `older`,
|
|
13
|
+
* so that a caller who refuses an admin key by name does not refuse a key
|
|
14
|
+
* whose holder has not yet been able to mint a server key in its place.
|
|
15
|
+
*/
|
|
16
|
+
export type FoirKeyKind = 'publishable' | 'server' | 'admin';
|
|
17
|
+
/** What a key's prefix says of it: its kind, or that it is an older secret. */
|
|
18
|
+
export type FoirKeyPrefixKind = FoirKeyKind | 'older';
|
|
19
|
+
/** Where each credential is for, and which key an app holds. */
|
|
20
|
+
export declare const FOIR_CREDENTIALS_DOC = "https://docs.foir.dev/credentials";
|
|
21
|
+
/**
|
|
22
|
+
* What a key says of itself by its prefix, or `undefined` for a string that
|
|
23
|
+
* is no key Fóir made.
|
|
24
|
+
*/
|
|
25
|
+
export declare function foirKeyKind(apiKey: string | null | undefined): FoirKeyPrefixKind | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* What an app's client says when it is handed an admin key, naming the client
|
|
28
|
+
* it was handed to. An admin key is a machine's own, so every client an app
|
|
29
|
+
* builds refuses one before any request, in the same words.
|
|
30
|
+
*
|
|
31
|
+
* Only a key that says it is an admin key by its prefix is refused. A key
|
|
32
|
+
* minted before a key had a kind does not say, and is let through.
|
|
33
|
+
*/
|
|
34
|
+
export declare function adminKeyRefusal(client: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Whether a key may be named in a URL. Only a publishable key may: it ships in
|
|
37
|
+
* the page and is served to every visitor, so naming it discloses nothing. Any
|
|
38
|
+
* other key would land in access logs, referrer headers and browser history.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isPublishableFoirKey(apiKey: string | null | undefined): boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foir/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Official Foir SDK — editor iframe client + server-side platform helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -177,18 +177,19 @@
|
|
|
177
177
|
"ws": "^8.18.0",
|
|
178
178
|
"y-websocket": "^3.0.0",
|
|
179
179
|
"yjs": "^13.6.30",
|
|
180
|
+
"@foir/access-vocab": "0.0.0",
|
|
180
181
|
"@foir/proto-ts": "0.109.0"
|
|
181
182
|
},
|
|
182
183
|
"engines": {
|
|
183
184
|
"node": ">=18.0.0"
|
|
184
185
|
},
|
|
185
186
|
"scripts": {
|
|
186
|
-
"build": "node scripts/generate-app-vocabulary.mjs && node scripts/clean-dist.mjs && tsup && tsc -p tsconfig.build.json && node scripts/bundle-app-contract-types.mjs && node scripts/assert-export-types.mjs && node scripts/assert-public-exports.mjs",
|
|
187
|
+
"build": "node scripts/generate-app-vocabulary.mjs && node scripts/generate-grant-table.mjs && node scripts/clean-dist.mjs && tsup && tsc -p tsconfig.build.json && node scripts/bundle-app-contract-types.mjs && node scripts/assert-export-types.mjs && node scripts/assert-public-exports.mjs",
|
|
187
188
|
"snapshot:exports": "node scripts/assert-public-exports.mjs --update",
|
|
188
189
|
"dev": "node scripts/dev-watch.mjs",
|
|
189
|
-
"check-types": "node scripts/generate-app-vocabulary.mjs && tsc --noEmit",
|
|
190
|
-
"test": "node scripts/generate-app-vocabulary.mjs && vitest run",
|
|
191
|
-
"test:watch": "node scripts/generate-app-vocabulary.mjs && vitest watch",
|
|
190
|
+
"check-types": "node scripts/generate-app-vocabulary.mjs && node scripts/generate-grant-table.mjs && tsc --noEmit",
|
|
191
|
+
"test": "node scripts/generate-app-vocabulary.mjs && node scripts/generate-grant-table.mjs && vitest run",
|
|
192
|
+
"test:watch": "node scripts/generate-app-vocabulary.mjs && node scripts/generate-grant-table.mjs && vitest watch",
|
|
192
193
|
"release:patch": "pnpm version patch",
|
|
193
194
|
"release:minor": "pnpm version minor",
|
|
194
195
|
"release:major": "pnpm version major"
|