@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/placement.js
CHANGED
|
@@ -1,3 +1,197 @@
|
|
|
1
|
+
// src/auth/grant-table.generated.ts
|
|
2
|
+
var GRANT_TABLE = [
|
|
3
|
+
["records:read", true, ["own", "all"], []],
|
|
4
|
+
["records:create", true, [], []],
|
|
5
|
+
["records:write", true, ["own", "all"], []],
|
|
6
|
+
["records:delete", true, ["own", "all"], []],
|
|
7
|
+
["records:publish", true, ["own", "all"], []],
|
|
8
|
+
["records:attribute", true, [], []],
|
|
9
|
+
["search:read", true, [], []],
|
|
10
|
+
["embeddings:read", false, [], []],
|
|
11
|
+
["embeddings:write", false, [], []],
|
|
12
|
+
["schemas:read", false, [], []],
|
|
13
|
+
["drafts:read", false, [], []],
|
|
14
|
+
["files:read", false, ["own", "all"], []],
|
|
15
|
+
["files:write", false, ["own", "all"], []],
|
|
16
|
+
["files:delete", false, ["own", "all"], []],
|
|
17
|
+
["notifications:read", false, ["own", "all"], []],
|
|
18
|
+
["notifications:write", false, ["own", "named", "all"], []],
|
|
19
|
+
["notifications:consent", false, [], []],
|
|
20
|
+
["pushcredentials:read", false, [], []],
|
|
21
|
+
["pushcredentials:write", false, [], []],
|
|
22
|
+
["notes:read", false, ["own", "all"], []],
|
|
23
|
+
["notes:write", false, ["own", "all"], []],
|
|
24
|
+
["notes:create", false, [], []],
|
|
25
|
+
["approvals:read", false, [], []],
|
|
26
|
+
["approvals:write", false, [], []],
|
|
27
|
+
["rooms:read", true, [], []],
|
|
28
|
+
["rooms:write", true, [], []],
|
|
29
|
+
["presence:read", true, [], []],
|
|
30
|
+
["operations:read", false, [], []],
|
|
31
|
+
["operations:execute", true, [], []],
|
|
32
|
+
["operations:write", false, [], []],
|
|
33
|
+
["operations:complete", false, [], []],
|
|
34
|
+
["status:write", false, [], []],
|
|
35
|
+
["appconfig:read", false, [], []],
|
|
36
|
+
["schedules:read", false, [], []],
|
|
37
|
+
["schedules:write", false, [], []],
|
|
38
|
+
["hooks:read", false, [], []],
|
|
39
|
+
["hooks:write", false, [], []],
|
|
40
|
+
["relyingparties:read", false, [], []],
|
|
41
|
+
["relyingparties:write", false, [], []],
|
|
42
|
+
["domains:read", false, [], []],
|
|
43
|
+
["domains:write", false, [], []],
|
|
44
|
+
["secrets:read", false, [], []],
|
|
45
|
+
["secrets:list", false, [], []],
|
|
46
|
+
["secrets:put", false, [], []],
|
|
47
|
+
["secrets:delete", false, [], []],
|
|
48
|
+
["appsecrets:read", false, [], []],
|
|
49
|
+
["appsecrets:list", false, [], []],
|
|
50
|
+
["appsecrets:put", false, [], []],
|
|
51
|
+
["appsecrets:delete", false, [], []],
|
|
52
|
+
["opsecrets:read", false, [], []],
|
|
53
|
+
["opsecrets:write", false, [], []],
|
|
54
|
+
["settings:read", false, [], []],
|
|
55
|
+
["settings:write", false, [], []],
|
|
56
|
+
["designtokens:read", false, [], []],
|
|
57
|
+
["designtokens:write", false, [], []],
|
|
58
|
+
["models:read", false, [], []],
|
|
59
|
+
["models:write", false, [], []],
|
|
60
|
+
["configs:read", false, [], []],
|
|
61
|
+
["configs:write", false, [], []],
|
|
62
|
+
["apps:read", false, [], []],
|
|
63
|
+
["apps:write", false, [], []],
|
|
64
|
+
["events:read", false, [], []],
|
|
65
|
+
["targeting:debug", false, [], []],
|
|
66
|
+
["visitors:forward", false, [], []],
|
|
67
|
+
["apikeys:read", false, [], []],
|
|
68
|
+
["apikeys:write", false, [], []],
|
|
69
|
+
["organizations:read", false, [], []],
|
|
70
|
+
["organizations:write", false, [], []],
|
|
71
|
+
["organizations:create", false, [], []],
|
|
72
|
+
["organizations:delete", false, [], []],
|
|
73
|
+
["organizations:reach", false, [], []],
|
|
74
|
+
["members:read", false, [], []],
|
|
75
|
+
["members:write", false, [], []],
|
|
76
|
+
["roles:read", false, [], []],
|
|
77
|
+
["roles:write", false, [], []],
|
|
78
|
+
["projects:read", false, [], []],
|
|
79
|
+
["projects:write", false, [], []],
|
|
80
|
+
["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"]],
|
|
81
|
+
["projects:create", false, [], []],
|
|
82
|
+
["projects:delete", false, [], []],
|
|
83
|
+
["tenants:read", false, [], []],
|
|
84
|
+
["tenants:write", false, [], []],
|
|
85
|
+
["tenants:delete", false, [], []],
|
|
86
|
+
["audit:read", false, [], []],
|
|
87
|
+
["export:run", false, [], []],
|
|
88
|
+
["billing:read", false, [], []],
|
|
89
|
+
["billing:manage", false, [], []],
|
|
90
|
+
["usage:read", false, [], []],
|
|
91
|
+
["controlkeys:read", false, [], []],
|
|
92
|
+
["controlkeys:write", false, [], []],
|
|
93
|
+
["tenanthooks:read", false, [], []],
|
|
94
|
+
["tenanthooks:write", false, [], []],
|
|
95
|
+
["apps:install", false, [], []],
|
|
96
|
+
["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"]],
|
|
97
|
+
["platform:read", false, [], []],
|
|
98
|
+
["platform:manage", false, [], ["tenant:manage", "platform:read", "platform:create_workspace", "users:setup"]],
|
|
99
|
+
["platform:create_workspace", false, [], []],
|
|
100
|
+
["users:read", false, ["own", "named", "all"], []],
|
|
101
|
+
["users:write", false, ["own", "all"], []],
|
|
102
|
+
["users:setup", false, [], []],
|
|
103
|
+
["users:delete", false, ["own", "all"], []],
|
|
104
|
+
["users:create", false, [], []],
|
|
105
|
+
["identities:read", false, [], []],
|
|
106
|
+
["identities:delete", false, [], []],
|
|
107
|
+
["usersecrets:read", false, [], []],
|
|
108
|
+
["usersecrets:list", false, [], []],
|
|
109
|
+
["usersecrets:put", false, [], []],
|
|
110
|
+
["usersecrets:delete", false, [], []],
|
|
111
|
+
["auth:login_with_foir", false, [], []],
|
|
112
|
+
["signinproviders:read", false, [], []],
|
|
113
|
+
["signinproviders:write", false, [], []]
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
// src/auth/grants.ts
|
|
117
|
+
var WILDCARD = "*";
|
|
118
|
+
var RESOURCE_WILDCARD = ":*";
|
|
119
|
+
var index;
|
|
120
|
+
function catalogue() {
|
|
121
|
+
if (!index) {
|
|
122
|
+
const byName = new Map(GRANT_TABLE.map((row) => [row[0], row]));
|
|
123
|
+
const longestFirst = [...byName.keys()].sort(
|
|
124
|
+
(a, b) => a.length !== b.length ? b.length - a.length : a < b ? -1 : 1
|
|
125
|
+
);
|
|
126
|
+
index = { byName, longestFirst };
|
|
127
|
+
}
|
|
128
|
+
return index;
|
|
129
|
+
}
|
|
130
|
+
var reachRank = { own: 1, named: 2, all: 3 };
|
|
131
|
+
function hasReach(permission, reach) {
|
|
132
|
+
const reaches = permission[2];
|
|
133
|
+
return reaches.length === 0 ? reach === "own" : reaches.includes(reach);
|
|
134
|
+
}
|
|
135
|
+
function resourceOf(name) {
|
|
136
|
+
const i = name.indexOf(":");
|
|
137
|
+
return i < 0 ? name : name.slice(0, i);
|
|
138
|
+
}
|
|
139
|
+
function parseGrant(s) {
|
|
140
|
+
const { byName, longestFirst } = catalogue();
|
|
141
|
+
for (const name of longestFirst) {
|
|
142
|
+
if (!s.startsWith(name)) continue;
|
|
143
|
+
let rest = s.slice(name.length);
|
|
144
|
+
if (rest !== "" && rest[0] !== "." && rest[0] !== ":") continue;
|
|
145
|
+
const permission = byName.get(name);
|
|
146
|
+
let reach = "own";
|
|
147
|
+
if (rest.startsWith(".")) {
|
|
148
|
+
const after = rest.slice(1);
|
|
149
|
+
const colon = after.indexOf(":");
|
|
150
|
+
const word = colon < 0 ? after : after.slice(0, colon);
|
|
151
|
+
if (word !== "named" && word !== "all") return null;
|
|
152
|
+
if (!hasReach(permission, word)) return null;
|
|
153
|
+
reach = word;
|
|
154
|
+
rest = after.slice(word.length);
|
|
155
|
+
}
|
|
156
|
+
let target = "";
|
|
157
|
+
if (rest.startsWith(":")) {
|
|
158
|
+
target = rest.slice(1);
|
|
159
|
+
if (!permission[1]) return null;
|
|
160
|
+
if (target === "" || /[: \t\n*]/.test(target)) return null;
|
|
161
|
+
}
|
|
162
|
+
return { permission, reach, target };
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
function covers(held, required) {
|
|
167
|
+
return held.permission === required.permission && reachRank[held.reach] >= reachRank[required.reach] && (held.target === "" || held.target === required.target);
|
|
168
|
+
}
|
|
169
|
+
function resourceWildcard(held) {
|
|
170
|
+
if (!held.endsWith(RESOURCE_WILDCARD)) return null;
|
|
171
|
+
const resource = held.slice(0, -RESOURCE_WILDCARD.length);
|
|
172
|
+
return resource.includes(":") ? null : resource;
|
|
173
|
+
}
|
|
174
|
+
function holdsAt(held, required, depth) {
|
|
175
|
+
for (const h of held) {
|
|
176
|
+
if (h === WILDCARD) return true;
|
|
177
|
+
const resource = resourceWildcard(h);
|
|
178
|
+
if (resource !== null) {
|
|
179
|
+
if (resource === resourceOf(required.permission[0])) return true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const grant = parseGrant(h);
|
|
183
|
+
if (grant === null) continue;
|
|
184
|
+
if (covers(grant, required)) return true;
|
|
185
|
+
const implies = grant.permission[3];
|
|
186
|
+
if (depth < 4 && implies.length > 0 && holdsAt(implies, required, depth + 1)) return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
function holds(held, required) {
|
|
191
|
+
const grant = parseGrant(required);
|
|
192
|
+
return grant !== null && holdsAt(held, grant, 0);
|
|
193
|
+
}
|
|
194
|
+
|
|
1
195
|
// src/placement/index.ts
|
|
2
196
|
var PLACEMENT_MESSAGE = {
|
|
3
197
|
/** Page to console: the page is listening and wants a token. */
|
|
@@ -13,6 +207,27 @@ var PLACEMENT_MESSAGE = {
|
|
|
13
207
|
/** Console to page: how the console looks, now and whenever it changes. */
|
|
14
208
|
appearance: "foir:placement:appearance"
|
|
15
209
|
};
|
|
210
|
+
function strings(value) {
|
|
211
|
+
return Array.isArray(value) ? value.filter((v) => typeof v === "string") : null;
|
|
212
|
+
}
|
|
213
|
+
function reasons(value) {
|
|
214
|
+
if (!Array.isArray(value)) return null;
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const entry of value) {
|
|
217
|
+
const e = entry;
|
|
218
|
+
if (typeof e?.grant === "string") out.push({ grant: e.grant, reason: typeof e.reason === "string" ? e.reason : "" });
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
function grantFrom(sent) {
|
|
223
|
+
return {
|
|
224
|
+
accessToken: sent.accessToken,
|
|
225
|
+
expiresAt: sent.expiresAt,
|
|
226
|
+
orgId: sent.orgId,
|
|
227
|
+
grants: strings(sent.grants) ?? [],
|
|
228
|
+
withheldGrants: reasons(sent.withheldGrants) ?? []
|
|
229
|
+
};
|
|
230
|
+
}
|
|
16
231
|
var FOIR_CONSOLE_ORIGINS = {
|
|
17
232
|
production: ["https://app.foir.io"],
|
|
18
233
|
dev: ["https://app-dev.foir.io"]
|
|
@@ -87,7 +302,9 @@ function connectPlacement(options) {
|
|
|
87
302
|
get appearance() {
|
|
88
303
|
return appearance;
|
|
89
304
|
},
|
|
90
|
-
|
|
305
|
+
// As written, for a list a console of the release before sent in the
|
|
306
|
+
// names a placement declared then; and by what the list covers.
|
|
307
|
+
can: (asked) => grant ? grant.grants.includes(asked) || holds(grant.grants, asked) : false,
|
|
91
308
|
async request(document, variables) {
|
|
92
309
|
if (!grant) throw new PlacementRefused("the operator has signed out");
|
|
93
310
|
const res = await doFetch(`${apiUrl}/graphql`, {
|
|
@@ -113,7 +330,7 @@ function connectPlacement(options) {
|
|
|
113
330
|
const msg = event.data;
|
|
114
331
|
switch (msg.type) {
|
|
115
332
|
case PLACEMENT_MESSAGE.token:
|
|
116
|
-
grant = msg.grant;
|
|
333
|
+
grant = grantFrom(msg.grant);
|
|
117
334
|
scheduleRefresh();
|
|
118
335
|
if (!settled) {
|
|
119
336
|
settled = true;
|
package/dist/push.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" };
|
|
@@ -1034,6 +1080,9 @@ var AUTH_OPS = {
|
|
|
1034
1080
|
// Post-credential self reads.
|
|
1035
1081
|
currentUser: "currentUser",
|
|
1036
1082
|
myProviderToken: "myProviderToken",
|
|
1083
|
+
// What the calling credential may do, and for a signed-in customer the roles
|
|
1084
|
+
// they hold. `./access` reads it.
|
|
1085
|
+
whoami: "whoami",
|
|
1037
1086
|
// Methods.
|
|
1038
1087
|
customerLogin: "customerLogin",
|
|
1039
1088
|
customerRegister: "customerRegister",
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { AuthProviderPublic, FoirCustomer, FoirSessionConfig, SessionAdapter } from '../auth';
|
|
2
|
+
import type { AuthProviderPublic, FoirAccess, FoirCustomer, FoirSessionConfig, SessionAdapter } from '../auth';
|
|
3
3
|
import type { ProviderCallbackInput, ProviderLoginInput } from '../auth';
|
|
4
4
|
import type { FoirEnv } from '../graphql/foir-request';
|
|
5
5
|
/** Where the session hydration stands. */
|
|
6
6
|
export type FoirStatus = 'loading' | 'authenticated' | 'unauthenticated';
|
|
7
|
+
/**
|
|
8
|
+
* Where reading the signed-in customer's access stands. `idle` until something
|
|
9
|
+
* asks for it; `unavailable` when the platform could not be asked.
|
|
10
|
+
*/
|
|
11
|
+
export type FoirAccessStatus = 'idle' | 'loading' | 'ready' | 'unavailable';
|
|
7
12
|
/** The full context value. Read it through the hooks below, not directly. */
|
|
8
13
|
export interface FoirContextValue {
|
|
9
14
|
status: FoirStatus;
|
|
10
15
|
customer: FoirCustomer | null;
|
|
16
|
+
/** The signed-in customer's access, once read. See {@link useFoirAccess}. */
|
|
17
|
+
access: FoirAccess | null;
|
|
18
|
+
accessStatus: FoirAccessStatus;
|
|
19
|
+
/** Ask the platform for the signed-in customer's access, again if it was read before. */
|
|
20
|
+
loadAccess(): Promise<void>;
|
|
21
|
+
/** The same, unless a read is already under way: then that one is awaited. */
|
|
22
|
+
ensureAccess(): Promise<void>;
|
|
11
23
|
/** The API config; exposed so advanced consumers can call `@foir/sdk/auth` directly. */
|
|
12
24
|
env: FoirEnv;
|
|
13
25
|
/** The session store backing this provider. */
|
|
@@ -89,6 +101,33 @@ export interface FoirSession {
|
|
|
89
101
|
export declare function useFoirSession(): FoirSession;
|
|
90
102
|
/** The signed-in customer (`{ id, email }`), or null. */
|
|
91
103
|
export declare function useFoirUser(): FoirCustomer | null;
|
|
104
|
+
/** What {@link useFoirAccess} returns. */
|
|
105
|
+
export interface FoirAccessState {
|
|
106
|
+
/**
|
|
107
|
+
* `loading` until the platform has answered for this session, `ready` once it
|
|
108
|
+
* has (and at once when nobody is signed in), `unavailable` when it could not
|
|
109
|
+
* be asked.
|
|
110
|
+
*/
|
|
111
|
+
status: 'loading' | 'ready' | 'unavailable';
|
|
112
|
+
isLoading: boolean;
|
|
113
|
+
/** The signed-in customer's access, or null: signed out, loading or unavailable. */
|
|
114
|
+
access: FoirAccess | null;
|
|
115
|
+
/** Whether the customer holds the role with this key. False until known. */
|
|
116
|
+
hasRole(key: string): boolean;
|
|
117
|
+
/** Whether the customer's scopes cover this one. False until known. */
|
|
118
|
+
hasScope(scope: string): boolean;
|
|
119
|
+
/** Ask the platform again, for a role given or taken away since. */
|
|
120
|
+
reload(): Promise<void>;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The roles the signed-in customer holds and the scopes they resolve to, as the
|
|
124
|
+
* platform resolved them. Read on first use, again when the session refreshes,
|
|
125
|
+
* and whenever `reload` is called.
|
|
126
|
+
*
|
|
127
|
+
* Use it to decide what to show. It protects nothing: the platform checks every
|
|
128
|
+
* request whatever the page offers.
|
|
129
|
+
*/
|
|
130
|
+
export declare function useFoirAccess(): FoirAccessState;
|
|
92
131
|
/** The sign-in / sign-out actions. */
|
|
93
132
|
export declare function useFoirAuth(): {
|
|
94
133
|
signIn: (vars: {
|
|
@@ -45,7 +45,7 @@ export interface FoirFeedbackClient {
|
|
|
45
45
|
*
|
|
46
46
|
* The distinction earns its place because the three have different audiences.
|
|
47
47
|
* `signIn` is the visitor's to fix. `scope` is the OPERATOR's — a role that does
|
|
48
|
-
* not grant `notes:
|
|
48
|
+
* not grant `notes:create` produces `missing required scope: notes:create`, which
|
|
49
49
|
* reads like a bad API key and is not one; the key is fine and the customer's
|
|
50
50
|
* role is short a capability. `other` is everything the widget will not pretend
|
|
51
51
|
* to understand.
|
package/dist/react/index.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* `@foir/sdk/react` — drop-in React components for Foir customers.
|
|
3
3
|
*
|
|
4
4
|
* Wrap your app in `<FoirProvider>`, then read the session with
|
|
5
|
-
* `useFoirSession` / `useFoirUser
|
|
5
|
+
* `useFoirSession` / `useFoirUser`, ask what the customer may see with
|
|
6
|
+
* `useFoirAccess`, and drive it with `useFoirAuth` — or drop in
|
|
6
7
|
* the `<SignIn>`, `<OAuthButtons>`, and `<UserButton>` components. Built entirely
|
|
7
8
|
* on `@foir/sdk/auth`; `react` is the only peer dependency.
|
|
8
9
|
*
|
|
@@ -10,8 +11,8 @@
|
|
|
10
11
|
* RECORD the page was rendered from rather than to its URL. It takes the data
|
|
11
12
|
* client from `@foir/sdk/data` and the subject registry you built for it.
|
|
12
13
|
*/
|
|
13
|
-
export { FoirProvider, useFoirSession, useFoirUser, useFoirAuth, } from './FoirProvider';
|
|
14
|
-
export type { FoirProviderProps, FoirContextValue, FoirSession, FoirStatus, } from './FoirProvider';
|
|
14
|
+
export { FoirProvider, useFoirSession, useFoirUser, useFoirAuth, useFoirAccess, } from './FoirProvider';
|
|
15
|
+
export type { FoirProviderProps, FoirContextValue, FoirSession, FoirStatus, FoirAccessState, FoirAccessStatus, } from './FoirProvider';
|
|
15
16
|
export { SignIn } from './SignIn';
|
|
16
17
|
export type { SignInProps } from './SignIn';
|
|
17
18
|
export { OAuthButtons } from './OAuthButtons';
|
|
@@ -21,5 +22,5 @@ export type { UserButtonProps } from './UserButton';
|
|
|
21
22
|
export { FoirFeedback } from './FoirFeedback';
|
|
22
23
|
export type { FoirFeedbackProps, FoirFeedbackClient } from './FoirFeedback';
|
|
23
24
|
export { createLocalStorageAdapter } from './localStorageAdapter';
|
|
24
|
-
export type { FoirCustomer, SessionAdapter, AuthProviderPublic } from '../auth';
|
|
25
|
+
export type { FoirAccess, FoirCustomer, SessionAdapter, AuthProviderPublic } from '../auth';
|
|
25
26
|
export type { FoirEnv } from '../graphql/foir-request';
|