@foir/sdk 0.11.0 → 0.13.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +165 -2
  2. package/dist/app/client.d.ts +445 -0
  3. package/dist/app/contract/index.d.ts +23 -0
  4. package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +1465 -0
  5. package/dist/app/contract/proto/apps/v1/discovery_pb.d.ts +678 -0
  6. package/dist/app/dispatch.d.ts +64 -0
  7. package/dist/app/errors.d.ts +50 -0
  8. package/dist/app/index.d.ts +40 -0
  9. package/dist/app/manifest.d.ts +56 -0
  10. package/dist/app/naming.d.ts +31 -0
  11. package/dist/app/record-key.d.ts +17 -0
  12. package/dist/app/ticket.d.ts +37 -0
  13. package/dist/app/vocabulary.generated.d.ts +20 -0
  14. package/dist/app-contract.js +124 -0
  15. package/dist/app.js +1106 -0
  16. package/dist/auth/auth.d.ts +24 -6
  17. package/dist/auth/index.d.ts +2 -1
  18. package/dist/auth.js +139 -23
  19. package/dist/client.js +36 -2
  20. package/dist/data/index.d.ts +2 -2
  21. package/dist/data/transport.d.ts +34 -2
  22. package/dist/data.js +77 -2
  23. package/dist/graphql/client.d.ts +1 -1
  24. package/dist/graphql/foir-request.d.ts +1 -1
  25. package/dist/next.js +70 -2
  26. package/dist/placement/index.d.ts +129 -0
  27. package/dist/placement.js +144 -0
  28. package/dist/push.js +121 -13
  29. package/dist/react.js +70 -2
  30. package/dist/server/callback-client.d.ts +30 -9
  31. package/dist/server/continue-run.d.ts +56 -0
  32. package/dist/server/index.d.ts +5 -3
  33. package/dist/server/operation-types.d.ts +31 -8
  34. package/dist/server/require-scoped-token.d.ts +52 -7
  35. package/dist/server/verify-scoped-token.d.ts +109 -3
  36. package/dist/server.js +415 -127
  37. package/dist/transport/foir-http.d.ts +48 -0
  38. package/package.json +24 -6
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The bodies the platform sends an app's operation endpoint.
3
+ *
4
+ * `readDispatch` (`@foir/sdk/server`) reads the body an operation run is
5
+ * queued with. An app's endpoint is reached two more ways, each with a body of
6
+ * its own, and all three arrive at the same route:
7
+ *
8
+ * a queued run `{ executionId, operationKey, trigger, input, context,
9
+ * callback?, continuation?, ... }`: every ASYNC run,
10
+ * and every run a hook, a schedule or `execute<Op>`
11
+ * starts.
12
+ * a direct call `{ project, app, operation, triggered_by,
13
+ * execution_id, input? }`: an operator running a SYNC
14
+ * operation from the console, discovery during setup,
15
+ * and the uninstall teardown. Nothing is queued, so
16
+ * `execution_id` is the id of the token the call
17
+ * carried and no execution holds it.
18
+ * the credential probe `{ project, app, operation, values }`: the reserved
19
+ * `__validate_credentials` operation, sent the
20
+ * credential an operator is about to save.
21
+ *
22
+ * `readAppDispatch` reads the first two as one shape. `readCredentialProbe`
23
+ * reads the third.
24
+ */
25
+ import type { OperationCallback, OperationContinuationBlock } from '../server/operation-types';
26
+ /** An app operation's dispatch, whichever way it arrived. */
27
+ export interface AppDispatch {
28
+ /** The run's id on a queued run. On a direct call, the id of the token it carried. Absent when the body named neither. */
29
+ executionId?: string;
30
+ /** The operation's key as the platform stores it, `<app>/<key>` for an app's own. For logging: route on your URL. */
31
+ operationKey?: string;
32
+ /** How the run was started: a trigger type on a queued run (`api`, `lifecycle`...), or what the caller named (`manual`, `uninstall`). */
33
+ triggeredBy?: string;
34
+ /** What the caller passed. `null` when it passed nothing. */
35
+ input: unknown;
36
+ /** Where to report an ASYNC run's outcome. Present only on a queued ASYNC run. */
37
+ callback?: OperationCallback;
38
+ /** Where this slice of a run resumes. See `continueRun`. */
39
+ continuation?: OperationContinuationBlock;
40
+ /** The body as it was sent, for a block this reader has no name for. */
41
+ raw: Record<string, unknown>;
42
+ }
43
+ /**
44
+ * Read an app operation's dispatch body, already parsed from JSON.
45
+ *
46
+ * It does not refuse a body it cannot place: a direct call may carry no input
47
+ * and no id, and an endpoint that answered 4xx to one would fail an operator's
48
+ * action over a field it does not need. What is absent is absent on the
49
+ * answer.
50
+ */
51
+ export declare function readAppDispatch(body: unknown): AppDispatch;
52
+ /**
53
+ * Read the credential probe's body: the values an operator submitted, by the
54
+ * manifest's credential field names. Every value the credential store accepts
55
+ * is a string, so anything else is left out rather than handed on as a
56
+ * surprise.
57
+ *
58
+ * The probe carries no `X-Foir-Context`, and whatever answers it with a
59
+ * non-2xx blocks the operator's save. So answer 400 only for a value they can
60
+ * retype, and 200 for everything else, including a check you could not run.
61
+ */
62
+ export declare function readCredentialProbe(body: unknown): {
63
+ values: Record<string, string>;
64
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * What the platform said when it refused an app's call.
3
+ *
4
+ * Every call an app client makes fails with the package's one error,
5
+ * `FoirRequestError`, carrying the platform's own answer. Nothing is checked
6
+ * before the call: the token's capabilities are the platform's to hold it to,
7
+ * and its refusal names the reason. The readers here pick out the refusals an
8
+ * app acts on rather than reports, as data and never by the wording of a
9
+ * message.
10
+ */
11
+ /** A write refused because another record holds its key on one of the model's lookups. */
12
+ export interface AppLookupConflict {
13
+ model: string;
14
+ lookup: string;
15
+ /** The fields the lookup is keyed by, as the model declares them. */
16
+ keyBy: string[];
17
+ }
18
+ /**
19
+ * The lookup a refused write collided on, or null for any other failure.
20
+ *
21
+ * The usual cause is a vendor that lets its users change the value a record is
22
+ * keyed on: written under the new key, the object is refused because its own
23
+ * id already belongs to a record. That record is the object under its old key,
24
+ * and `records.rename` moves it.
25
+ */
26
+ export declare function appLookupConflictOf(error: unknown): AppLookupConflict | null;
27
+ /**
28
+ * A save to a customer's record refused because no customer holds the
29
+ * identity at the install's level and no email came to make one, or null for
30
+ * any other failure. Nothing is wrong with the row: the customer is known once
31
+ * they sign in with the sign-in the install pairs with the source.
32
+ */
33
+ export declare function appCustomerUnlinkedOf(error: unknown): {
34
+ model: string;
35
+ identityProvider: string;
36
+ } | null;
37
+ /**
38
+ * Whether a read failed because the token is not served the field it asked
39
+ * for: the field is not on the schema this token's capabilities reach. That is
40
+ * the platform's refusal of a read, as data: the selection it stripped.
41
+ *
42
+ * `field` narrows it to one root field. The platform names a stripped
43
+ * selection by its path from the operation, which leads with the operation's
44
+ * name (`query` when it has none), so a root field is the path's second and
45
+ * last step.
46
+ *
47
+ * A refused WRITE is not answered here: the platform refuses a mutation
48
+ * outright, with its reason in the error.
49
+ */
50
+ export declare function isAppFieldWithheld(error: unknown, field?: string): boolean;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @foir/sdk/app: what an app installed from the marketplace is built with.
3
+ *
4
+ * An app is a server Foir calls: it serves a manifest, and the platform
5
+ * dispatches its operations, its webhooks and its credential probe to it, each
6
+ * carrying a token for one install. This subpath is the app's half of that
7
+ * conversation, beside `@foir/sdk/server`, which holds the half every
8
+ * operation endpoint shares (verifying the token, the callback, continuing a
9
+ * run).
10
+ *
11
+ * createAppClient call Foir as the install, with the token a dispatch
12
+ * carried: its configuration, credential and status,
13
+ * records of the models it maps, customers and
14
+ * notifications.
15
+ * readAppDispatch the body of a dispatch, whichever way it arrived,
16
+ * readCredentialProbe and of the credential probe.
17
+ * redeemExchangeTicket the token an OAuth callback writes a credential with.
18
+ * validateAppManifest ask the platform whether it would accept a manifest.
19
+ * APP_CAPABILITIES… the capabilities and hook events the contract lists.
20
+ *
21
+ * The manifest's own messages are on `@foir/sdk/app/contract`.
22
+ */
23
+ export { createAppClient } from './client';
24
+ export type { AppBinding, AppClient, AppClientConfig, AppConsentChange, AppCustomerEraseResult, AppCustomerSaveResult, AppFieldMapping, AppInstallConfig, AppNotification, AppOperationStart, AppRecord, AppRecordList, AppStartedOperation, AppStatusReport, AppSyncDirection, AppUpsertResult, AppWhoami, } from './client';
25
+ export { appCustomerUnlinkedOf, appLookupConflictOf, isAppFieldWithheld } from './errors';
26
+ export type { AppLookupConflict } from './errors';
27
+ export { appModelFieldName, appModelTypeName, appSelectorEnumValue } from './naming';
28
+ export { RECORD_KEY_RULE, recordKeyRefusal } from './record-key';
29
+ export { readAppDispatch, readCredentialProbe } from './dispatch';
30
+ export type { AppDispatch } from './dispatch';
31
+ export { redeemExchangeTicket } from './ticket';
32
+ export type { RedeemedExchangeTicket } from './ticket';
33
+ export { validateAppManifest, MAX_APP_MANIFEST_BYTES } from './manifest';
34
+ export type { AppManifestIssue, AppManifestValidation, ValidateAppManifestOptions } from './manifest';
35
+ export { APP_CAPABILITIES, APP_HOOK_EVENTS, APP_RECORD_CAPABILITY_PREFIXES } from './vocabulary.generated';
36
+ export type { AppCapability, AppHookEvent, AppRecordCapabilityPrefix } from './vocabulary.generated';
37
+ export type { FoirDispatchCredential } from '../data/transport';
38
+ export { FoirRequestError } from '../transport/foir-http';
39
+ export type { FoirErrorCode, FoirErrorExtensions, FoirGraphQLErrorItem, FoirPrunedSelection } from '../transport/foir-http';
40
+ export type { FoirRetryPolicy } from '../transport/retry';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Ask the platform whether it would accept an app's manifest.
3
+ *
4
+ * A manifest is held to rules only the platform knows in full: which
5
+ * capabilities exist, which events a hook may name, how a feature's storage
6
+ * may be declared, what an install would refuse. Copying those rules into an
7
+ * app's build means the copy and the platform disagree the first time either
8
+ * changes. So the build asks instead. `validateAppManifest` sends the manifest
9
+ * to the platform's own validation, as a dry run: nothing is registered and
10
+ * nothing is stored.
11
+ *
12
+ * ```ts
13
+ * const { ok, issues } = await validateAppManifest({ apiUrl, apiKey, manifest });
14
+ * if (!ok) throw new Error(issues.map((issue) => `${issue.path}: ${issue.message}`).join('\n'));
15
+ * ```
16
+ *
17
+ * Any credential the data API accepts may ask, a publishable key included: the
18
+ * check reads only the manifest it is handed.
19
+ */
20
+ import type { FoirDispatchCredential } from '../data/transport';
21
+ /** One thing validation found, at the path in the manifest it found it. */
22
+ export interface AppManifestIssue {
23
+ /** ERROR: an install refuses the manifest over it. WARNING: it does not. */
24
+ severity: 'ERROR' | 'WARNING';
25
+ /** Where in the manifest, as a dotted path of its own field names. */
26
+ path: string;
27
+ message: string;
28
+ }
29
+ export interface AppManifestValidation {
30
+ /** True when no issue is an ERROR. */
31
+ ok: boolean;
32
+ issues: AppManifestIssue[];
33
+ }
34
+ /** The largest manifest the platform reads, in bytes. */
35
+ export declare const MAX_APP_MANIFEST_BYTES: number;
36
+ export type ValidateAppManifestOptions = {
37
+ /** The data API's origin, e.g. `https://api.foir.dev`. */
38
+ apiUrl: string;
39
+ /** The manifest: the object, or the JSON you serve. */
40
+ manifest: string | Record<string, unknown>;
41
+ /** Defaults to the global `fetch`. */
42
+ fetch?: typeof globalThis.fetch;
43
+ requestInit?: Omit<RequestInit, 'method' | 'headers' | 'body'>;
44
+ } & ({
45
+ apiKey: string;
46
+ dispatch?: never;
47
+ } | {
48
+ dispatch: FoirDispatchCredential;
49
+ apiKey?: never;
50
+ });
51
+ /**
52
+ * Validate a manifest against the platform's rules. Resolves with what the
53
+ * platform found; throws `FoirRequestError` only when the question could not
54
+ * be asked or answered.
55
+ */
56
+ export declare function validateAppManifest(options: ValidateAppManifestOptions): Promise<AppManifestValidation>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The data API's names for a model's own fields.
3
+ *
4
+ * A project's schema gives each model a set of typed fields, named from the
5
+ * model's key: the singular query, `delete<Type>`, `<Type>WhereInput` and the
6
+ * rest. A client generated for one project reads those names off the schema.
7
+ * An app installed in many projects cannot, because the model is whichever one
8
+ * each operator mapped, so it derives them from the key the same way the
9
+ * schema does.
10
+ *
11
+ * The rule: split the key on every run of characters that are not letters or
12
+ * digits, upper-case the first character of each part, and join. The rest of a
13
+ * part is left as written, so `shopifyCustomer` stays `ShopifyCustomer`.
14
+ */
15
+ /** The type name of a model: `blog_post` is `BlogPost`. */
16
+ export declare function appModelTypeName(modelKey: string): string;
17
+ /** The name of a model's singular query: `blog_post` is `blogPost`. */
18
+ export declare function appModelFieldName(modelKey: string): string;
19
+ /**
20
+ * A field key as a value of a selector enum (an aggregate's group-by):
21
+ * upper-cased, each run of anything that is not a letter or digit made one
22
+ * underscore, and led by an underscore when it would start with a digit. So
23
+ * `variantId` is `VARIANTID`.
24
+ */
25
+ export declare function appSelectorEnumValue(fieldKey: string): string;
26
+ /**
27
+ * Whether a model field key can be selected as written. A key that is not a
28
+ * legal GraphQL name is not on the type at all, and asking for it refuses the
29
+ * whole document.
30
+ */
31
+ export declare function isSelectableFieldKey(fieldKey: string): boolean;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The rule for a record's key.
3
+ *
4
+ * A record's key is its address: the console routes to it and every client
5
+ * builds a link from it. So a key is one URL-safe path segment (letters,
6
+ * digits, "-", "_", "." and "~"), and never "." or "..", which a browser
7
+ * resolves away before the request leaves it. Every write refuses anything
8
+ * else, naming the key.
9
+ *
10
+ * It is stated here for an app that BUILDS keys, from a vendor's ids and
11
+ * handles: it can tell which of them can be a key as written, and encode the
12
+ * ones that cannot, instead of finding out from a refused write.
13
+ */
14
+ /** The rule in words. */
15
+ export declare const RECORD_KEY_RULE = "a record's key is one URL-safe segment: letters, digits, \"-\", \"_\", \".\" and \"~\", and never \".\" or \"..\"";
16
+ /** Why a key would be refused, in the platform's words, or null for a key that is fine. */
17
+ export declare function recordKeyRefusal(key: string): string | null;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The exchange ticket: how an app's own OAuth callback gets to write the
3
+ * credential it was just granted.
4
+ *
5
+ * An install that needs a vendor's consent sends the operator to the app's
6
+ * authorize route with a single-use ticket. When the vendor returns them, the
7
+ * app redeems the ticket for a short-lived token that can do one thing: write
8
+ * this install's credential. It cannot read the configuration, report status
9
+ * or read the credential back, and it cannot be renewed.
10
+ *
11
+ * ```ts
12
+ * const { dispatch } = await redeemExchangeTicket({ apiUrl, ticket });
13
+ * await createAppClient({ apiUrl, dispatch }).install.writeCredentials(values);
14
+ * ```
15
+ *
16
+ * Holding the ticket is the whole of the proof, so carry it only inside a
17
+ * state you signed and never in a URL you did not.
18
+ */
19
+ import type { FoirDispatchCredential } from '../data/transport';
20
+ export interface RedeemedExchangeTicket {
21
+ /** The token, as the credential for `createAppClient`. */
22
+ dispatch: FoirDispatchCredential;
23
+ /** When the token stops being accepted, as the platform stated it. */
24
+ expiresAt: string;
25
+ }
26
+ /**
27
+ * Redeem an exchange ticket. ONE ATTEMPT, on purpose: the ticket is single-use,
28
+ * so repeating a call whose answer was lost would find the ticket spent, and
29
+ * the operator's only way forward would be to start the install again. A
30
+ * failure here is the answer.
31
+ */
32
+ export declare function redeemExchangeTicket(options: {
33
+ apiUrl: string;
34
+ ticket: string;
35
+ fetch?: typeof globalThis.fetch;
36
+ requestInit?: Omit<RequestInit, 'method' | 'headers' | 'body'>;
37
+ }): Promise<RedeemedExchangeTicket>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Every event the platform emits, as a hook names it. Which of them a
3
+ * manifest's hook may subscribe to (an event of the project, that something
4
+ * emits) is the platform's to say: `validateAppManifest` answers it.
5
+ */
6
+ export declare const APP_HOOK_EVENTS: readonly ["RECORD_CREATED", "RECORD_UPDATED", "RECORD_DELETED", "RECORD_PUBLISHED", "RECORD_UNPUBLISHED", "CUSTOMER_CREATED", "CUSTOMER_UPDATED", "CUSTOMER_DELETED", "CUSTOMER_MERGED", "SESSION_REVOKED", "OPERATION_COMPLETED", "OPERATION_FAILED", "OPERATION_CANCELLED", "OPERATION_TIMED_OUT", "ADMIN_MEMBERSHIP_GRANTED", "ADMIN_MEMBERSHIP_REVOKED", "ADMIN_ROLE_CHANGED", "TENANT_PLAN_CHANGED", "TENANT_SUBSCRIPTION_STATUS_CHANGED", "PROJECT_CREATED", "PROJECT_ARCHIVED", "INVITATION_ACCEPTED", "IMPORT_COMPLETED", "MIGRATION_APPLIED", "BULK_UPDATE_COMPLETED", "BULK_PUBLISH_COMPLETED", "CUSTOMER_PREFERENCE_CHANGED", "ORGANIZATION_CREATED", "ORGANIZATION_UPDATED", "ORGANIZATION_MOVED", "ORGANIZATION_SUSPENDED", "ORGANIZATION_RESUMED", "ORGANIZATION_DELETION_SCHEDULED", "ORGANIZATION_DELETION_CANCELLED", "ORGANIZATION_DELETED", "ORGANIZATION_MEMBER_ADDED", "ORGANIZATION_MEMBER_REMOVED", "ORGANIZATION_MEMBER_ROLE_CHANGED"];
7
+ /** An event a hook can name. */
8
+ export type AppHookEvent = (typeof APP_HOOK_EVENTS)[number];
9
+ /** The capabilities an operation declares as written. */
10
+ export declare const APP_CAPABILITIES: readonly ["config:read", "secrets:read.app", "secrets:put.app", "secrets:list.app", "secrets:delete.app", "status:write", "files:read", "files:list", "files:write", "customers:write", "notifications:write", "notifications:consent", "operations:complete"];
11
+ /** A capability an operation declares as written. */
12
+ export type AppCapability = (typeof APP_CAPABILITIES)[number];
13
+ /**
14
+ * The capabilities an operation declares once per model. Each is a prefix: a
15
+ * model key follows it, or `$<name>` for the model an install maps one of the
16
+ * app's sources or sinks onto.
17
+ */
18
+ export declare const APP_RECORD_CAPABILITY_PREFIXES: readonly ["records:read:", "records:write:", "records:delete:", "records:publish:", "records:attribute:"];
19
+ /** A per-model capability's prefix. */
20
+ export type AppRecordCapabilityPrefix = (typeof APP_RECORD_CAPABILITY_PREFIXES)[number];