@foir/sdk 0.30.0 → 0.31.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 +14 -0
- package/README.md +1 -1
- package/dist/ai/index.d.ts +10 -15
- package/dist/ai.js +185 -8
- package/dist/app/client.d.ts +9 -5
- package/dist/app.js +12 -13
- package/dist/auth/auth.d.ts +80 -101
- package/dist/auth/hosted.d.ts +12 -12
- package/dist/auth/index.d.ts +11 -7
- package/dist/auth/operations.d.ts +17 -32
- package/dist/auth/ops.d.ts +4 -13
- package/dist/auth/session-client.d.ts +4 -4
- package/dist/auth/steps.d.ts +50 -0
- package/dist/auth-session.js +3 -3
- package/dist/auth.js +157 -75
- package/dist/content/content-renderer.d.ts +2 -2
- package/dist/content/richtext.d.ts +2 -2
- package/dist/content.js +7 -7
- package/dist/data/builtins.d.ts +64 -33
- 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/selection.d.ts +3 -3
- package/dist/data.js +5 -2
- package/dist/next/index.d.ts +1 -1
- package/dist/next/session.d.ts +27 -11
- package/dist/next.js +98 -40
- package/dist/push.js +9 -19
- package/dist/react/FoirProvider.d.ts +18 -16
- package/dist/react/feedback-notes.d.ts +1 -2
- package/dist/react.js +167 -84
- package/dist/runs/step-reply.d.ts +81 -0
- package/dist/server/callback-client.d.ts +11 -16
- package/dist/server/index.d.ts +2 -0
- package/dist/server/operation-types.d.ts +6 -13
- package/dist/server.js +76 -107
- package/dist/transport/foir-http.d.ts +20 -6
- package/package.json +3 -3
|
@@ -83,28 +83,21 @@ export interface OperationContinuationBlock {
|
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Callback context the platform hands the endpoint when it dispatches an
|
|
86
|
-
* ASYNC operation. Everything the endpoint needs to
|
|
87
|
-
*
|
|
88
|
-
* mutation names, so the endpoint derives none of them from the operation key.
|
|
86
|
+
* ASYNC operation. Everything the endpoint needs to reply is here: where, the
|
|
87
|
+
* token, and the key this run signs with.
|
|
89
88
|
*/
|
|
90
89
|
export interface OperationCallback {
|
|
91
90
|
/**
|
|
92
|
-
*
|
|
91
|
+
* Where the run replies: its own reply route, e.g.
|
|
92
|
+
* https://api.foir.dev/v1/steps/<executionId>/reply. Empty when the
|
|
93
93
|
* platform that dispatched has no public address of its own configured;
|
|
94
|
-
* `createCallbackClient` then
|
|
94
|
+
* `createCallbackClient` then replies at the `apiUrl` it is given.
|
|
95
95
|
*/
|
|
96
|
-
|
|
96
|
+
endpoint: string;
|
|
97
97
|
/** Bearer token for the Authorization header (Ed25519, single execution scope) */
|
|
98
98
|
token: string;
|
|
99
99
|
/** RFC3339 expiry — extensions can short-circuit work past this. */
|
|
100
100
|
expiresAt: string;
|
|
101
|
-
/** Per-op typed mutation names the extension should call back with. */
|
|
102
|
-
mutations: {
|
|
103
|
-
complete: string;
|
|
104
|
-
fail: string;
|
|
105
|
-
progress: string;
|
|
106
|
-
cancel: string;
|
|
107
|
-
};
|
|
108
101
|
/**
|
|
109
102
|
* The operation keys that led to this run, its own last. Present when the
|
|
110
103
|
* run was caused by another operation's work.
|
package/dist/server.js
CHANGED
|
@@ -730,8 +730,11 @@ function stepUpRequired(error) {
|
|
|
730
730
|
if (!(error instanceof FoirRequestError)) return null;
|
|
731
731
|
for (const item of error.graphQLErrors ?? []) {
|
|
732
732
|
if (item.extensions?.code !== "STEP_UP_REQUIRED") continue;
|
|
733
|
-
const
|
|
734
|
-
if (
|
|
733
|
+
const kind = item.extensions.stepUp;
|
|
734
|
+
if (kind === "proof" || kind === "second_factor" || kind === "sign_in_method") {
|
|
735
|
+
const url = item.extensions.stepUpUrl;
|
|
736
|
+
return { kind, url: typeof url === "string" && url !== "" ? url : null };
|
|
737
|
+
}
|
|
735
738
|
}
|
|
736
739
|
return null;
|
|
737
740
|
}
|
|
@@ -952,131 +955,96 @@ async function withFoirRetry(fn, policy = {}) {
|
|
|
952
955
|
}
|
|
953
956
|
}
|
|
954
957
|
|
|
958
|
+
// src/runs/step-reply.ts
|
|
959
|
+
async function replyToStep(reply, body, opts = {}) {
|
|
960
|
+
if (!reply?.token || !reply.signingKey || !reply.endpoint) {
|
|
961
|
+
throw new Error("replyToStep: the step was handed no reply, so it cannot answer");
|
|
962
|
+
}
|
|
963
|
+
return sendReply(reply.endpoint, reply.token, reply.signingKey, body, opts);
|
|
964
|
+
}
|
|
965
|
+
async function sendReply(endpoint, token, signingKey, body, opts = {}) {
|
|
966
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
967
|
+
if (!fetchImpl) throw new Error("replyToStep: no fetch available; pass opts.fetch on runtimes without a global fetch");
|
|
968
|
+
const payload = JSON.stringify(body);
|
|
969
|
+
const signature = await hmacSha256Hex(signingKey, payload);
|
|
970
|
+
return withFoirRetry(async () => {
|
|
971
|
+
let resp;
|
|
972
|
+
try {
|
|
973
|
+
resp = await fetchImpl(endpoint, {
|
|
974
|
+
method: "POST",
|
|
975
|
+
headers: {
|
|
976
|
+
"Content-Type": "application/json",
|
|
977
|
+
Authorization: `Bearer ${token}`,
|
|
978
|
+
"X-Foir-Signature": `sha256=${signature}`
|
|
979
|
+
},
|
|
980
|
+
body: payload,
|
|
981
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
982
|
+
});
|
|
983
|
+
} catch (err) {
|
|
984
|
+
if (opts.signal?.aborted) throw err;
|
|
985
|
+
throw new FoirRequestError("the reply got no answer", { cause: err });
|
|
986
|
+
}
|
|
987
|
+
if (!resp.ok) {
|
|
988
|
+
throw new FoirRequestError(`the reply was refused with HTTP ${resp.status}: ${await resp.text()}`, { status: resp.status });
|
|
989
|
+
}
|
|
990
|
+
const out = await resp.json();
|
|
991
|
+
if (!out || typeof out.status !== "string") {
|
|
992
|
+
throw new FoirRequestError("the reply was answered with nothing", { status: resp.status });
|
|
993
|
+
}
|
|
994
|
+
return { status: out.status, cancelled: out.cancelled === true, applied: out.applied === true };
|
|
995
|
+
}, opts.retry);
|
|
996
|
+
}
|
|
997
|
+
async function hmacSha256Hex(secret, body) {
|
|
998
|
+
const enc = new TextEncoder();
|
|
999
|
+
const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
1000
|
+
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(body));
|
|
1001
|
+
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1002
|
+
}
|
|
1003
|
+
|
|
955
1004
|
// src/server/callback-client.ts
|
|
956
1005
|
function createCallbackClient(callback, opts) {
|
|
957
1006
|
const { executionId } = opts;
|
|
958
|
-
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
959
|
-
if (!fetchImpl) {
|
|
960
|
-
throw new Error(
|
|
961
|
-
"createCallbackClient: no fetch available; pass opts.fetch on runtimes without a global fetch"
|
|
962
|
-
);
|
|
963
|
-
}
|
|
964
1007
|
if (!executionId) throw new Error("createCallbackClient: executionId is required");
|
|
965
|
-
if (!callback?.token
|
|
1008
|
+
if (!callback?.token) {
|
|
966
1009
|
throw new Error("createCallbackClient: callback block is missing required fields");
|
|
967
1010
|
}
|
|
968
|
-
if (!callback.
|
|
1011
|
+
if (!callback.endpoint && !opts.apiUrl) {
|
|
969
1012
|
throw new Error(
|
|
970
|
-
"createCallbackClient: nowhere to
|
|
1013
|
+
"createCallbackClient: nowhere to reply. The dispatch named no callback.endpoint and no apiUrl was passed."
|
|
971
1014
|
);
|
|
972
1015
|
}
|
|
973
|
-
const endpoint = callback.
|
|
1016
|
+
const endpoint = callback.endpoint || foirApiEndpoint(opts.apiUrl ?? "", `/v1/steps/${encodeURIComponent(executionId)}/reply`);
|
|
974
1017
|
const signingSecret = opts.signingSecret || callback.signingKey;
|
|
975
1018
|
if (!signingSecret) {
|
|
976
1019
|
throw new Error(
|
|
977
1020
|
"createCallbackClient: nothing to sign with. The dispatch carried no callback.signingKey and no signingSecret was passed."
|
|
978
1021
|
);
|
|
979
1022
|
}
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
});
|
|
986
|
-
const signature = await hmacSha256Hex(signingSecret, body);
|
|
987
|
-
return withFoirRetry(async () => {
|
|
988
|
-
let resp;
|
|
989
|
-
try {
|
|
990
|
-
resp = await fetchImpl(endpoint, {
|
|
991
|
-
method: "POST",
|
|
992
|
-
headers: {
|
|
993
|
-
"Content-Type": "application/json",
|
|
994
|
-
Authorization: `Bearer ${callback.token}`,
|
|
995
|
-
"X-Foir-Signature": `sha256=${signature}`
|
|
996
|
-
},
|
|
997
|
-
body,
|
|
998
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
999
|
-
});
|
|
1000
|
-
} catch (err) {
|
|
1001
|
-
if (opts.signal?.aborted) throw err;
|
|
1002
|
-
throw new FoirRequestError(`callback ${mutationName} got no answer`, { cause: err });
|
|
1003
|
-
}
|
|
1004
|
-
if (!resp.ok) {
|
|
1005
|
-
throw new FoirRequestError(
|
|
1006
|
-
`callback ${mutationName} failed with HTTP ${resp.status}: ${await resp.text()}`,
|
|
1007
|
-
{ status: resp.status }
|
|
1008
|
-
);
|
|
1009
|
-
}
|
|
1010
|
-
const json = await resp.json();
|
|
1011
|
-
if (json.errors?.length) {
|
|
1012
|
-
throw new FoirRequestError(`callback ${mutationName} GraphQL error: ${json.errors[0].message}`, {
|
|
1013
|
-
status: resp.status,
|
|
1014
|
-
graphQLErrors: json.errors
|
|
1015
|
-
});
|
|
1016
|
-
}
|
|
1017
|
-
const out = json.data?.[mutationName];
|
|
1018
|
-
if (!out) {
|
|
1019
|
-
throw new FoirRequestError(`callback ${mutationName} returned no data`, { status: resp.status });
|
|
1020
|
-
}
|
|
1021
|
-
return out;
|
|
1022
|
-
}, opts.retry);
|
|
1023
|
-
};
|
|
1023
|
+
const send = (body) => sendReply(endpoint, callback.token, signingSecret, body, {
|
|
1024
|
+
...opts.fetch ? { fetch: opts.fetch } : {},
|
|
1025
|
+
...opts.retry ? { retry: opts.retry } : {},
|
|
1026
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
1027
|
+
});
|
|
1024
1028
|
return {
|
|
1025
|
-
complete: (result) =>
|
|
1026
|
-
fail: (code, message, options = {}) =>
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1029
|
+
complete: (result) => send({ result: result ?? null }),
|
|
1030
|
+
fail: (code, message, options = {}) => send({
|
|
1031
|
+
failure: {
|
|
1032
|
+
code,
|
|
1033
|
+
message,
|
|
1034
|
+
...options.retryable === void 0 ? {} : { retryable: options.retryable },
|
|
1035
|
+
...options.details === void 0 ? {} : { details: options.details }
|
|
1036
|
+
}
|
|
1031
1037
|
}),
|
|
1032
|
-
progress: (update) =>
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1038
|
+
progress: (update) => send({
|
|
1039
|
+
progress: {
|
|
1040
|
+
...update.pct === void 0 ? {} : { percent: update.pct },
|
|
1041
|
+
...update.message === void 0 ? {} : { message: update.message },
|
|
1042
|
+
...update.metadata === void 0 ? {} : { metadata: update.metadata }
|
|
1043
|
+
}
|
|
1036
1044
|
}),
|
|
1037
|
-
cancel: () =>
|
|
1045
|
+
cancel: () => send({ cancel: {} })
|
|
1038
1046
|
};
|
|
1039
1047
|
}
|
|
1040
|
-
function fieldArgsForMutation(mutationName, vars) {
|
|
1041
|
-
const parts = [];
|
|
1042
|
-
for (const key of Object.keys(vars)) {
|
|
1043
|
-
if (vars[key] === void 0) continue;
|
|
1044
|
-
parts.push(`, $${key}: ${graphQLTypeFor(mutationName, key)}`);
|
|
1045
|
-
}
|
|
1046
|
-
return parts.join("");
|
|
1047
|
-
}
|
|
1048
|
-
function fieldArgsCallSite(_mutationName, vars) {
|
|
1049
|
-
const parts = [];
|
|
1050
|
-
for (const key of Object.keys(vars)) {
|
|
1051
|
-
if (vars[key] === void 0) continue;
|
|
1052
|
-
parts.push(`, ${key}: $${key}`);
|
|
1053
|
-
}
|
|
1054
|
-
return parts.join("");
|
|
1055
|
-
}
|
|
1056
|
-
function graphQLTypeFor(mutationName, key) {
|
|
1057
|
-
if (key === "code" || key === "message") return "String!";
|
|
1058
|
-
if (key === "pct") return "Int";
|
|
1059
|
-
if (key === "retryable") return "Boolean";
|
|
1060
|
-
if (key === "details" || key === "metadata") return "JSON";
|
|
1061
|
-
if (key === "result") {
|
|
1062
|
-
const base = mutationName.replace(/^complete/, "").replace(/Execution$/, "");
|
|
1063
|
-
return `${base}ExecutionResultInput!`;
|
|
1064
|
-
}
|
|
1065
|
-
if (key === "message") return "String";
|
|
1066
|
-
return "String";
|
|
1067
|
-
}
|
|
1068
|
-
async function hmacSha256Hex(secret, body) {
|
|
1069
|
-
const enc = new TextEncoder();
|
|
1070
|
-
const key = await crypto.subtle.importKey(
|
|
1071
|
-
"raw",
|
|
1072
|
-
enc.encode(secret),
|
|
1073
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
1074
|
-
false,
|
|
1075
|
-
["sign"]
|
|
1076
|
-
);
|
|
1077
|
-
const sig = await crypto.subtle.sign("HMAC", key, enc.encode(body));
|
|
1078
|
-
return Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1079
|
-
}
|
|
1080
1048
|
|
|
1081
1049
|
// src/transport/key-kind.ts
|
|
1082
1050
|
var KEY_PREFIXES = [
|
|
@@ -1836,6 +1804,7 @@ export {
|
|
|
1836
1804
|
recordsCreateModels,
|
|
1837
1805
|
recordsReadModels,
|
|
1838
1806
|
recordsWriteModels,
|
|
1807
|
+
replyToStep,
|
|
1839
1808
|
requireScopedToken,
|
|
1840
1809
|
stepUpRequired,
|
|
1841
1810
|
verifyScopedToken,
|
|
@@ -62,7 +62,17 @@ export type FoirErrorCode = 'INVALID_ARGUMENT' | 'NOT_FOUND' | 'ALREADY_EXISTS'
|
|
|
62
62
|
* flow) and retry; `sign_in_method`, sign in again with a method the
|
|
63
63
|
* organization the request stands in accepts.
|
|
64
64
|
*/
|
|
65
|
-
export type FoirStepUp = 'second_factor' | 'sign_in_method';
|
|
65
|
+
export type FoirStepUp = 'proof' | 'second_factor' | 'sign_in_method';
|
|
66
|
+
/** A step-up a refusal asks for, and the hosted page to prove it on. */
|
|
67
|
+
export interface FoirStepUpAsk {
|
|
68
|
+
kind: FoirStepUp;
|
|
69
|
+
/**
|
|
70
|
+
* Fóir's hosted step-up page, where the refusal names one: send the user
|
|
71
|
+
* there with `return_to` added to its query, and ask again when they come
|
|
72
|
+
* back. Its token is in the fragment.
|
|
73
|
+
*/
|
|
74
|
+
url: string | null;
|
|
75
|
+
}
|
|
66
76
|
export interface FoirErrorExtensions {
|
|
67
77
|
/** Stable machine-readable code. Branch on this, never on `message`. */
|
|
68
78
|
code?: FoirErrorCode | string;
|
|
@@ -72,6 +82,8 @@ export interface FoirErrorExtensions {
|
|
|
72
82
|
retryAfter?: number;
|
|
73
83
|
/** On `STEP_UP_REQUIRED`: what the signed-in user must do before retrying. */
|
|
74
84
|
stepUp?: FoirStepUp;
|
|
85
|
+
/** On `STEP_UP_REQUIRED`, where the refusal names one: the hosted step-up page. */
|
|
86
|
+
stepUpUrl?: string;
|
|
75
87
|
[key: string]: unknown;
|
|
76
88
|
}
|
|
77
89
|
export interface FoirGraphQLErrorItem {
|
|
@@ -126,11 +138,13 @@ export interface FoirRateLimitSnapshot {
|
|
|
126
138
|
}
|
|
127
139
|
/**
|
|
128
140
|
* What an error asks of the signed-in user before the request can succeed, or
|
|
129
|
-
* null:
|
|
130
|
-
*
|
|
131
|
-
*
|
|
141
|
+
* null: a change to their factors needs a fresh proof (`proof`), or an
|
|
142
|
+
* organization's sign-in rules turned the session away, because it requires a
|
|
143
|
+
* second factor the session has not proven (`second_factor`) or does not
|
|
144
|
+
* accept how the user signed in (`sign_in_method`). `url` is the hosted page
|
|
145
|
+
* to prove it on, where the refusal names one.
|
|
132
146
|
*/
|
|
133
|
-
export declare function stepUpRequired(error: unknown):
|
|
147
|
+
export declare function stepUpRequired(error: unknown): FoirStepUpAsk | null;
|
|
134
148
|
export declare class FoirRequestError extends Error {
|
|
135
149
|
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
136
150
|
readonly status?: number;
|
|
@@ -224,7 +238,7 @@ export interface FoirRequestOptions {
|
|
|
224
238
|
/**
|
|
225
239
|
* User bearer token. api-public still needs the `x-api-key` to resolve
|
|
226
240
|
* the project; the bearer adds the user identity for self-scoped reads
|
|
227
|
-
* (e.g. `myProfile`). Used by `@foir/sdk/auth`'s
|
|
241
|
+
* (e.g. `myProfile`). Used by `@foir/sdk/auth`'s signed-in profile fetch.
|
|
228
242
|
*/
|
|
229
243
|
bearerToken?: string;
|
|
230
244
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foir/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Official Foir SDK — editor iframe client + server-side platform helpers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -186,8 +186,8 @@
|
|
|
186
186
|
"ws": "^8.18.0",
|
|
187
187
|
"y-websocket": "^3.0.0",
|
|
188
188
|
"yjs": "^13.6.30",
|
|
189
|
-
"@foir/
|
|
190
|
-
"@foir/
|
|
189
|
+
"@foir/access-vocab": "0.0.0",
|
|
190
|
+
"@foir/proto-ts": "0.109.0"
|
|
191
191
|
},
|
|
192
192
|
"engines": {
|
|
193
193
|
"node": ">=18.0.0"
|