@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/react.js
CHANGED
|
@@ -14,6 +14,26 @@ import {
|
|
|
14
14
|
// src/graphql/foir-request.ts
|
|
15
15
|
import { print as print2 } from "graphql";
|
|
16
16
|
|
|
17
|
+
// src/transport/key-kind.ts
|
|
18
|
+
var KEY_PREFIXES = [
|
|
19
|
+
["foir_pub_", "publishable"],
|
|
20
|
+
["foir_srv_", "server"],
|
|
21
|
+
["foir_adm_", "admin"],
|
|
22
|
+
["pk_", "publishable"],
|
|
23
|
+
["sk_", "older"],
|
|
24
|
+
["tc_", "older"]
|
|
25
|
+
];
|
|
26
|
+
function foirKeyKind(apiKey) {
|
|
27
|
+
if (typeof apiKey !== "string") return void 0;
|
|
28
|
+
for (const [prefix, kind] of KEY_PREFIXES) {
|
|
29
|
+
if (apiKey.startsWith(prefix)) return kind;
|
|
30
|
+
}
|
|
31
|
+
return void 0;
|
|
32
|
+
}
|
|
33
|
+
function isPublishableFoirKey(apiKey) {
|
|
34
|
+
return foirKeyKind(apiKey) === "publishable";
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
// src/graphql/cdn-route.ts
|
|
18
38
|
import { print } from "graphql";
|
|
19
39
|
|
|
@@ -117,6 +137,28 @@ function refuseCredentialHeaders(extra) {
|
|
|
117
137
|
}
|
|
118
138
|
}
|
|
119
139
|
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
140
|
+
function sentCoordinate(value) {
|
|
141
|
+
return Math.round(value * 100) / 100;
|
|
142
|
+
}
|
|
143
|
+
async function resolveFoirVisitor(source) {
|
|
144
|
+
const value = typeof source === "function" ? await source() : source;
|
|
145
|
+
if (value === null || value === void 0) return { place: {} };
|
|
146
|
+
if (typeof value === "string") {
|
|
147
|
+
const address2 = value.trim();
|
|
148
|
+
return { address: address2 || void 0, place: {} };
|
|
149
|
+
}
|
|
150
|
+
const address = value.address?.trim();
|
|
151
|
+
const country = value.country?.trim();
|
|
152
|
+
const place = {};
|
|
153
|
+
if (country) place.country = country;
|
|
154
|
+
if (Number.isFinite(value.latitude) && Number.isFinite(value.longitude)) {
|
|
155
|
+
place.location = {
|
|
156
|
+
latitude: sentCoordinate(value.latitude),
|
|
157
|
+
longitude: sentCoordinate(value.longitude)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return { address: address || void 0, place };
|
|
161
|
+
}
|
|
120
162
|
function buildFoirHeaders(input) {
|
|
121
163
|
const headers = {
|
|
122
164
|
"Content-Type": "application/json"
|
|
@@ -560,7 +602,7 @@ function toVariableValue(value, label) {
|
|
|
560
602
|
return value.toISOString();
|
|
561
603
|
}
|
|
562
604
|
if (Array.isArray(value)) {
|
|
563
|
-
return value.filter((item) => item !== void 0).map((item,
|
|
605
|
+
return value.filter((item) => item !== void 0).map((item, index2) => toVariableValue(item, `${label}[${index2}]`));
|
|
564
606
|
}
|
|
565
607
|
if (kind === "object") {
|
|
566
608
|
const out = {};
|
|
@@ -733,10 +775,7 @@ function foirDataEndpoint(apiUrl, path) {
|
|
|
733
775
|
return foirApiEndpoint(apiUrl, path);
|
|
734
776
|
}
|
|
735
777
|
async function resolveVisitorAddress(config) {
|
|
736
|
-
|
|
737
|
-
const value = typeof source === "function" ? await source() : source;
|
|
738
|
-
const address = value?.trim();
|
|
739
|
-
return address ? address : void 0;
|
|
778
|
+
return (await resolveFoirVisitor(config.visitor)).address;
|
|
740
779
|
}
|
|
741
780
|
async function resolveBearerToken(config) {
|
|
742
781
|
const source = config.bearerToken;
|
|
@@ -777,7 +816,7 @@ async function resolveFoirAuth(config) {
|
|
|
777
816
|
};
|
|
778
817
|
}
|
|
779
818
|
function foirRoute(input) {
|
|
780
|
-
if (!input.apiKey
|
|
819
|
+
if (!isPublishableFoirKey(input.apiKey)) return { lane: "origin", reason: "key" };
|
|
781
820
|
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
782
821
|
if (input.orgId || input.orgUrl) return { lane: "origin", reason: "org" };
|
|
783
822
|
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
@@ -915,6 +954,9 @@ var AUTH_OPS = {
|
|
|
915
954
|
// Post-credential self reads.
|
|
916
955
|
currentUser: "currentUser",
|
|
917
956
|
myProviderToken: "myProviderToken",
|
|
957
|
+
// What the calling credential may do, and for a signed-in customer the roles
|
|
958
|
+
// they hold. `./access` reads it.
|
|
959
|
+
whoami: "whoami",
|
|
918
960
|
// Methods.
|
|
919
961
|
customerLogin: "customerLogin",
|
|
920
962
|
customerRegister: "customerRegister",
|
|
@@ -1078,6 +1120,280 @@ async function refreshSession(adapter, env) {
|
|
|
1078
1120
|
return null;
|
|
1079
1121
|
}
|
|
1080
1122
|
}
|
|
1123
|
+
function isAuthExpiredError(err) {
|
|
1124
|
+
if (!(err instanceof FoirRequestError)) return false;
|
|
1125
|
+
if (err.status === 401) return true;
|
|
1126
|
+
return (err.graphQLErrors ?? []).some(
|
|
1127
|
+
(e) => /authentication required/i.test(e.message)
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
var refreshing = /* @__PURE__ */ new WeakMap();
|
|
1131
|
+
function refreshSessionOnce(adapter, env) {
|
|
1132
|
+
const running = refreshing.get(adapter);
|
|
1133
|
+
if (running) return running;
|
|
1134
|
+
const next = refreshSession(adapter, env).finally(() => refreshing.delete(adapter));
|
|
1135
|
+
refreshing.set(adapter, next);
|
|
1136
|
+
return next;
|
|
1137
|
+
}
|
|
1138
|
+
function sessionLapse(err) {
|
|
1139
|
+
if (err instanceof FoirAuthDowngradedError) {
|
|
1140
|
+
switch (err.downgrade.reason) {
|
|
1141
|
+
case "expired":
|
|
1142
|
+
return "refresh";
|
|
1143
|
+
case "invalid":
|
|
1144
|
+
return "expired";
|
|
1145
|
+
default:
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
return isAuthExpiredError(err) ? "refresh" : null;
|
|
1150
|
+
}
|
|
1151
|
+
async function withSessionRefresh(adapter, env, run) {
|
|
1152
|
+
const token = await getSessionToken(adapter);
|
|
1153
|
+
try {
|
|
1154
|
+
return await run(token ?? void 0);
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
const lapse = sessionLapse(err);
|
|
1157
|
+
if (!lapse) throw err;
|
|
1158
|
+
if (lapse === "expired") throw new FoirSessionExpiredError({ cause: err });
|
|
1159
|
+
const refreshed = await refreshSessionOnce(adapter, env);
|
|
1160
|
+
if (!refreshed) {
|
|
1161
|
+
if (err instanceof FoirAuthDowngradedError) throw new FoirSessionExpiredError({ cause: err });
|
|
1162
|
+
throw err;
|
|
1163
|
+
}
|
|
1164
|
+
try {
|
|
1165
|
+
return await run(refreshed.token);
|
|
1166
|
+
} catch (again) {
|
|
1167
|
+
if (again instanceof FoirAuthDowngradedError) throw new FoirSessionExpiredError({ cause: again });
|
|
1168
|
+
throw again;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
async function authedOp(adapter, env, op, args) {
|
|
1173
|
+
return withSessionRefresh(
|
|
1174
|
+
adapter,
|
|
1175
|
+
env,
|
|
1176
|
+
(bearerToken) => authOp(env, op, args, { bearerToken })
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// src/auth/grant-table.generated.ts
|
|
1181
|
+
var GRANT_TABLE = [
|
|
1182
|
+
["records:read", true, ["own", "all"], []],
|
|
1183
|
+
["records:create", true, [], []],
|
|
1184
|
+
["records:write", true, ["own", "all"], []],
|
|
1185
|
+
["records:delete", true, ["own", "all"], []],
|
|
1186
|
+
["records:publish", true, ["own", "all"], []],
|
|
1187
|
+
["records:attribute", true, [], []],
|
|
1188
|
+
["search:read", true, [], []],
|
|
1189
|
+
["embeddings:read", false, [], []],
|
|
1190
|
+
["embeddings:write", false, [], []],
|
|
1191
|
+
["schemas:read", false, [], []],
|
|
1192
|
+
["drafts:read", false, [], []],
|
|
1193
|
+
["files:read", false, ["own", "all"], []],
|
|
1194
|
+
["files:write", false, ["own", "all"], []],
|
|
1195
|
+
["files:delete", false, ["own", "all"], []],
|
|
1196
|
+
["notifications:read", false, ["own", "all"], []],
|
|
1197
|
+
["notifications:write", false, ["own", "named", "all"], []],
|
|
1198
|
+
["notifications:consent", false, [], []],
|
|
1199
|
+
["pushcredentials:read", false, [], []],
|
|
1200
|
+
["pushcredentials:write", false, [], []],
|
|
1201
|
+
["notes:read", false, ["own", "all"], []],
|
|
1202
|
+
["notes:write", false, ["own", "all"], []],
|
|
1203
|
+
["notes:create", false, [], []],
|
|
1204
|
+
["approvals:read", false, [], []],
|
|
1205
|
+
["approvals:write", false, [], []],
|
|
1206
|
+
["rooms:read", true, [], []],
|
|
1207
|
+
["rooms:write", true, [], []],
|
|
1208
|
+
["presence:read", true, [], []],
|
|
1209
|
+
["operations:read", false, [], []],
|
|
1210
|
+
["operations:execute", true, [], []],
|
|
1211
|
+
["operations:write", false, [], []],
|
|
1212
|
+
["operations:complete", false, [], []],
|
|
1213
|
+
["status:write", false, [], []],
|
|
1214
|
+
["appconfig:read", false, [], []],
|
|
1215
|
+
["schedules:read", false, [], []],
|
|
1216
|
+
["schedules:write", false, [], []],
|
|
1217
|
+
["hooks:read", false, [], []],
|
|
1218
|
+
["hooks:write", false, [], []],
|
|
1219
|
+
["relyingparties:read", false, [], []],
|
|
1220
|
+
["relyingparties:write", false, [], []],
|
|
1221
|
+
["domains:read", false, [], []],
|
|
1222
|
+
["domains:write", false, [], []],
|
|
1223
|
+
["secrets:read", false, [], []],
|
|
1224
|
+
["secrets:list", false, [], []],
|
|
1225
|
+
["secrets:put", false, [], []],
|
|
1226
|
+
["secrets:delete", false, [], []],
|
|
1227
|
+
["appsecrets:read", false, [], []],
|
|
1228
|
+
["appsecrets:list", false, [], []],
|
|
1229
|
+
["appsecrets:put", false, [], []],
|
|
1230
|
+
["appsecrets:delete", false, [], []],
|
|
1231
|
+
["opsecrets:read", false, [], []],
|
|
1232
|
+
["opsecrets:write", false, [], []],
|
|
1233
|
+
["settings:read", false, [], []],
|
|
1234
|
+
["settings:write", false, [], []],
|
|
1235
|
+
["designtokens:read", false, [], []],
|
|
1236
|
+
["designtokens:write", false, [], []],
|
|
1237
|
+
["models:read", false, [], []],
|
|
1238
|
+
["models:write", false, [], []],
|
|
1239
|
+
["configs:read", false, [], []],
|
|
1240
|
+
["configs:write", false, [], []],
|
|
1241
|
+
["apps:read", false, [], []],
|
|
1242
|
+
["apps:write", false, [], []],
|
|
1243
|
+
["events:read", false, [], []],
|
|
1244
|
+
["targeting:debug", false, [], []],
|
|
1245
|
+
["visitors:forward", false, [], []],
|
|
1246
|
+
["apikeys:read", false, [], []],
|
|
1247
|
+
["apikeys:write", false, [], []],
|
|
1248
|
+
["organizations:read", false, [], []],
|
|
1249
|
+
["organizations:write", false, [], []],
|
|
1250
|
+
["organizations:create", false, [], []],
|
|
1251
|
+
["organizations:delete", false, [], []],
|
|
1252
|
+
["organizations:reach", false, [], []],
|
|
1253
|
+
["members:read", false, [], []],
|
|
1254
|
+
["members:write", false, [], []],
|
|
1255
|
+
["roles:read", false, [], []],
|
|
1256
|
+
["roles:write", false, [], []],
|
|
1257
|
+
["projects:read", false, [], []],
|
|
1258
|
+
["projects:write", false, [], []],
|
|
1259
|
+
["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"]],
|
|
1260
|
+
["projects:create", false, [], []],
|
|
1261
|
+
["projects:delete", false, [], []],
|
|
1262
|
+
["tenants:read", false, [], []],
|
|
1263
|
+
["tenants:write", false, [], []],
|
|
1264
|
+
["tenants:delete", false, [], []],
|
|
1265
|
+
["audit:read", false, [], []],
|
|
1266
|
+
["export:run", false, [], []],
|
|
1267
|
+
["billing:read", false, [], []],
|
|
1268
|
+
["billing:manage", false, [], []],
|
|
1269
|
+
["usage:read", false, [], []],
|
|
1270
|
+
["controlkeys:read", false, [], []],
|
|
1271
|
+
["controlkeys:write", false, [], []],
|
|
1272
|
+
["tenanthooks:read", false, [], []],
|
|
1273
|
+
["tenanthooks:write", false, [], []],
|
|
1274
|
+
["apps:install", false, [], []],
|
|
1275
|
+
["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"]],
|
|
1276
|
+
["platform:read", false, [], []],
|
|
1277
|
+
["platform:manage", false, [], ["tenant:manage", "platform:read", "platform:create_workspace", "users:setup"]],
|
|
1278
|
+
["platform:create_workspace", false, [], []],
|
|
1279
|
+
["users:read", false, ["own", "named", "all"], []],
|
|
1280
|
+
["users:write", false, ["own", "all"], []],
|
|
1281
|
+
["users:setup", false, [], []],
|
|
1282
|
+
["users:delete", false, ["own", "all"], []],
|
|
1283
|
+
["users:create", false, [], []],
|
|
1284
|
+
["identities:read", false, [], []],
|
|
1285
|
+
["identities:delete", false, [], []],
|
|
1286
|
+
["usersecrets:read", false, [], []],
|
|
1287
|
+
["usersecrets:list", false, [], []],
|
|
1288
|
+
["usersecrets:put", false, [], []],
|
|
1289
|
+
["usersecrets:delete", false, [], []],
|
|
1290
|
+
["auth:login_with_foir", false, [], []],
|
|
1291
|
+
["signinproviders:read", false, [], []],
|
|
1292
|
+
["signinproviders:write", false, [], []]
|
|
1293
|
+
];
|
|
1294
|
+
|
|
1295
|
+
// src/auth/grants.ts
|
|
1296
|
+
var WILDCARD = "*";
|
|
1297
|
+
var RESOURCE_WILDCARD = ":*";
|
|
1298
|
+
var index;
|
|
1299
|
+
function catalogue() {
|
|
1300
|
+
if (!index) {
|
|
1301
|
+
const byName = new Map(GRANT_TABLE.map((row) => [row[0], row]));
|
|
1302
|
+
const longestFirst = [...byName.keys()].sort(
|
|
1303
|
+
(a, b) => a.length !== b.length ? b.length - a.length : a < b ? -1 : 1
|
|
1304
|
+
);
|
|
1305
|
+
index = { byName, longestFirst };
|
|
1306
|
+
}
|
|
1307
|
+
return index;
|
|
1308
|
+
}
|
|
1309
|
+
var reachRank = { own: 1, named: 2, all: 3 };
|
|
1310
|
+
function hasReach(permission, reach) {
|
|
1311
|
+
const reaches = permission[2];
|
|
1312
|
+
return reaches.length === 0 ? reach === "own" : reaches.includes(reach);
|
|
1313
|
+
}
|
|
1314
|
+
function resourceOf(name) {
|
|
1315
|
+
const i = name.indexOf(":");
|
|
1316
|
+
return i < 0 ? name : name.slice(0, i);
|
|
1317
|
+
}
|
|
1318
|
+
function parseGrant(s) {
|
|
1319
|
+
const { byName, longestFirst } = catalogue();
|
|
1320
|
+
for (const name of longestFirst) {
|
|
1321
|
+
if (!s.startsWith(name)) continue;
|
|
1322
|
+
let rest = s.slice(name.length);
|
|
1323
|
+
if (rest !== "" && rest[0] !== "." && rest[0] !== ":") continue;
|
|
1324
|
+
const permission = byName.get(name);
|
|
1325
|
+
let reach = "own";
|
|
1326
|
+
if (rest.startsWith(".")) {
|
|
1327
|
+
const after = rest.slice(1);
|
|
1328
|
+
const colon = after.indexOf(":");
|
|
1329
|
+
const word = colon < 0 ? after : after.slice(0, colon);
|
|
1330
|
+
if (word !== "named" && word !== "all") return null;
|
|
1331
|
+
if (!hasReach(permission, word)) return null;
|
|
1332
|
+
reach = word;
|
|
1333
|
+
rest = after.slice(word.length);
|
|
1334
|
+
}
|
|
1335
|
+
let target = "";
|
|
1336
|
+
if (rest.startsWith(":")) {
|
|
1337
|
+
target = rest.slice(1);
|
|
1338
|
+
if (!permission[1]) return null;
|
|
1339
|
+
if (target === "" || /[: \t\n*]/.test(target)) return null;
|
|
1340
|
+
}
|
|
1341
|
+
return { permission, reach, target };
|
|
1342
|
+
}
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
function covers(held, required) {
|
|
1346
|
+
return held.permission === required.permission && reachRank[held.reach] >= reachRank[required.reach] && (held.target === "" || held.target === required.target);
|
|
1347
|
+
}
|
|
1348
|
+
function resourceWildcard(held) {
|
|
1349
|
+
if (!held.endsWith(RESOURCE_WILDCARD)) return null;
|
|
1350
|
+
const resource = held.slice(0, -RESOURCE_WILDCARD.length);
|
|
1351
|
+
return resource.includes(":") ? null : resource;
|
|
1352
|
+
}
|
|
1353
|
+
function holdsAt(held, required, depth) {
|
|
1354
|
+
for (const h of held) {
|
|
1355
|
+
if (h === WILDCARD) return true;
|
|
1356
|
+
const resource = resourceWildcard(h);
|
|
1357
|
+
if (resource !== null) {
|
|
1358
|
+
if (resource === resourceOf(required.permission[0])) return true;
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
const grant = parseGrant(h);
|
|
1362
|
+
if (grant === null) continue;
|
|
1363
|
+
if (covers(grant, required)) return true;
|
|
1364
|
+
const implies = grant.permission[3];
|
|
1365
|
+
if (depth < 4 && implies.length > 0 && holdsAt(implies, required, depth + 1)) return true;
|
|
1366
|
+
}
|
|
1367
|
+
return false;
|
|
1368
|
+
}
|
|
1369
|
+
function holds(held, required) {
|
|
1370
|
+
const grant = parseGrant(required);
|
|
1371
|
+
return grant !== null && holdsAt(held, grant, 0);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// src/auth/access.ts
|
|
1375
|
+
function accessFrom(who) {
|
|
1376
|
+
const customer = who?.credential?.customer;
|
|
1377
|
+
if (!customer?.id) return null;
|
|
1378
|
+
return {
|
|
1379
|
+
customerId: customer.id,
|
|
1380
|
+
roles: Array.isArray(customer.roles) ? [...customer.roles] : null,
|
|
1381
|
+
scopes: [...who?.authority?.effective ?? []]
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
async function customerAccess(env, options) {
|
|
1385
|
+
return accessFrom(await authOp(env, AUTH_OPS.whoami, void 0, options));
|
|
1386
|
+
}
|
|
1387
|
+
async function getSessionAccess(adapter, env) {
|
|
1388
|
+
if (!await getSessionToken(adapter)) return null;
|
|
1389
|
+
return accessFrom(await authedOp(adapter, env, AUTH_OPS.whoami));
|
|
1390
|
+
}
|
|
1391
|
+
function hasRole(access, key) {
|
|
1392
|
+
return access?.roles?.includes(key) ?? false;
|
|
1393
|
+
}
|
|
1394
|
+
function hasScope(access, scope) {
|
|
1395
|
+
return access ? holds(access.scopes, scope) : false;
|
|
1396
|
+
}
|
|
1081
1397
|
|
|
1082
1398
|
// src/auth/session-client.ts
|
|
1083
1399
|
var DEFAULT_SESSION_PATH = "/session/token";
|
|
@@ -1254,9 +1570,16 @@ function FoirProvider({
|
|
|
1254
1570
|
if (adapter && adapter !== adapterRef.current) adapterRef.current = adapter;
|
|
1255
1571
|
const [status, setStatus] = useState("loading");
|
|
1256
1572
|
const [customer, setCustomer] = useState(null);
|
|
1573
|
+
const [access, setAccess] = useState(null);
|
|
1574
|
+
const [accessStatus, setAccessStatus] = useState("idle");
|
|
1575
|
+
const accessRead = useRef(0);
|
|
1576
|
+
const accessPending = useRef(null);
|
|
1257
1577
|
const apply = useCallback((c) => {
|
|
1258
1578
|
setCustomer(c);
|
|
1259
1579
|
setStatus(c ? "authenticated" : "unauthenticated");
|
|
1580
|
+
accessRead.current += 1;
|
|
1581
|
+
setAccess(null);
|
|
1582
|
+
setAccessStatus("idle");
|
|
1260
1583
|
}, []);
|
|
1261
1584
|
const loginOrigin = hostedSession?.loginOrigin;
|
|
1262
1585
|
const sessionPath = hostedSession?.sessionPath;
|
|
@@ -1381,6 +1704,39 @@ function FoirProvider({
|
|
|
1381
1704
|
() => hosted ? hosted.getToken() : getSessionToken(adapterRef.current),
|
|
1382
1705
|
[hosted]
|
|
1383
1706
|
);
|
|
1707
|
+
const loadAccess = useCallback(() => {
|
|
1708
|
+
const read = accessRead.current += 1;
|
|
1709
|
+
setAccessStatus((was) => was === "ready" ? was : "loading");
|
|
1710
|
+
const pending = (async () => {
|
|
1711
|
+
try {
|
|
1712
|
+
const next = hosted ? await hosted.getToken().then(
|
|
1713
|
+
(bearerToken) => bearerToken ? customerAccess(envRef.current, { bearerToken }) : null
|
|
1714
|
+
) : await getSessionAccess(adapterRef.current, envRef.current);
|
|
1715
|
+
if (accessRead.current !== read) return;
|
|
1716
|
+
setAccess(next);
|
|
1717
|
+
setAccessStatus("ready");
|
|
1718
|
+
} catch {
|
|
1719
|
+
if (accessRead.current !== read) return;
|
|
1720
|
+
setAccess(null);
|
|
1721
|
+
setAccessStatus("unavailable");
|
|
1722
|
+
}
|
|
1723
|
+
})();
|
|
1724
|
+
accessPending.current = pending;
|
|
1725
|
+
void pending.finally(() => {
|
|
1726
|
+
if (accessPending.current === pending) accessPending.current = null;
|
|
1727
|
+
});
|
|
1728
|
+
return pending;
|
|
1729
|
+
}, [hosted]);
|
|
1730
|
+
const ensureAccess = useCallback(
|
|
1731
|
+
() => accessPending.current ?? loadAccess(),
|
|
1732
|
+
[loadAccess]
|
|
1733
|
+
);
|
|
1734
|
+
const accessWasRead = accessStatus !== "idle";
|
|
1735
|
+
const refreshWithAccess = useCallback(async () => {
|
|
1736
|
+
const ok = await refresh();
|
|
1737
|
+
if (ok && accessWasRead) await loadAccess();
|
|
1738
|
+
return ok;
|
|
1739
|
+
}, [refresh, loadAccess, accessWasRead]);
|
|
1384
1740
|
const getProviders = useCallback(() => getAuthProviders(envRef.current), []);
|
|
1385
1741
|
const hostedSignInUrl = useMemo(
|
|
1386
1742
|
() => hosted ? (returnTo) => hosted.signInUrl(returnTo) : null,
|
|
@@ -1390,6 +1746,10 @@ function FoirProvider({
|
|
|
1390
1746
|
() => ({
|
|
1391
1747
|
status,
|
|
1392
1748
|
customer,
|
|
1749
|
+
access,
|
|
1750
|
+
accessStatus,
|
|
1751
|
+
loadAccess,
|
|
1752
|
+
ensureAccess,
|
|
1393
1753
|
env,
|
|
1394
1754
|
adapter: adapterRef.current,
|
|
1395
1755
|
signIn,
|
|
@@ -1399,7 +1759,7 @@ function FoirProvider({
|
|
|
1399
1759
|
signInWithProvider,
|
|
1400
1760
|
completeProviderLogin,
|
|
1401
1761
|
signOut: signOut2,
|
|
1402
|
-
refresh,
|
|
1762
|
+
refresh: refreshWithAccess,
|
|
1403
1763
|
getToken,
|
|
1404
1764
|
getProviders,
|
|
1405
1765
|
hostedSignInUrl
|
|
@@ -1407,6 +1767,10 @@ function FoirProvider({
|
|
|
1407
1767
|
[
|
|
1408
1768
|
status,
|
|
1409
1769
|
customer,
|
|
1770
|
+
access,
|
|
1771
|
+
accessStatus,
|
|
1772
|
+
loadAccess,
|
|
1773
|
+
ensureAccess,
|
|
1410
1774
|
env,
|
|
1411
1775
|
signIn,
|
|
1412
1776
|
register,
|
|
@@ -1415,7 +1779,7 @@ function FoirProvider({
|
|
|
1415
1779
|
signInWithProvider,
|
|
1416
1780
|
completeProviderLogin,
|
|
1417
1781
|
signOut2,
|
|
1418
|
-
|
|
1782
|
+
refreshWithAccess,
|
|
1419
1783
|
getToken,
|
|
1420
1784
|
getProviders,
|
|
1421
1785
|
hostedSignInUrl
|
|
@@ -1442,6 +1806,24 @@ function useFoirSession() {
|
|
|
1442
1806
|
function useFoirUser() {
|
|
1443
1807
|
return useFoirContext().customer;
|
|
1444
1808
|
}
|
|
1809
|
+
function useFoirAccess() {
|
|
1810
|
+
const { status, access, accessStatus, loadAccess, ensureAccess } = useFoirContext();
|
|
1811
|
+
useEffect(() => {
|
|
1812
|
+
if (status === "authenticated" && accessStatus === "idle") void ensureAccess();
|
|
1813
|
+
}, [status, accessStatus, ensureAccess]);
|
|
1814
|
+
const settled = status === "loading" ? "loading" : status === "unauthenticated" ? "ready" : accessStatus === "idle" ? "loading" : accessStatus;
|
|
1815
|
+
return useMemo(
|
|
1816
|
+
() => ({
|
|
1817
|
+
status: settled,
|
|
1818
|
+
isLoading: settled === "loading",
|
|
1819
|
+
access,
|
|
1820
|
+
hasRole: (key) => hasRole(access, key),
|
|
1821
|
+
hasScope: (scope) => hasScope(access, scope),
|
|
1822
|
+
reload: loadAccess
|
|
1823
|
+
}),
|
|
1824
|
+
[settled, access, loadAccess]
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1445
1827
|
function useFoirAuth() {
|
|
1446
1828
|
const {
|
|
1447
1829
|
signIn,
|
|
@@ -2280,6 +2662,7 @@ export {
|
|
|
2280
2662
|
SignIn,
|
|
2281
2663
|
UserButton,
|
|
2282
2664
|
createLocalStorageAdapter,
|
|
2665
|
+
useFoirAccess,
|
|
2283
2666
|
useFoirAuth,
|
|
2284
2667
|
useFoirSession,
|
|
2285
2668
|
useFoirUser
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* const events = createEventsClient({
|
|
9
9
|
* apiUrl: process.env.PLATFORM_API_URL!,
|
|
10
|
-
* apiKey: process.env.FOIR_SECRET_KEY!, // sk_…, holding events:
|
|
10
|
+
* apiKey: process.env.FOIR_SECRET_KEY!, // sk_…, holding events:read
|
|
11
11
|
* });
|
|
12
12
|
*
|
|
13
13
|
* const page = await events.list({ after: lastSeq, limit: 500 });
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* DECIDED 2026-08-04. The event log is reachable by an `sk_` api key
|
|
25
25
|
* and by an admin session. It is NOT reachable by a customer token, there is no
|
|
26
26
|
* customer lane for it on api-public, and a `pk_` key can never be minted
|
|
27
|
-
* holding `events:
|
|
27
|
+
* holding `events:read` at all.
|
|
28
28
|
*
|
|
29
29
|
* The reason is a predicate asymmetry rather than a policy preference.
|
|
30
30
|
* `events.view` is plain tenant+project containment; `record.view` carries
|
|
@@ -81,7 +81,7 @@ export declare const FOIR_EVENTS_PATH = "/v1/events";
|
|
|
81
81
|
* absent from the platform's `publicSafeScopeResources` — and never present in a
|
|
82
82
|
* customer's resolved authority.
|
|
83
83
|
*/
|
|
84
|
-
export declare const FOIR_EVENTS_SCOPE = "events:
|
|
84
|
+
export declare const FOIR_EVENTS_SCOPE = "events:read";
|
|
85
85
|
/**
|
|
86
86
|
* The flat retention window, in days. Reported on every page as
|
|
87
87
|
* {@link FoirEventRetention.windowDays} too; this constant is the one to compare
|
|
@@ -200,16 +200,16 @@ export interface FoirEventsClientConfig {
|
|
|
200
200
|
*/
|
|
201
201
|
apiUrl: string;
|
|
202
202
|
/**
|
|
203
|
-
* An
|
|
203
|
+
* An admin key holding `events:read`.
|
|
204
204
|
*
|
|
205
|
-
* A
|
|
206
|
-
* note at the top of this file.
|
|
205
|
+
* A publishable or a server key is refused here rather than at the server:
|
|
206
|
+
* see the operator note at the top of this file.
|
|
207
207
|
*/
|
|
208
208
|
apiKey: string;
|
|
209
209
|
/**
|
|
210
210
|
* Extra headers. An `Authorization` header is refused: a customer bearer
|
|
211
211
|
* flips api-public's resolved authority to the customer, and no customer
|
|
212
|
-
* authority can contain `events:
|
|
212
|
+
* authority can contain `events:read`.
|
|
213
213
|
*/
|
|
214
214
|
headers?: Readonly<Record<string, string>>;
|
|
215
215
|
/** `fetch` implementation. Defaults to `globalThis.fetch`. */
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createServerClient, ServerClientError } from './server-client';
|
|
2
2
|
export { verifyWebhookSignature } from './webhook-helpers';
|
|
3
|
-
export { verifyScopedToken, parseContextHeader, parseSubject, recordsReadModels, recordsWriteModels, type ScopedTokenClaims, type ScopedTokenSubject, type VerifyScopedTokenOptions, } from './verify-scoped-token';
|
|
3
|
+
export { verifyScopedToken, parseContextHeader, parseSubject, recordsReadModels, recordsWriteModels, recordsCreateModels, type ScopedTokenClaims, type ScopedTokenSubject, type VerifyScopedTokenOptions, } from './verify-scoped-token';
|
|
4
4
|
export { createScopedTokenVerifier, ScopedTokenUnavailableError, type ScopedTokenVerifier, type ScopedTokenVerifierOptions, type ScopedTokenVerifierStats, } from './verify-scoped-token';
|
|
5
5
|
export { requireScopedToken, ScopedTokenError, FOIR_TOKEN_HEADER, FOIR_CONTEXT_HEADER, type RequireScopedTokenOptions, type ScopedTokenIngress, } from './require-scoped-token';
|
|
6
6
|
export { readDispatch, OperationPayloadError, type BodyCarrier, } from './read-dispatch';
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* The verified token is also the endpoint's credential for calling Foir back.
|
|
20
20
|
* `dispatch` on the result is the token as a data-client credential, so the
|
|
21
|
-
* endpoint acts as the operation, with the
|
|
21
|
+
* endpoint acts as the operation, with the grants the operation
|
|
22
22
|
* declared, and holds no key of its own. `generated` is the module
|
|
23
23
|
* `foir types --operation <key>` wrote for the operation:
|
|
24
24
|
*
|
|
@@ -25,7 +25,7 @@ export declare class ServerClientError extends Error {
|
|
|
25
25
|
*
|
|
26
26
|
* const client = createServerClient({
|
|
27
27
|
* baseUrl: 'https://api.example.com',
|
|
28
|
-
* apiKey:
|
|
28
|
+
* apiKey: process.env.FOIR_ADMIN_KEY!, // foir_adm_...
|
|
29
29
|
* });
|
|
30
30
|
*
|
|
31
31
|
* const { data } = await client.query('query { models { items { key } } }');
|
|
@@ -24,26 +24,19 @@ export interface ScopedTokenClaims {
|
|
|
24
24
|
* {@link parseSubject} to split into a typed object.
|
|
25
25
|
*/
|
|
26
26
|
sub: string;
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* What the token carries, as grants in the permission catalogue's names:
|
|
29
|
+
* `records:read:<model>`, `records:write:<model>`,
|
|
30
|
+
* `records:create:<model>`, `users:create` and the rest. It is the whole of
|
|
31
|
+
* what the token may do. A token that does not say its grants is refused, so
|
|
32
|
+
* this is always set.
|
|
33
|
+
*/
|
|
34
|
+
grants: Array<string>;
|
|
28
35
|
jti: string;
|
|
29
36
|
exp: number;
|
|
30
37
|
nbf: number;
|
|
31
38
|
iat: number;
|
|
32
39
|
ctx?: Record<string, string>;
|
|
33
|
-
/**
|
|
34
|
-
* Records-read model allow-list. Present when the operation declared one
|
|
35
|
-
* or more `records:read:<model>` capabilities. Pair with the
|
|
36
|
-
* `records:read` marker in {@link ScopedTokenClaims.cap} to authorize a
|
|
37
|
-
* record read for a specific model.
|
|
38
|
-
*/
|
|
39
|
-
rrm?: Array<string>;
|
|
40
|
-
/**
|
|
41
|
-
* Records-write model allow-list. Present when the operation declared
|
|
42
|
-
* one or more `records:write:<model>` capabilities. Pair with the
|
|
43
|
-
* `records:write` marker in {@link ScopedTokenClaims.cap} to authorize
|
|
44
|
-
* a record write for a specific model.
|
|
45
|
-
*/
|
|
46
|
-
rwm?: Array<string>;
|
|
47
40
|
/**
|
|
48
41
|
* The customer the token acts for, when it acts for one: the signed-in
|
|
49
42
|
* customer who ran the operation, or the owner of the record whose event
|
|
@@ -77,19 +70,21 @@ export interface ScopedTokenSubject {
|
|
|
77
70
|
*/
|
|
78
71
|
export declare function parseSubject(sub: string): ScopedTokenSubject;
|
|
79
72
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* capabilities. Convenience over reading {@link ScopedTokenClaims.rrm}
|
|
83
|
-
* directly, mirroring the platform's
|
|
84
|
-
* `scopedtoken.Claims.RecordsReadModels` field.
|
|
73
|
+
* The models a verified token may read records of: each model a
|
|
74
|
+
* `records:read:<model>` grant names. Empty when it names none.
|
|
85
75
|
*/
|
|
86
76
|
export declare function recordsReadModels(claims: ScopedTokenClaims): Array<string>;
|
|
87
77
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
78
|
+
* The models a verified token may change records of: each model a
|
|
79
|
+
* `records:write:<model>` grant names. Changing a record and making one are
|
|
80
|
+
* two permissions: see {@link recordsCreateModels}.
|
|
91
81
|
*/
|
|
92
82
|
export declare function recordsWriteModels(claims: ScopedTokenClaims): Array<string>;
|
|
83
|
+
/**
|
|
84
|
+
* The models a verified token may make records of: each model a
|
|
85
|
+
* `records:create:<model>` grant names.
|
|
86
|
+
*/
|
|
87
|
+
export declare function recordsCreateModels(claims: ScopedTokenClaims): Array<string>;
|
|
93
88
|
export interface VerifyScopedTokenOptions {
|
|
94
89
|
/**
|
|
95
90
|
* Expected `iss` claim. Defaults to `'foir-platform'`, the issuer the
|