@foir/sdk 0.18.0 → 0.20.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/dist/placement.js CHANGED
@@ -1,3 +1,197 @@
1
+ // src/auth/grant-table.generated.ts
2
+ var GRANT_TABLE = [
3
+ ["records:read", true, ["own", "named", "all"], []],
4
+ ["records:create", true, [], []],
5
+ ["records:write", true, ["own", "named", "all"], []],
6
+ ["records:delete", true, ["own", "named", "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, [], []],
23
+ ["notes:write", false, [], []],
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", "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"]],
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", "audit:read", "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,32 @@ 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, key) {
214
+ if (!Array.isArray(value)) return null;
215
+ const out = [];
216
+ for (const entry of value) {
217
+ const e = entry;
218
+ if (typeof e?.[key] === "string") out.push({ name: e[key], reason: typeof e.reason === "string" ? e.reason : "" });
219
+ }
220
+ return out;
221
+ }
222
+ function grantFrom(sent) {
223
+ const grants = strings(sent.grants) ?? strings(sent.grantedScopes) ?? [];
224
+ const withheld = reasons(sent.withheldGrants, "grant") ?? reasons(sent.withheld, "scope") ?? [];
225
+ const before = reasons(sent.withheld, "scope") ?? withheld;
226
+ return {
227
+ accessToken: sent.accessToken,
228
+ expiresAt: sent.expiresAt,
229
+ orgId: sent.orgId,
230
+ grants,
231
+ withheldGrants: withheld.map((w) => ({ grant: w.name, reason: w.reason })),
232
+ grantedScopes: strings(sent.grantedScopes) ?? grants,
233
+ withheld: before.map((w) => ({ scope: w.name, reason: w.reason }))
234
+ };
235
+ }
16
236
  var FOIR_CONSOLE_ORIGINS = {
17
237
  production: ["https://app.foir.io"],
18
238
  dev: ["https://app-dev.foir.io"]
@@ -87,7 +307,9 @@ function connectPlacement(options) {
87
307
  get appearance() {
88
308
  return appearance;
89
309
  },
90
- can: (scope) => grant?.grantedScopes.includes(scope) ?? false,
310
+ // As written, for a list a console of the release before sent in the
311
+ // names a placement declared then; and by what the list covers.
312
+ can: (asked) => grant ? grant.grants.includes(asked) || holds(grant.grants, asked) : false,
91
313
  async request(document, variables) {
92
314
  if (!grant) throw new PlacementRefused("the operator has signed out");
93
315
  const res = await doFetch(`${apiUrl}/graphql`, {
@@ -113,7 +335,7 @@ function connectPlacement(options) {
113
335
  const msg = event.data;
114
336
  switch (msg.type) {
115
337
  case PLACEMENT_MESSAGE.token:
116
- grant = msg.grant;
338
+ grant = grantFrom(msg.grant);
117
339
  scheduleRefresh();
118
340
  if (!settled) {
119
341
  settled = true;
package/dist/push.js CHANGED
@@ -1034,6 +1034,9 @@ var AUTH_OPS = {
1034
1034
  // Post-credential self reads.
1035
1035
  currentUser: "currentUser",
1036
1036
  myProviderToken: "myProviderToken",
1037
+ // What the calling credential may do, and for a signed-in customer the roles
1038
+ // they hold. `./access` reads it.
1039
+ whoami: "whoami",
1037
1040
  // Methods.
1038
1041
  customerLogin: "customerLogin",
1039
1042
  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:write` produces `missing required scope: notes:write`, which
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.
@@ -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` and drive it with `useFoirAuth` — or drop in
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';