@foir/sdk 0.20.0 → 0.22.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 +135 -1
- package/README.md +9 -9
- package/dist/app/client.d.ts +90 -84
- package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +14 -13
- package/dist/app/dispatch.d.ts +5 -5
- package/dist/app/errors.d.ts +11 -11
- package/dist/app/index.d.ts +4 -4
- package/dist/app/naming.d.ts +1 -1
- package/dist/app/ticket.d.ts +2 -2
- package/dist/app/vocabulary.generated.d.ts +2 -2
- package/dist/app-contract.js +3 -3
- package/dist/app.js +92 -90
- package/dist/auth/access.d.ts +19 -19
- package/dist/auth/auth.d.ts +216 -114
- package/dist/auth/hosted.d.ts +14 -14
- package/dist/auth/index.d.ts +7 -7
- package/dist/auth/operations.d.ts +128 -32
- package/dist/auth/ops.d.ts +39 -21
- package/dist/auth/session-client.d.ts +3 -3
- package/dist/auth-session.js +3 -3
- package/dist/auth.js +291 -146
- package/dist/client/platform-client.d.ts +1 -1
- package/dist/client.js +32 -5
- package/dist/codegen/index.js +1 -1
- package/dist/data/builtins.d.ts +41 -82
- package/dist/data/client.d.ts +2 -2
- package/dist/data/decimal.d.ts +1 -1
- package/dist/data/document.d.ts +14 -0
- package/dist/data/index.d.ts +2 -2
- package/dist/data/manifest.d.ts +1 -1
- package/dist/data/schema.d.ts +1 -1
- package/dist/data/transport.d.ts +33 -26
- package/dist/data/ws-transport.d.ts +1 -1
- package/dist/data.js +97 -19
- package/dist/graphql/content-client.d.ts +13 -13
- package/dist/index.js +24 -0
- package/dist/live/drift.d.ts +1 -1
- package/dist/media/screen-capture.d.ts +1 -1
- package/dist/next/index.d.ts +2 -2
- package/dist/next/session.d.ts +10 -10
- package/dist/next.js +121 -61
- package/dist/placement/index.d.ts +15 -32
- package/dist/placement.js +18 -20
- package/dist/push.js +113 -46
- package/dist/react/FoirFeedback.d.ts +2 -2
- package/dist/react/FoirProvider.d.ts +23 -23
- package/dist/react/SignIn.d.ts +4 -4
- package/dist/react/UserButton.d.ts +1 -1
- package/dist/react/feedback-notes.d.ts +4 -4
- package/dist/react/index.d.ts +3 -3
- package/dist/react.js +168 -105
- package/dist/realtime/index.d.ts +1 -1
- package/dist/server/callback-client.d.ts +4 -4
- package/dist/server/events-client.d.ts +18 -18
- package/dist/server/operation-types.d.ts +8 -8
- package/dist/server/require-scoped-token.d.ts +3 -3
- package/dist/server/server-client.d.ts +1 -1
- package/dist/server/types.d.ts +1 -1
- package/dist/server/verify-scoped-token.d.ts +4 -28
- package/dist/server.js +67 -112
- package/dist/transport/foir-http.d.ts +57 -18
- package/dist/transport/key-kind.d.ts +40 -0
- package/dist/types/index.d.ts +2 -2
- package/package.json +3 -3
- package/dist/server/older-claims.generated.d.ts +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -108,6 +108,89 @@ nobody asking for `@foir/sdk` gets it early. The push that carries the commit to
|
|
|
108
108
|
|
|
109
109
|
Changes on `dev` that have not been published to npm.
|
|
110
110
|
|
|
111
|
+
## [0.22.0] — 2026-10-15
|
|
112
|
+
|
|
113
|
+
### Added
|
|
114
|
+
|
|
115
|
+
- **A signed-in user manages their own account** (`@foir/sdk/auth`), in any
|
|
116
|
+
pool. `updateMyProfile` changes their name, picture or language and asks no
|
|
117
|
+
permission. `mySessions`, `endMySession` and `signOutEverywhere` list and end
|
|
118
|
+
their sessions. `createPersonalAccessToken` makes a token that begins
|
|
119
|
+
`foir_pat_`, answered once (`NewPersonalAccessToken`). `mySignInMethods`, `connectSignInMethod` and
|
|
120
|
+
`disconnectSignInMethod` list, add and remove the ways they sign in.
|
|
121
|
+
`myAuthorizedApps` and `revokeAuthorizedApp` list the apps they have let in
|
|
122
|
+
and take one back. New types: `MyProfile`, `MyProfileChange`, `MySession`,
|
|
123
|
+
`MySignInMethod`, `AuthorizedApp`.
|
|
124
|
+
- **Second factors and recovery codes** (`@foir/sdk/auth`). `myFactors` lists
|
|
125
|
+
what a user holds. `beginStepUp` and `completeStepUp` prove a factor and
|
|
126
|
+
answer an elevated handle for one change in the next ten minutes, which
|
|
127
|
+
`beginFactorEnrollment`, `completeFactorEnrollment`, `removeFactor` and
|
|
128
|
+
`makeRecoveryCodes` take. A factor is `totp`, `webauthn` or `passkey`. New
|
|
129
|
+
types: `MyFactor`, `FactorEnrollment`, `StepUp`.
|
|
130
|
+
- **An organisation that signs its own people in** (`@foir/sdk/app`).
|
|
131
|
+
`organizations.provision({ ..., ownPool: true })` starts a pool of its own
|
|
132
|
+
with the organisation, apart from its parent's and the project's; its
|
|
133
|
+
descendants share it unless one starts another.
|
|
134
|
+
- All of the above need the platform release that serves them.
|
|
135
|
+
|
|
136
|
+
### Changed
|
|
137
|
+
|
|
138
|
+
- **Breaking: everyone who signs in is a user, and nothing says customer or
|
|
139
|
+
admin as a kind.** It needs the platform release that serves the same
|
|
140
|
+
names; an earlier platform refuses every call below by name.
|
|
141
|
+
- `@foir/sdk/auth`: the raw calls take the server's op names: `login`,
|
|
142
|
+
`register`, `requestOTP`, `loginWithOTP`, `refreshTokens` (the
|
|
143
|
+
`refreshToken` op), `logout`, `joinWaitlist`, `signupContext`,
|
|
144
|
+
`requestPasswordReset`, `resetPassword`, `updatePassword`, `verifyEmail`,
|
|
145
|
+
`loginWithProvider`, `providerCallback`, `providerVerifyOTP` (were
|
|
146
|
+
`customerLogin` and the rest), and `registerOrganization` founds an
|
|
147
|
+
organization over the `registerOrganization` op (was
|
|
148
|
+
`registerOperatorOrganization`). The helpers that save a session are
|
|
149
|
+
`signInWithPassword`, `signUpWithPassword` and `signInWithOTP` (were
|
|
150
|
+
`loginWithPassword`, `registerWithPassword`, `loginWithOTP`), beside
|
|
151
|
+
`signOut`. `getSessionCustomer` is `getSessionUser`, `customerAccess` is
|
|
152
|
+
`userAccess`, `CustomerAuthResult` is `AuthResult` and carries `userId`,
|
|
153
|
+
`FoirCustomer` is `FoirUser` (`{ id, email }`), and `FoirAccess.customerId`
|
|
154
|
+
is `userId`. A session is stored under `foir:userId`, so one saved by an
|
|
155
|
+
earlier version reads as signed out once.
|
|
156
|
+
- `@foir/sdk/auth`: a provider sign-in that names an address somebody
|
|
157
|
+
already holds waits for the code sent to it: `providerSignInRefusalOf`
|
|
158
|
+
reads the refusal (`link_code_sent` with its `pending`), and
|
|
159
|
+
`confirmProviderLink` and `resendProviderLinkCode` finish it.
|
|
160
|
+
- `@foir/sdk/auth`: the hosted session answers `userId`
|
|
161
|
+
(`HostedSession.userId`, `FoirSessionUser.userId`), from the platform's
|
|
162
|
+
`user_id`, and its cookie is `foir_session`.
|
|
163
|
+
- `@foir/sdk/react`: `useFoirSession()` answers `user` (was `customer`).
|
|
164
|
+
- `@foir/sdk/app`: `records.user` and `users` (were `records.customer` and
|
|
165
|
+
`customers`), over the platform's `saveUserRecord`, `userRecordByIdentity`,
|
|
166
|
+
`eraseUserRecord`, `createUser`, `user`, `users`, `userByIdentity` and
|
|
167
|
+
`userIdentitiesById`. `users.list()` answers `{ users }`. `AppUser`,
|
|
168
|
+
`AppUserList`, `AppUserSaveResult` (`userCreated`), `AppUserEraseResult`
|
|
169
|
+
(`userDeleted`, `userKeptBecause`), `AppUserEmailUnavailableReason`,
|
|
170
|
+
`appUserUnlinkedOf` and `appUserEmailUnavailableOf` replace the
|
|
171
|
+
`AppCustomer` names. A notification, a consent change and an
|
|
172
|
+
organization's first member name `userId`.
|
|
173
|
+
- `@foir/sdk/server`: an operation's context names who started the run as
|
|
174
|
+
`actorId` and the user it acts for as `userId` (was `userId` and
|
|
175
|
+
`customerId`). `ScopedTokenIngress.userId` was `customerId`.
|
|
176
|
+
- `@foir/sdk/data`: `FoirGranteeKind` is `'USER'`, a note's subject may be
|
|
177
|
+
`'user'`, `FoirUserNote` and `FoirUserNoteAttachment` replace the
|
|
178
|
+
`FoirCustomerNote` names, and `FoirAuthDowngrade.userToken` was
|
|
179
|
+
`customerToken`.
|
|
180
|
+
|
|
181
|
+
### Removed
|
|
182
|
+
|
|
183
|
+
- **Breaking: `selectOrganization` and `SelectedOrganization`**
|
|
184
|
+
(`@foir/sdk/auth`). Nothing stores where a user stands: a request names the
|
|
185
|
+
organization it acts in (`orgId` or `orgUrl`), and the platform checks they
|
|
186
|
+
stand there on that request. `myOrganizations` still lists where they may.
|
|
187
|
+
- **Breaking: `requireAdmin` and `RequireAdminOptions`, and `isAdmin` on
|
|
188
|
+
`FoirUser`** (`@foir/sdk/auth`). What a user may do is a role or a scope:
|
|
189
|
+
guard on it with `requireRole` or `requireScope`. `requireAuth` takes the
|
|
190
|
+
type `resolveUser` answers, for a project that reads its own profile.
|
|
191
|
+
|
|
192
|
+
## [0.21.0] — 2026-10-13
|
|
193
|
+
|
|
111
194
|
### Added
|
|
112
195
|
|
|
113
196
|
- **An operation provisions an organization** (`@foir/sdk/app`).
|
|
@@ -131,14 +214,65 @@ Changes on `dev` that have not been published to npm.
|
|
|
131
214
|
release that carries `writeNote`. `foir.ns.notes` answers any field the
|
|
132
215
|
platform serves, so the call itself needs no new SDK.
|
|
133
216
|
|
|
217
|
+
- **A server says where its visitor is** (`@foir/sdk/data`). `visitor` takes
|
|
218
|
+
an object as well as an address: `{ address, country, latitude, longitude }`,
|
|
219
|
+
or a function that yields one for each request. The address is sent as
|
|
220
|
+
`X-Foir-Visitor-IP`, as before. The country and the coordinates are attached
|
|
221
|
+
to every read, in its `contexts`, so location targeting is decided by where
|
|
222
|
+
the visitor is and not by where your server is. Coordinates are sent at two
|
|
223
|
+
decimal places, and latitude and longitude go together or not at all. The
|
|
224
|
+
visitor is resolved once for a request, so the header and the read name the
|
|
225
|
+
same one. A string is still the address alone. New type: `FoirVisitor`, and
|
|
226
|
+
`country` and `location` on `FoirResolveContext`. It needs a key holding
|
|
227
|
+
`visitors:forward`, and the platform release that takes `contexts.country`
|
|
228
|
+
and `contexts.location`: an earlier platform refuses a read that names them.
|
|
229
|
+
|
|
134
230
|
### Changed
|
|
135
231
|
|
|
232
|
+
- **Breaking: an app client refuses an admin key** (`@foir/sdk/data`).
|
|
233
|
+
`createFoirClient` given a key that begins `foir_adm_` throws before any
|
|
234
|
+
request. An admin key is a machine's own: nobody signs in through it, and an
|
|
235
|
+
app never holds one. Build the client from a publishable key, or from a
|
|
236
|
+
server key on the server that renders your pages. A machine that acts for
|
|
237
|
+
itself uses `createServerClient` from `@foir/sdk/server`. A key minted
|
|
238
|
+
before a key had a kind (`sk_`) is not refused here.
|
|
239
|
+
- **A key's kind is read from its prefix in one place.** A key is publishable
|
|
240
|
+
(`foir_pub_`), server (`foir_srv_`) or admin (`foir_adm_`), and a key minted
|
|
241
|
+
earlier is `pk_`, `sk_` or `tc_`. Only a publishable key, in either format,
|
|
242
|
+
is named in a URL and takes the shared cache lane. Earlier versions looked
|
|
243
|
+
for the letters `pk_` alone, so a `foir_pub_` key was sent to the origin and
|
|
244
|
+
missed the shared cache.
|
|
245
|
+
- **`visitor` takes a server key** (`@foir/sdk/data`). A client that names the
|
|
246
|
+
visitor it calls for is built from a server key (`foir_srv_`), or from a
|
|
247
|
+
secret key minted before a key had a kind. Earlier versions looked for the
|
|
248
|
+
letters `sk_` alone and refused a server key. A publishable key is still
|
|
249
|
+
refused `visitor`, in either format.
|
|
250
|
+
- **The events client takes an admin key** (`@foir/sdk/server`).
|
|
251
|
+
`createEventsClient` refuses a publishable key and a server key by name, and
|
|
252
|
+
takes an admin key (`foir_adm_`) or a secret key minted earlier.
|
|
136
253
|
- **`FOIR_EVENTS_SCOPE` is `events:read`** (`@foir/sdk/server`). It was
|
|
137
254
|
`events:list.project`, the name the event log's permission had when a key
|
|
138
255
|
was written with scopes. A key is made with the permission catalogue's
|
|
139
256
|
names, where it is `events:read`, and that is what a refusal names. A key
|
|
140
257
|
minted under the older name holds `events:read` and needs no change.
|
|
141
258
|
|
|
259
|
+
### Removed
|
|
260
|
+
|
|
261
|
+
- **Breaking: `ScopedTokenClaims` has no `cap`, `rrm` or `rwm`**
|
|
262
|
+
(`@foir/sdk/server`). The platform no longer writes the claims a token
|
|
263
|
+
carried before it said its grants. Read `grants`, or `recordsReadModels`,
|
|
264
|
+
`recordsWriteModels` and `recordsCreateModels`.
|
|
265
|
+
- **Breaking: `PlacementGrant` has no `grantedScopes` or `withheld`**
|
|
266
|
+
(`@foir/sdk/placement`). A console sends what a page's token carries as
|
|
267
|
+
`grants` and what it was refused as `withheldGrants`, and no longer sends
|
|
268
|
+
the same two lists under their older names. Read `session.grant.grants`, or
|
|
269
|
+
ask `session.can`.
|
|
270
|
+
- **Breaking: `verifyScopedToken` refuses a token that does not say its
|
|
271
|
+
grants** (`@foir/sdk/server`), with `the token says no grants`, as the
|
|
272
|
+
platform refuses it. It used to read such a token as what its older claims
|
|
273
|
+
were worth. `requireScopedToken` answers it 401 like any token that does not
|
|
274
|
+
verify.
|
|
275
|
+
|
|
142
276
|
### Deprecated
|
|
143
277
|
|
|
144
278
|
- **`FoirCreateMyNoteArgs`, `FoirSubmitAnonymousNoteArgs` and
|
|
@@ -1074,7 +1208,7 @@ returns a result object rather than `HostedSession | null`, and
|
|
|
1074
1208
|
silent wrong answer for a thrown one that discards the answer. Nothing changes
|
|
1075
1209
|
for a caller that does not pass it.
|
|
1076
1210
|
|
|
1077
|
-
- `registerOrganization(env, { key, name }, { bearerToken })` on `@foir/sdk/auth`: a customer founds an organization of their own and becomes its first member, holding the role the project's `
|
|
1211
|
+
- `registerOrganization(env, { key, name }, { bearerToken })` on `@foir/sdk/auth`: a customer founds an organization of their own and becomes its first member, holding the role the project's `organizationSignupRole` setting names. Returns the same `MyOrganization` entry `myOrganizations` lists. Present on the wire only while the project's `organizationSignupEnabled` setting is on.
|
|
1078
1212
|
- **The operator's organization context, on `@foir/sdk/auth`** — `myOrganizations`
|
|
1079
1213
|
and `selectOrganization`, plus the types `MyOrganization` and
|
|
1080
1214
|
`SelectedOrganization`, and two entries on `AUTH_OPS`.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @foir/sdk
|
|
2
2
|
|
|
3
|
-
Official Foir SDK — client-agnostic typed GraphQL transport,
|
|
3
|
+
Official Foir SDK — client-agnostic typed GraphQL transport, sign-in for your app's users,
|
|
4
4
|
progressive media, content rendering, realtime collaboration, and editor/server
|
|
5
5
|
helpers for building apps on the [Foir](https://foir.dev) platform.
|
|
6
6
|
|
|
@@ -25,7 +25,7 @@ pnpm add yjs y-websocket # for @foir/sdk/realtime
|
|
|
25
25
|
| `@foir/sdk` | Editor iframe client — `EditorProvider`, `useEditor`, `useAutoResize`, `createPlatformClient`, host↔editor message protocol | `react` |
|
|
26
26
|
| `@foir/sdk/client` | Client-agnostic GraphQL transport for SSR/edge loaders — `foirRequest`, `isFoirPreview`, `absolutiseFoirMediaUrl` | `graphql` |
|
|
27
27
|
| `@foir/sdk/data` | Typed data client over `/graphql` — `createFoirClient`, per-key-shaped ops, `include`/`omit`, `fetchFoirManifest`; prints the document for you, so still no `graphql` | **none** |
|
|
28
|
-
| `@foir/sdk/auth` | Framework-agnostic
|
|
28
|
+
| `@foir/sdk/auth` | Framework-agnostic sign-in for your app's users: login/register/OTP/provider flows, token lifecycle (`SessionAdapter`), `createFoirSessionClient` (Foir-hosted cookie sessions), `authedRequest`, guards | `graphql` |
|
|
29
29
|
| `@foir/sdk/react` | Drop-in React auth — `<FoirProvider>` (token or `hostedSession` mode), `useFoirSession`/`useFoirUser`/`useFoirAuth`, `<SignIn>`, `<OAuthButtons>`, `<UserButton>` | `react`, `graphql` |
|
|
30
30
|
| `@foir/sdk/next` | Next.js App Router BFF auth — `createFoirCookieAdapter`, `getFoirSession`, `foirSignIn*`, `completeFoirProviderLogin`, `createFoirMiddleware` | `graphql` |
|
|
31
31
|
| `@foir/sdk/server` | Server-side helpers for extension/config API backends — `createServerClient`, `verifyWebhookSignature`, `verifyScopedToken`, callback client | — |
|
|
@@ -120,13 +120,13 @@ that way; silence it with `announceManifestFallback: false`.
|
|
|
120
120
|
Reads that can be shared are sent to `GET /cdn/graphql` with only the
|
|
121
121
|
persisted-query hash in the URL, so a CDN in front of you can serve the next
|
|
122
122
|
visitor without reaching origin. That is a query, on a `pk_` key, with no
|
|
123
|
-
|
|
123
|
+
user bearer, on the published channel; everything else is a `POST /graphql`.
|
|
124
124
|
|
|
125
125
|
`foir.raw()` is the escape hatch, and it takes a **GraphQL document** — it
|
|
126
126
|
reaches anything the credential can, including root fields no model or namespace
|
|
127
127
|
claims. It returns the whole `data` object.
|
|
128
128
|
|
|
129
|
-
Namespaces (`search`, `
|
|
129
|
+
Namespaces (`search`, `users`, `notifications`) live under `foir.ns` because
|
|
130
130
|
the client carries no runtime schema to tell a namespace from a model. `ns` and
|
|
131
131
|
`raw` are the only reserved names.
|
|
132
132
|
|
|
@@ -178,18 +178,18 @@ export async function loader({ context }: LoaderArgs) {
|
|
|
178
178
|
}
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
###
|
|
181
|
+
### Signing your users in
|
|
182
182
|
|
|
183
183
|
```ts
|
|
184
184
|
import {
|
|
185
|
-
|
|
185
|
+
signInWithPassword,
|
|
186
186
|
authedRequest,
|
|
187
187
|
type SessionAdapter,
|
|
188
188
|
} from '@foir/sdk/auth';
|
|
189
189
|
import { MyProfileDocument } from './generated/graphql';
|
|
190
190
|
|
|
191
191
|
// SessionAdapter = { get, set, delete } over your session store (cookie, KV…).
|
|
192
|
-
const
|
|
192
|
+
const user = await signInWithPassword(adapter, env, { email, password });
|
|
193
193
|
|
|
194
194
|
// Bearer-authed request with automatic refresh-and-retry on expired sessions.
|
|
195
195
|
const { myProfile } = await authedRequest(adapter, env, MyProfileDocument);
|
|
@@ -282,8 +282,8 @@ import { cookies } from 'next/headers';
|
|
|
282
282
|
import { getFoirSession } from '@foir/sdk/next';
|
|
283
283
|
|
|
284
284
|
export default async function Dashboard() {
|
|
285
|
-
const
|
|
286
|
-
return <p>Signed in as {
|
|
285
|
+
const user = await getFoirSession(await cookies());
|
|
286
|
+
return <p>Signed in as {user?.email}</p>;
|
|
287
287
|
}
|
|
288
288
|
```
|
|
289
289
|
|
package/dist/app/client.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* The client an installed app calls Foir with.
|
|
3
3
|
*
|
|
4
4
|
* An app is installed in many projects, and each install maps the app's
|
|
5
|
-
* sources onto whichever models that project
|
|
5
|
+
* sources onto whichever models that project chose. So an app has
|
|
6
6
|
* no schema to generate a client from: it learns the model keys at request
|
|
7
7
|
* time, from the install's own configuration, and names them in each call.
|
|
8
8
|
* `createFoirClient` is the client for an endpoint that knows its project;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import type { FoirDispatchCredential } from '../data/transport';
|
|
30
30
|
import { type FoirRetryPolicy } from '../transport/retry';
|
|
31
|
-
import type {
|
|
31
|
+
import type { AppUserEmailUnavailableReason } from './errors';
|
|
32
32
|
type Json = null | boolean | number | string | Json[] | {
|
|
33
33
|
[key: string]: Json;
|
|
34
34
|
};
|
|
@@ -65,18 +65,18 @@ export interface AppFieldMapping {
|
|
|
65
65
|
/** The project model's field key. */
|
|
66
66
|
modelField: string;
|
|
67
67
|
/**
|
|
68
|
-
* SINK_ONLY means the
|
|
68
|
+
* SINK_ONLY means the project has said Foir owns the field: a value from
|
|
69
69
|
* the app's side must not be written over it. An unrecognised direction
|
|
70
70
|
* reads as SINK_ONLY, which withholds the field rather than writing on a
|
|
71
71
|
* guess.
|
|
72
72
|
*/
|
|
73
73
|
direction: AppSyncDirection;
|
|
74
74
|
}
|
|
75
|
-
/** One of the
|
|
75
|
+
/** One of the install's mappings: a source or sink of the app onto a model. */
|
|
76
76
|
export interface AppBinding {
|
|
77
77
|
/** The app's source or sink name. */
|
|
78
78
|
key: string;
|
|
79
|
-
/** The project model
|
|
79
|
+
/** The project model it was mapped onto. */
|
|
80
80
|
model: string;
|
|
81
81
|
/** The model field the app's records are keyed on. Empty when the mapping names none. */
|
|
82
82
|
naturalKey: string;
|
|
@@ -84,7 +84,7 @@ export interface AppBinding {
|
|
|
84
84
|
/** REFERENCE: Foir reads the source's fields live. SYNC: it holds a copy. A sink is a copy. */
|
|
85
85
|
mode: 'SYNC' | 'REFERENCE';
|
|
86
86
|
}
|
|
87
|
-
/** An install's own configuration, as the
|
|
87
|
+
/** An install's own configuration, as the project left it. */
|
|
88
88
|
export interface AppInstallConfig {
|
|
89
89
|
name: string;
|
|
90
90
|
enabled: boolean;
|
|
@@ -92,25 +92,25 @@ export interface AppInstallConfig {
|
|
|
92
92
|
targetModels: string[];
|
|
93
93
|
/** The address the app's vendor is told to deliver webhooks to. Empty when the app declares none. */
|
|
94
94
|
webhookUrl: string;
|
|
95
|
-
/** The
|
|
95
|
+
/** The project's answers to the manifest's settings schema. */
|
|
96
96
|
settings: JsonObject;
|
|
97
97
|
sources: AppBinding[];
|
|
98
98
|
sinks: AppBinding[];
|
|
99
99
|
/**
|
|
100
|
-
* Why the project cannot send a
|
|
100
|
+
* Why the project cannot send a user an email at the level this install
|
|
101
101
|
* sits at, or null when it can. While it is set, a notification that carries
|
|
102
102
|
* an email and an opt-in to a category confirmed by email are refused
|
|
103
|
-
* (`
|
|
103
|
+
* (`appUserEmailUnavailableOf`), so say so before one is.
|
|
104
104
|
*/
|
|
105
|
-
|
|
105
|
+
userEmailUnavailable: AppUserEmailUnavailableReason | null;
|
|
106
106
|
/**
|
|
107
|
-
* The
|
|
108
|
-
*
|
|
107
|
+
* The sign-in this install pairs with the app, by key, for an app whose own
|
|
108
|
+
* system has its own users; null when it pairs none. Every identity
|
|
109
109
|
* the app names is resolved through it, so while it is null nothing finds a
|
|
110
|
-
*
|
|
110
|
+
* user by identity: `users.byIdentity` and `users.identities`
|
|
111
111
|
* are refused, and an operation that needs them is not registered.
|
|
112
112
|
*/
|
|
113
|
-
|
|
113
|
+
userSignIn: string | null;
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
116
|
* The status an install reports. Leaving out `subscriptionHealth`,
|
|
@@ -136,7 +136,7 @@ export interface AppRecord {
|
|
|
136
136
|
/** The platform's own id. What `records.delete` takes. */
|
|
137
137
|
id: string;
|
|
138
138
|
naturalKey: string | null;
|
|
139
|
-
/** The
|
|
139
|
+
/** The user the record belongs to, or null. */
|
|
140
140
|
ownerId: string | null;
|
|
141
141
|
/** The fields that were asked for and could be read, by model field key. */
|
|
142
142
|
fields: JsonObject;
|
|
@@ -163,21 +163,21 @@ export interface AppUpsertResult {
|
|
|
163
163
|
/** True when nothing was written because Foir already holds a newer copy (`sourceModifiedAt`). */
|
|
164
164
|
stale: boolean;
|
|
165
165
|
}
|
|
166
|
-
export type
|
|
166
|
+
export type AppUserSaveResult = (AppUpsertResult & {
|
|
167
167
|
unlinked: false;
|
|
168
|
-
|
|
168
|
+
userCreated: boolean;
|
|
169
169
|
}) | {
|
|
170
170
|
unlinked: true;
|
|
171
171
|
};
|
|
172
|
-
export interface
|
|
172
|
+
export interface AppUserEraseResult {
|
|
173
173
|
recordDeleted: boolean;
|
|
174
174
|
identityRemoved: boolean;
|
|
175
|
-
|
|
176
|
-
/** Another sign-in method, or something else they own; null when the
|
|
177
|
-
|
|
175
|
+
userDeleted: boolean;
|
|
176
|
+
/** Another sign-in method, or something else they own; null when the user went or none was found. */
|
|
177
|
+
userKeptBecause: string | null;
|
|
178
178
|
}
|
|
179
179
|
export interface AppNotification {
|
|
180
|
-
|
|
180
|
+
userId: string;
|
|
181
181
|
/** The in-app title, and the email's subject when `email` names none. */
|
|
182
182
|
title: string;
|
|
183
183
|
message?: string;
|
|
@@ -194,10 +194,10 @@ export interface AppNotification {
|
|
|
194
194
|
url: string;
|
|
195
195
|
}>;
|
|
196
196
|
};
|
|
197
|
-
/** Makes the send safe to repeat: the same key from the same operation tells the
|
|
197
|
+
/** Makes the send safe to repeat: the same key from the same operation tells the user once. */
|
|
198
198
|
idempotencyKey?: string;
|
|
199
199
|
}
|
|
200
|
-
/** An organisation an operation makes for one of its
|
|
200
|
+
/** An organisation an operation makes for one of its users. */
|
|
201
201
|
export interface AppOrganizationProvision {
|
|
202
202
|
/**
|
|
203
203
|
* Yours to choose and to keep. Sending the same one again with the same
|
|
@@ -221,30 +221,36 @@ export interface AppOrganizationProvision {
|
|
|
221
221
|
data: Record<string, unknown>;
|
|
222
222
|
};
|
|
223
223
|
/**
|
|
224
|
-
* The
|
|
224
|
+
* The user who becomes its first member. They are given the role the
|
|
225
225
|
* project names for an organisation's founder, in its settings: the
|
|
226
226
|
* operation chooses who, and the project decides what they hold.
|
|
227
227
|
*/
|
|
228
228
|
firstMember?: {
|
|
229
|
-
|
|
229
|
+
userId: string;
|
|
230
230
|
};
|
|
231
|
+
/**
|
|
232
|
+
* The organisation signs its own people in, to a pool of its own, apart
|
|
233
|
+
* from its parent's and the project's. Its descendants share that pool
|
|
234
|
+
* unless one of them starts another. Chosen now, when it is made.
|
|
235
|
+
*/
|
|
236
|
+
ownPool?: boolean;
|
|
231
237
|
}
|
|
232
238
|
/**
|
|
233
|
-
* Who a consent change is for: a
|
|
239
|
+
* Who a consent change is for: a user whose id the app holds, or an
|
|
234
240
|
* address.
|
|
235
241
|
*
|
|
236
|
-
* By `email`, the
|
|
242
|
+
* By `email`, the user holding the address at the operation's level is
|
|
237
243
|
* found, or made, together with the change, so a change the platform refuses
|
|
238
|
-
* leaves no
|
|
239
|
-
*
|
|
244
|
+
* leaves no user behind. It is for an opt-in from somebody who may not be a
|
|
245
|
+
* user yet (a sign-up form), and the operation must declare
|
|
240
246
|
* `users:create` as well as `notifications:consent`.
|
|
241
247
|
*/
|
|
242
248
|
export type AppConsentSubject = {
|
|
243
|
-
|
|
249
|
+
userId: string;
|
|
244
250
|
email?: undefined;
|
|
245
251
|
} | {
|
|
246
252
|
email: string;
|
|
247
|
-
|
|
253
|
+
userId?: undefined;
|
|
248
254
|
};
|
|
249
255
|
export type AppConsentChange = AppConsentSubject & {
|
|
250
256
|
/** A notification category the project makes opt-in. */
|
|
@@ -259,19 +265,19 @@ export type AppConsentChange = AppConsentSubject & {
|
|
|
259
265
|
*/
|
|
260
266
|
sourceModifiedAt?: string;
|
|
261
267
|
/**
|
|
262
|
-
* True when the
|
|
268
|
+
* True when the user gave this consent in another system and the app is
|
|
263
269
|
* carrying it over (a mirror). It is applied as it stands.
|
|
264
270
|
*
|
|
265
271
|
* Left out, the app collected the consent itself (a sign-up form), and an
|
|
266
272
|
* opt-in is applied the way its category says: at once, or, in a category
|
|
267
|
-
* that confirms by email, only when the
|
|
273
|
+
* that confirms by email, only when the user follows the link the
|
|
268
274
|
* platform sends them. Until then nothing is written and the answer says
|
|
269
275
|
* `confirmationPending`.
|
|
270
276
|
*/
|
|
271
277
|
givenElsewhere?: boolean;
|
|
272
278
|
/**
|
|
273
279
|
* The language of the page the consent was collected on. The confirmation
|
|
274
|
-
* email is written in it when the
|
|
280
|
+
* email is written in it when the user has stated none of their own.
|
|
275
281
|
*/
|
|
276
282
|
locale?: string;
|
|
277
283
|
};
|
|
@@ -308,7 +314,7 @@ export interface AppClient {
|
|
|
308
314
|
whoami(): Promise<AppWhoami>;
|
|
309
315
|
/** The install this token acts for: its configuration, its credential, its status. */
|
|
310
316
|
install: {
|
|
311
|
-
/** The
|
|
317
|
+
/** The install's settings and mappings. Needs `appconfig:read`. */
|
|
312
318
|
config(): Promise<AppInstallConfig>;
|
|
313
319
|
/**
|
|
314
320
|
* The install's credential, by the manifest's credential field names.
|
|
@@ -330,7 +336,7 @@ export interface AppClient {
|
|
|
330
336
|
* Write the record at a natural key, making it when there is none.
|
|
331
337
|
* Changing the record there needs `records:write:<model>` and making one
|
|
332
338
|
* needs `records:create:<model>`, so an operation that does both declares
|
|
333
|
-
* both. `ownerId` names the
|
|
339
|
+
* both. `ownerId` names the user it belongs to (needs
|
|
334
340
|
* `records:attribute:<model>`); `sourceModifiedAt` is when the object this
|
|
335
341
|
* mirrors last changed at its source, so an older copy arriving late is
|
|
336
342
|
* not written.
|
|
@@ -365,7 +371,7 @@ export interface AppClient {
|
|
|
365
371
|
* other is refused, naming it. `hasMore` says the filter matched more than
|
|
366
372
|
* were answered. Like `read`, it sees the PUBLISHED version.
|
|
367
373
|
*
|
|
368
|
-
* The name of a model's list is the
|
|
374
|
+
* The name of a model's list is the project's to set, so the first list a
|
|
369
375
|
* client makes asks the data API what this token is served (`GET
|
|
370
376
|
* /v1/manifest`), once.
|
|
371
377
|
*/
|
|
@@ -420,16 +426,16 @@ export interface AppClient {
|
|
|
420
426
|
truncated: boolean;
|
|
421
427
|
}>;
|
|
422
428
|
/**
|
|
423
|
-
* The record of a
|
|
429
|
+
* The record of a user the app names by identity: their id at the
|
|
424
430
|
* external system, which is also the subject of their sign-in with it. For
|
|
425
|
-
* a source that declares an identity. The platform finds the
|
|
431
|
+
* a source that declares an identity. The platform finds the user
|
|
426
432
|
* through the sign-in the install pairs with the source, at the install's
|
|
427
433
|
* level, and never by email.
|
|
428
434
|
*/
|
|
429
|
-
|
|
435
|
+
user: {
|
|
430
436
|
/**
|
|
431
|
-
* Merge `data` into the
|
|
432
|
-
* have none, and the
|
|
437
|
+
* Merge `data` into the user's record, making the record when they
|
|
438
|
+
* have none, and the user when `email` is given and nobody holds the
|
|
433
439
|
* identity. An identity nobody holds, with no email, is `{ unlinked:
|
|
434
440
|
* true }`: nothing was written, and they are known once they sign in.
|
|
435
441
|
*/
|
|
@@ -439,29 +445,29 @@ export interface AppClient {
|
|
|
439
445
|
data: JsonObject;
|
|
440
446
|
email?: string;
|
|
441
447
|
sourceModifiedAt?: string;
|
|
442
|
-
}): Promise<
|
|
443
|
-
/** The
|
|
448
|
+
}): Promise<AppUserSaveResult>;
|
|
449
|
+
/** The user's record, or null when nobody holds the identity or they hold no record. */
|
|
444
450
|
read(model: string, identity: string): Promise<AppRecord | null>;
|
|
445
451
|
/**
|
|
446
|
-
* Erase what the app holds for the
|
|
447
|
-
* their record and the identity's link, and the
|
|
452
|
+
* Erase what the app holds for the user, as a privacy request asks:
|
|
453
|
+
* their record and the identity's link, and the user when nothing
|
|
448
454
|
* else is attached to them. It removes a record, so it needs
|
|
449
455
|
* `records:delete` and `records:read` for the model, and
|
|
450
456
|
* `identities:delete`. The grant to save the record is not enough.
|
|
451
457
|
*/
|
|
452
|
-
erase(model: string, identity: string): Promise<
|
|
458
|
+
erase(model: string, identity: string): Promise<AppUserEraseResult>;
|
|
453
459
|
};
|
|
454
460
|
};
|
|
455
|
-
|
|
461
|
+
users: {
|
|
456
462
|
/**
|
|
457
|
-
* The
|
|
458
|
-
* is none. The same
|
|
463
|
+
* The user who has this email at the install's level, made when there
|
|
464
|
+
* is none. The same user every time for one email. Needs
|
|
459
465
|
* `users:create`.
|
|
460
466
|
*
|
|
461
|
-
* `verified` says whether that address is one the
|
|
462
|
-
* theirs. A
|
|
467
|
+
* `verified` says whether that address is one the user has proved is
|
|
468
|
+
* theirs. A user this call makes has not, and neither has one an
|
|
463
469
|
* earlier call made: anybody can type an address. So what is written for
|
|
464
|
-
* an unverified
|
|
470
|
+
* an unverified user is written for whoever holds the address, and
|
|
465
471
|
* anything that should wait for proof asks `verified` first.
|
|
466
472
|
*/
|
|
467
473
|
ensure(email: string): Promise<{
|
|
@@ -470,53 +476,53 @@ export interface AppClient {
|
|
|
470
476
|
verified: boolean;
|
|
471
477
|
}>;
|
|
472
478
|
/**
|
|
473
|
-
* One
|
|
479
|
+
* One user by id, or null when there is none at the install's level.
|
|
474
480
|
* Needs `users:read.named`, or `users:read.all`.
|
|
475
481
|
*/
|
|
476
|
-
get(id: string): Promise<
|
|
482
|
+
get(id: string): Promise<AppUser | null>;
|
|
477
483
|
/**
|
|
478
|
-
* The
|
|
484
|
+
* The user who has this email at the install's level, or null. The
|
|
479
485
|
* exact address, as `ensure` matches it, and nothing wider: it finds one
|
|
480
|
-
*
|
|
486
|
+
* user and never searches. Needs `users:read.named`, or
|
|
481
487
|
* `users:read.all`.
|
|
482
488
|
*/
|
|
483
|
-
byEmail(email: string): Promise<
|
|
489
|
+
byEmail(email: string): Promise<AppUser | null>;
|
|
484
490
|
/**
|
|
485
|
-
* The id of the
|
|
491
|
+
* The id of the user who holds this identity at the app's own system,
|
|
486
492
|
* or null when nobody does: for an app hearing from its system about one
|
|
487
|
-
* of its
|
|
488
|
-
* with that system, and it is resolved through the
|
|
489
|
-
* install pairs (`install.config().
|
|
493
|
+
* of its own users. The identity is the subject of the user's sign-in
|
|
494
|
+
* with that system, and it is resolved through the sign-in the
|
|
495
|
+
* install pairs (`install.config().userSignIn`) and no other. Never
|
|
490
496
|
* matched by email. Needs `identities:read`, declared by name:
|
|
491
497
|
* `users:read.all` does not include it.
|
|
492
498
|
*/
|
|
493
499
|
byIdentity(identity: string): Promise<string | null>;
|
|
494
500
|
/**
|
|
495
|
-
* The other direction: the identities a
|
|
501
|
+
* The other direction: the identities a user the app names by id
|
|
496
502
|
* holds at the app's own system, most recently seen first, through the
|
|
497
503
|
* same pairing. Empty when they have never signed in that way. For an app
|
|
498
|
-
* acting on an event that names the Foir
|
|
504
|
+
* acting on an event that names the Foir user. Needs
|
|
499
505
|
* `identities:read`, declared by name.
|
|
500
506
|
*/
|
|
501
|
-
identities(
|
|
507
|
+
identities(userId: string): Promise<string[]>;
|
|
502
508
|
/**
|
|
503
|
-
* The
|
|
509
|
+
* The users at the install's level, a page at a time, newest first.
|
|
504
510
|
* Needs `users:read.all`: an operation declaring only
|
|
505
|
-
* `users:read.named` reads the
|
|
511
|
+
* `users:read.named` reads the users it names and is refused the
|
|
506
512
|
* list.
|
|
507
513
|
*/
|
|
508
|
-
list(query?:
|
|
509
|
-
|
|
514
|
+
list(query?: AppUserList): Promise<{
|
|
515
|
+
users: AppUser[];
|
|
510
516
|
hasMore: boolean;
|
|
511
517
|
endCursor: string | null;
|
|
512
518
|
}>;
|
|
513
519
|
};
|
|
514
520
|
organizations: {
|
|
515
521
|
/**
|
|
516
|
-
* Make an organisation, with its settings record and a
|
|
522
|
+
* Make an organisation, with its settings record and a user as its
|
|
517
523
|
* first member. Needs `organizations:create`, and for `settings` the write
|
|
518
524
|
* of the settings model. Sign-up is the use: one operation creates the
|
|
519
|
-
*
|
|
525
|
+
* user (`users.ensure`) and then the organisation that is theirs.
|
|
520
526
|
*
|
|
521
527
|
* The first member is given the role the project names for an
|
|
522
528
|
* organisation's founder. A project that names none refuses it, saying
|
|
@@ -551,20 +557,20 @@ export interface AppClient {
|
|
|
551
557
|
}>;
|
|
552
558
|
};
|
|
553
559
|
notifications: {
|
|
554
|
-
/** Tell a
|
|
560
|
+
/** Tell a user something. Needs `notifications:write.named`. */
|
|
555
561
|
send(notification: AppNotification): Promise<void>;
|
|
556
562
|
/**
|
|
557
|
-
* Set a named
|
|
563
|
+
* Set a named user's consent in an opt-in category. Needs
|
|
558
564
|
* `notifications:consent`, which only an operation's token carries.
|
|
559
565
|
*
|
|
560
566
|
* `confirmationPending` is true when an opt-in the app collected was not
|
|
561
|
-
* applied because its category confirms by email first: the
|
|
567
|
+
* applied because its category confirms by email first: the user has
|
|
562
568
|
* been sent a link, or was sent one recently, and the preference turns on
|
|
563
|
-
* when they follow it. The answer is the same whether or not
|
|
564
|
-
*
|
|
569
|
+
* when they follow it. The answer is the same whether or not anybody
|
|
570
|
+
* held the address already.
|
|
565
571
|
*
|
|
566
|
-
* Name the
|
|
567
|
-
*
|
|
572
|
+
* Name the user by `userId`, or by `email` for somebody who may not be
|
|
573
|
+
* a user yet: the platform finds or makes them with the change,
|
|
568
574
|
* and that also needs `users:create`.
|
|
569
575
|
*/
|
|
570
576
|
setConsent(change: AppConsentChange): Promise<{
|
|
@@ -580,7 +586,7 @@ export interface AppClient {
|
|
|
580
586
|
*
|
|
581
587
|
* For work a delivery cannot do in place. A delivery is not a run, so it
|
|
582
588
|
* holds none of the authority the platform gives only inside one (setting
|
|
583
|
-
* a
|
|
589
|
+
* a user's consent is one), and it has the few seconds its vendor
|
|
584
590
|
* waits. Hand the work to an operation and answer the vendor.
|
|
585
591
|
*
|
|
586
592
|
* Only the token a delivery arrived with may call it, for an operation
|
|
@@ -590,16 +596,16 @@ export interface AppClient {
|
|
|
590
596
|
start(operationKey: string, options?: AppOperationStart): Promise<AppStartedOperation>;
|
|
591
597
|
};
|
|
592
598
|
}
|
|
593
|
-
/** A
|
|
594
|
-
export interface
|
|
599
|
+
/** A user, as an operation that declared a user read sees them. */
|
|
600
|
+
export interface AppUser {
|
|
595
601
|
id: string;
|
|
596
602
|
email: string;
|
|
597
|
-
/** Whether the
|
|
603
|
+
/** Whether the user has proved the address is theirs. */
|
|
598
604
|
verified: boolean;
|
|
599
605
|
/** `ACTIVE`, `SUSPENDED` or `PENDING`, as the platform holds it. */
|
|
600
606
|
status: string;
|
|
601
607
|
}
|
|
602
|
-
export interface
|
|
608
|
+
export interface AppUserList {
|
|
603
609
|
/** Part of an email address to look for. Absent lists everyone. */
|
|
604
610
|
search?: string;
|
|
605
611
|
/** How many to answer, at most. The platform's own page size when absent. */
|