@foir/sdk 0.10.0 → 0.12.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 +353 -2
- package/README.md +10 -2
- package/dist/app/client.d.ts +371 -0
- package/dist/app/contract/index.d.ts +23 -0
- package/dist/app/contract/proto/apps/v1/apps_pb.d.ts +1465 -0
- package/dist/app/contract/proto/apps/v1/discovery_pb.d.ts +678 -0
- package/dist/app/dispatch.d.ts +64 -0
- package/dist/app/errors.d.ts +50 -0
- package/dist/app/index.d.ts +40 -0
- package/dist/app/manifest.d.ts +56 -0
- package/dist/app/naming.d.ts +31 -0
- package/dist/app/record-key.d.ts +17 -0
- package/dist/app/ticket.d.ts +37 -0
- package/dist/app/vocabulary.generated.d.ts +20 -0
- package/dist/app-contract.js +124 -0
- package/dist/app.js +998 -0
- package/dist/auth/auth.d.ts +24 -6
- package/dist/auth/index.d.ts +2 -1
- package/dist/auth/ops.d.ts +8 -0
- package/dist/auth.js +230 -60
- package/dist/client.js +80 -15
- package/dist/codegen/preset.d.ts +3 -2
- package/dist/data/builtins.d.ts +17 -1
- package/dist/data/client.d.ts +10 -8
- package/dist/data/document.d.ts +13 -0
- package/dist/data/index.d.ts +15 -5
- package/dist/data/manifest.d.ts +31 -3
- package/dist/data/schema.d.ts +16 -9
- package/dist/data/selection.d.ts +15 -0
- package/dist/data/transport.d.ts +277 -23
- package/dist/data/ws-transport.d.ts +8 -2
- package/dist/data.js +313 -72
- package/dist/graphql/client.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/index.js +0 -9
- package/dist/live/drift.d.ts +9 -10
- package/dist/live.js +1 -1
- package/dist/next.js +158 -37
- package/dist/placement/index.d.ts +129 -0
- package/dist/placement.js +144 -0
- package/dist/push.js +212 -50
- package/dist/react.js +158 -37
- package/dist/server/callback-client.d.ts +30 -9
- package/dist/server/continue-run.d.ts +56 -0
- package/dist/server/index.d.ts +6 -3
- package/dist/server/operation-types.d.ts +114 -65
- package/dist/server/read-dispatch.d.ts +53 -0
- package/dist/server/require-scoped-token.d.ts +71 -7
- package/dist/server/verify-scoped-token.d.ts +109 -3
- package/dist/server.js +566 -138
- package/dist/transport/foir-http.d.ts +103 -5
- package/package.json +24 -6
package/dist/auth/auth.d.ts
CHANGED
|
@@ -213,7 +213,15 @@ export declare function registerOrganization(env: FoirEnv, vars: {
|
|
|
213
213
|
name: string;
|
|
214
214
|
parentId?: string;
|
|
215
215
|
}, options?: AuthOpOptions): Promise<MyOrganization>;
|
|
216
|
-
/**
|
|
216
|
+
/**
|
|
217
|
+
* Persist an auth result to the session; returns the customer identity.
|
|
218
|
+
*
|
|
219
|
+
* A result that names no customer keeps the identity already stored. A refresh
|
|
220
|
+
* rotates the tokens of a session that already knows who it belongs to, and a
|
|
221
|
+
* platform that answers a refresh without the customer must not blank that:
|
|
222
|
+
* {@link requireAuth} reads the stored identity, so a valid token beside an
|
|
223
|
+
* empty identity reads as signed out.
|
|
224
|
+
*/
|
|
217
225
|
export declare function saveSession(adapter: SessionAdapter, result: CustomerAuthResult): Promise<FoirCustomer>;
|
|
218
226
|
/** Clear all Foir auth keys from the session. */
|
|
219
227
|
export declare function clearSession(adapter: SessionAdapter): Promise<void>;
|
|
@@ -262,13 +270,16 @@ export declare function signOut(adapter: SessionAdapter, env: FoirEnv): Promise<
|
|
|
262
270
|
* or the refresh fails (the session is cleared in that case).
|
|
263
271
|
*/
|
|
264
272
|
export declare function refreshSession(adapter: SessionAdapter, env: FoirEnv): Promise<FoirAuthTokens | null>;
|
|
273
|
+
/** {@link refreshSession}, shared by every request of the session that asks while one is running. */
|
|
274
|
+
export declare function refreshSessionOnce(adapter: SessionAdapter, env: FoirEnv): Promise<FoirAuthTokens | null>;
|
|
265
275
|
/**
|
|
266
276
|
* Run a bearer-authed GraphQL operation with refresh-with-retry: uses the
|
|
267
|
-
* stored access token, and
|
|
268
|
-
*
|
|
269
|
-
* blips, server errors, validation — propagates without a
|
|
270
|
-
* mutation failures can't be duplicated.
|
|
271
|
-
*
|
|
277
|
+
* stored access token, and when the session lapsed (a 401-shaped failure, or an
|
|
278
|
+
* answer served without the token) refreshes it once and retries once. Any
|
|
279
|
+
* other error — network blips, server errors, validation — propagates without a
|
|
280
|
+
* retry so transient mutation failures can't be duplicated. A 401 with no
|
|
281
|
+
* refresh path throws the original error; a lapsed session that cannot be
|
|
282
|
+
* renewed throws {@link FoirSessionExpiredError}.
|
|
272
283
|
*/
|
|
273
284
|
export declare function authedRequest<TData, TVariables>(adapter: SessionAdapter, env: FoirEnv, document: TypedDocumentNode<TData, TVariables>, variables?: TVariables): Promise<TData>;
|
|
274
285
|
/**
|
|
@@ -299,6 +310,13 @@ export interface RequireAuthOptions {
|
|
|
299
310
|
* (e.g. `throw redirect(loginUrl)`). Must not return.
|
|
300
311
|
*/
|
|
301
312
|
onUnauthenticated: () => never | Promise<never>;
|
|
313
|
+
/**
|
|
314
|
+
* The environment to ask when a session holds a token but no identity. With
|
|
315
|
+
* it, the identity is read once from `currentUser` and saved, rather than a
|
|
316
|
+
* session whose token verifies being turned away. Without it, such a session
|
|
317
|
+
* is treated as signed out.
|
|
318
|
+
*/
|
|
319
|
+
env?: FoirEnv;
|
|
302
320
|
}
|
|
303
321
|
/**
|
|
304
322
|
* Require a signed-in customer. Resolves the {@link FoirUser} via
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* the caller's own manifest advertises, through the same op transport
|
|
11
11
|
* `@foir/sdk/data` uses, so `graphql` is not on this module's path at all.
|
|
12
12
|
*/
|
|
13
|
-
export { customerLogin, customerRegister, customerRequestOTP, customerLoginOTP, customerRefreshToken, customerLoginWithProvider, customerProviderCallback, customerProviderVerifyOTP, getAuthProviders, getAuthConfig, getDefaultAuthProvider, customerSignupContext, customerJoinWaitlist, customerRequestPasswordReset, customerResetPassword, customerUpdatePassword, customerVerifyEmail, currentUser, myProviderToken, customerLogout, deleteMyAccount, myOrganizations, selectOrganization, registerOrganization, saveSession, clearSession, getSessionToken, getSessionCustomer, loginWithPassword, registerWithPassword, loginWithOTP, resetPasswordAndSignIn, refreshSession, signOut, authedRequest, authedOp, requireAuth, requireAdmin, } from './auth';
|
|
13
|
+
export { customerLogin, customerRegister, customerRequestOTP, customerLoginOTP, customerRefreshToken, customerLoginWithProvider, customerProviderCallback, customerProviderVerifyOTP, getAuthProviders, getAuthConfig, getDefaultAuthProvider, customerSignupContext, customerJoinWaitlist, customerRequestPasswordReset, customerResetPassword, customerUpdatePassword, customerVerifyEmail, currentUser, myProviderToken, customerLogout, deleteMyAccount, myOrganizations, selectOrganization, registerOrganization, saveSession, clearSession, getSessionToken, getSessionCustomer, loginWithPassword, registerWithPassword, loginWithOTP, resetPasswordAndSignIn, refreshSession, refreshSessionOnce, signOut, authedRequest, authedOp, requireAuth, requireAdmin, } from './auth';
|
|
14
14
|
export type { FoirAuthTokens, FoirCustomer, FoirUser, SessionAdapter, RequireAuthOptions, RequireAdminOptions, } from './auth';
|
|
15
|
+
export { FoirAuthDowngradedError, FoirSessionExpiredError } from '../graphql/foir-request';
|
|
15
16
|
export { AUTH_OPS } from './ops';
|
|
16
17
|
export type { AuthOpName, AuthOpOptions } from './ops';
|
|
17
18
|
export { hostedLoginUrl, hostedLogoutUrl, fetchHostedSession } from './hosted';
|
package/dist/auth/ops.d.ts
CHANGED
|
@@ -81,6 +81,14 @@ export interface AuthOpOptions {
|
|
|
81
81
|
* already confined to an organization may only name its own.
|
|
82
82
|
*/
|
|
83
83
|
orgId?: string;
|
|
84
|
+
/**
|
|
85
|
+
* The organization the call acts in, named by the address of the page it is
|
|
86
|
+
* for, sent as `X-Org-Url`: same contract as `FoirClientConfig.orgUrl`. A
|
|
87
|
+
* storefront on a path per organization signs its customers in on those
|
|
88
|
+
* pages, and names the organization the way every other request from them
|
|
89
|
+
* does.
|
|
90
|
+
*/
|
|
91
|
+
orgUrl?: string;
|
|
84
92
|
/**
|
|
85
93
|
* `fetch` implementation. Defaults to `globalThis.fetch`. Injectable for
|
|
86
94
|
* tests and for runtimes that hand you a bound `fetch` (Hydrogen/Oxygen).
|
package/dist/auth.js
CHANGED
|
@@ -68,7 +68,12 @@ var FoirRequestError = class extends Error {
|
|
|
68
68
|
* failed on the first attempt for an hour.
|
|
69
69
|
*/
|
|
70
70
|
get retryable() {
|
|
71
|
-
if (this.status === void 0)
|
|
71
|
+
if (this.status === void 0) {
|
|
72
|
+
if (this.graphQLErrors?.length || this.prunedSelections?.length) {
|
|
73
|
+
return this.code === "RATE_LIMITED" || this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
72
77
|
if (this.status === 429 || this.code === "RATE_LIMITED") return true;
|
|
73
78
|
if (this.status >= 500) return true;
|
|
74
79
|
return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
@@ -103,19 +108,50 @@ function readFoirRetryAfterMs(headers) {
|
|
|
103
108
|
const seconds = Number(headers.get("Retry-After"));
|
|
104
109
|
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
105
110
|
}
|
|
111
|
+
var DISPATCH_RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
112
|
+
"x-api-key",
|
|
113
|
+
"authorization",
|
|
114
|
+
"x-foir-token",
|
|
115
|
+
"x-foir-context"
|
|
116
|
+
]);
|
|
117
|
+
function refuseCredentialHeaders(extra) {
|
|
118
|
+
if (!extra) return;
|
|
119
|
+
for (const name of Object.keys(extra)) {
|
|
120
|
+
if (DISPATCH_RESERVED_HEADERS.has(name.toLowerCase())) {
|
|
121
|
+
throw new FoirRequestError(
|
|
122
|
+
`[foir] a client built from a dispatch token cannot send "${name}" in \`headers\`. The dispatch token is the credential, and an api key or a bearer beside it would make the request two actors at once. Build a separate client for the other credential.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
106
128
|
function buildFoirHeaders(input) {
|
|
107
129
|
const headers = {
|
|
108
|
-
"Content-Type": "application/json"
|
|
109
|
-
"x-api-key": input.apiKey
|
|
130
|
+
"Content-Type": "application/json"
|
|
110
131
|
};
|
|
111
|
-
if (input.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
headers["
|
|
132
|
+
if (input.dispatchToken !== void 0) {
|
|
133
|
+
refuseCredentialHeaders(input.extra);
|
|
134
|
+
headers["X-Foir-Token"] = input.dispatchToken;
|
|
135
|
+
if (input.dispatchContext) {
|
|
136
|
+
headers["X-Foir-Context"] = input.dispatchContext;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
headers["x-api-key"] = input.apiKey;
|
|
140
|
+
if (input.draftChannel) {
|
|
141
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
142
|
+
}
|
|
143
|
+
if (input.bearerToken) {
|
|
144
|
+
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
145
|
+
}
|
|
146
|
+
if (input.orgId) {
|
|
147
|
+
headers["X-Org-Id"] = input.orgId;
|
|
148
|
+
}
|
|
149
|
+
if (input.orgUrl) {
|
|
150
|
+
headers["X-Org-Url"] = input.orgUrl;
|
|
151
|
+
}
|
|
152
|
+
if (input.visitorAddress) {
|
|
153
|
+
headers[FOIR_VISITOR_IP_HEADER] = input.visitorAddress;
|
|
154
|
+
}
|
|
119
155
|
}
|
|
120
156
|
if (input.extra) {
|
|
121
157
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -131,6 +167,26 @@ function foirApiEndpoint(apiUrl, path) {
|
|
|
131
167
|
}
|
|
132
168
|
return base + path;
|
|
133
169
|
}
|
|
170
|
+
var FoirAuthDowngradedError = class extends FoirRequestError {
|
|
171
|
+
downgrade;
|
|
172
|
+
constructor(downgrade) {
|
|
173
|
+
super(
|
|
174
|
+
`[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}), so the request was served as ${downgrade.servedAs}`,
|
|
175
|
+
{ graphQLErrors: [{ message: "customer token rejected", extensions: { code: "AUTH_DOWNGRADED" } }] }
|
|
176
|
+
);
|
|
177
|
+
this.name = "FoirAuthDowngradedError";
|
|
178
|
+
this.downgrade = downgrade;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
var FoirSessionExpiredError = class extends FoirRequestError {
|
|
182
|
+
constructor(options) {
|
|
183
|
+
super("[foir] the customer session has expired and could not be refreshed: sign in again", {
|
|
184
|
+
cause: options?.cause,
|
|
185
|
+
graphQLErrors: [{ message: "session expired", extensions: { code: "SESSION_EXPIRED" } }]
|
|
186
|
+
});
|
|
187
|
+
this.name = "FoirSessionExpiredError";
|
|
188
|
+
}
|
|
189
|
+
};
|
|
134
190
|
var warnedDowngrade = null;
|
|
135
191
|
function reportAuthDowngrade(envelope) {
|
|
136
192
|
const downgrade = envelope.extensions?.auth;
|
|
@@ -159,12 +215,18 @@ function refusePrunedDocument(json, options) {
|
|
|
159
215
|
`[foir] selections stripped from this query: ${selections.map(describe).join(", ")}`
|
|
160
216
|
);
|
|
161
217
|
}
|
|
218
|
+
function answeredAnything(data) {
|
|
219
|
+
return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
|
|
220
|
+
}
|
|
162
221
|
function readFoirEnvelope(json, options) {
|
|
222
|
+
if (options?.rejectDowngrade && json.extensions?.auth) {
|
|
223
|
+
throw new FoirAuthDowngradedError(json.extensions.auth);
|
|
224
|
+
}
|
|
163
225
|
reportAuthDowngrade(json);
|
|
164
226
|
refusePrunedDocument(json, options);
|
|
165
227
|
if (json.errors?.length) {
|
|
166
228
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
167
|
-
if (json.data && !options?.strict) {
|
|
229
|
+
if (answeredAnything(json.data) && !options?.strict) {
|
|
168
230
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
169
231
|
} else {
|
|
170
232
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
@@ -232,12 +294,13 @@ async function foirRequest(env, document, variables, options) {
|
|
|
232
294
|
apiKey,
|
|
233
295
|
draftChannel: isFoirPreview(env),
|
|
234
296
|
bearerToken: options?.bearerToken,
|
|
235
|
-
orgId: options?.orgId
|
|
297
|
+
orgId: options?.orgId,
|
|
298
|
+
orgUrl: options?.orgUrl
|
|
236
299
|
});
|
|
237
300
|
const documentText = print2(document);
|
|
238
301
|
let res;
|
|
239
302
|
try {
|
|
240
|
-
if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId)) {
|
|
303
|
+
if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId ?? options?.orgUrl)) {
|
|
241
304
|
const hash = await documentHash(documentText);
|
|
242
305
|
let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
|
|
243
306
|
method: "GET",
|
|
@@ -283,7 +346,7 @@ async function foirRequest(env, document, variables, options) {
|
|
|
283
346
|
);
|
|
284
347
|
}
|
|
285
348
|
const json = await res.json();
|
|
286
|
-
return readFoirEnvelope(json);
|
|
349
|
+
return readFoirEnvelope(json, { rejectDowngrade: options?.rejectDowngrade });
|
|
287
350
|
}
|
|
288
351
|
async function readErrorEnvelope(res) {
|
|
289
352
|
let errors;
|
|
@@ -788,15 +851,54 @@ function operationName(scope, op) {
|
|
|
788
851
|
function foirDataEndpoint(apiUrl, path) {
|
|
789
852
|
return foirApiEndpoint(apiUrl, path);
|
|
790
853
|
}
|
|
854
|
+
async function resolveVisitorAddress(config) {
|
|
855
|
+
const source = config.visitor;
|
|
856
|
+
const value = typeof source === "function" ? await source() : source;
|
|
857
|
+
const address = value?.trim();
|
|
858
|
+
return address ? address : void 0;
|
|
859
|
+
}
|
|
791
860
|
async function resolveBearerToken(config) {
|
|
792
861
|
const source = config.bearerToken;
|
|
793
862
|
if (typeof source === "function") return await source() ?? void 0;
|
|
794
863
|
return source ?? void 0;
|
|
795
864
|
}
|
|
865
|
+
async function resolveDispatchToken(dispatch) {
|
|
866
|
+
const token = (await dispatch.token())?.trim();
|
|
867
|
+
if (!token) {
|
|
868
|
+
throw new FoirRequestError(
|
|
869
|
+
"[foir] the dispatch token provider returned no token. A client built from a dispatch calls Foir as that operation and holds no other credential. Build it from the token the endpoint was handed: `(await requireScopedToken(request)).dispatch`."
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
return token;
|
|
873
|
+
}
|
|
874
|
+
async function resolveFoirAuth(config) {
|
|
875
|
+
if (config.dispatch !== void 0) {
|
|
876
|
+
return {
|
|
877
|
+
headers: buildFoirHeaders({
|
|
878
|
+
dispatchToken: await resolveDispatchToken(config.dispatch),
|
|
879
|
+
dispatchContext: config.dispatch.context ?? void 0,
|
|
880
|
+
extra: config.headers
|
|
881
|
+
})
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
const bearerToken = await resolveBearerToken(config);
|
|
885
|
+
return {
|
|
886
|
+
headers: buildFoirHeaders({
|
|
887
|
+
apiKey: config.apiKey,
|
|
888
|
+
draftChannel: config.channel === "draft",
|
|
889
|
+
bearerToken,
|
|
890
|
+
orgId: config.orgId,
|
|
891
|
+
orgUrl: config.orgUrl,
|
|
892
|
+
visitorAddress: await resolveVisitorAddress(config),
|
|
893
|
+
extra: config.headers
|
|
894
|
+
}),
|
|
895
|
+
key: { apiKey: config.apiKey, bearerToken }
|
|
896
|
+
};
|
|
897
|
+
}
|
|
796
898
|
function foirRoute(input) {
|
|
797
899
|
if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
|
|
798
900
|
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
799
|
-
if (input.orgId) return { lane: "origin", reason: "org" };
|
|
901
|
+
if (input.orgId || input.orgUrl) return { lane: "origin", reason: "org" };
|
|
800
902
|
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
801
903
|
if (input.writeToken) return { lane: "origin", reason: "floor" };
|
|
802
904
|
if (input.channel === "draft") return { lane: "origin", reason: "channel" };
|
|
@@ -811,25 +913,45 @@ async function executeFoirDocument(config, plan) {
|
|
|
811
913
|
}
|
|
812
914
|
return data[plan.rootField];
|
|
813
915
|
}
|
|
916
|
+
var renewing = /* @__PURE__ */ new WeakMap();
|
|
917
|
+
function renewBearerOnce(refresh) {
|
|
918
|
+
const running = renewing.get(refresh);
|
|
919
|
+
if (running) return running;
|
|
920
|
+
const next = refresh().finally(() => renewing.delete(refresh));
|
|
921
|
+
renewing.set(refresh, next);
|
|
922
|
+
return next;
|
|
923
|
+
}
|
|
814
924
|
async function runFoirDocument(config, document, mutation, variables, options) {
|
|
925
|
+
try {
|
|
926
|
+
return await sendFoirDocument(config, document, mutation, variables, options);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
const refresh = config.refreshBearer;
|
|
929
|
+
if (!(err instanceof FoirAuthDowngradedError) || !refresh || err.downgrade.reason === "wrong_project") {
|
|
930
|
+
throw err;
|
|
931
|
+
}
|
|
932
|
+
const token = err.downgrade.reason === "expired" ? await renewBearerOnce(refresh) : null;
|
|
933
|
+
if (!token) throw new FoirSessionExpiredError({ cause: err });
|
|
934
|
+
try {
|
|
935
|
+
return await sendFoirDocument({ ...config, bearerToken: token }, document, mutation, variables, options);
|
|
936
|
+
} catch (again) {
|
|
937
|
+
if (again instanceof FoirAuthDowngradedError) throw new FoirSessionExpiredError({ cause: again });
|
|
938
|
+
throw again;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
async function sendFoirDocument(config, document, mutation, variables, options) {
|
|
815
943
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
816
944
|
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
817
|
-
const
|
|
818
|
-
const headers = buildFoirHeaders({
|
|
819
|
-
apiKey: config.apiKey,
|
|
820
|
-
draftChannel: config.channel === "draft",
|
|
821
|
-
bearerToken,
|
|
822
|
-
orgId: config.orgId,
|
|
823
|
-
extra: config.headers
|
|
824
|
-
});
|
|
945
|
+
const { headers, key } = await resolveFoirAuth(config);
|
|
825
946
|
const hash = await documentHash(document);
|
|
826
|
-
const
|
|
827
|
-
apiKey:
|
|
828
|
-
bearerToken,
|
|
947
|
+
const sharedKey = key !== void 0 && foirRoute({
|
|
948
|
+
apiKey: key.apiKey,
|
|
949
|
+
bearerToken: key.bearerToken,
|
|
829
950
|
orgId: config.orgId,
|
|
951
|
+
orgUrl: config.orgUrl,
|
|
830
952
|
mutation,
|
|
831
953
|
channel: config.channel
|
|
832
|
-
});
|
|
954
|
+
}).lane === "shared" ? key.apiKey : void 0;
|
|
833
955
|
const post = (body) => doFetch(endpoint, {
|
|
834
956
|
method: "POST",
|
|
835
957
|
headers,
|
|
@@ -839,8 +961,8 @@ async function runFoirDocument(config, document, mutation, variables, options) {
|
|
|
839
961
|
const extensions = persistedQueryExtension(hash);
|
|
840
962
|
let res;
|
|
841
963
|
try {
|
|
842
|
-
if (
|
|
843
|
-
res = await doFetch(buildCdnUrl(endpoint,
|
|
964
|
+
if (sharedKey !== void 0) {
|
|
965
|
+
res = await doFetch(buildCdnUrl(endpoint, sharedKey, hash, variables), {
|
|
844
966
|
method: "GET",
|
|
845
967
|
headers,
|
|
846
968
|
...config.requestInit
|
|
@@ -866,7 +988,7 @@ async function runFoirDocument(config, document, mutation, variables, options) {
|
|
|
866
988
|
});
|
|
867
989
|
}
|
|
868
990
|
const json = await res.json();
|
|
869
|
-
return readFoirEnvelope(json, options);
|
|
991
|
+
return readFoirEnvelope(json, { ...options, rejectDowngrade: Boolean(key?.bearerToken) });
|
|
870
992
|
}
|
|
871
993
|
async function readErrorBody(res) {
|
|
872
994
|
try {
|
|
@@ -878,20 +1000,16 @@ async function readErrorBody(res) {
|
|
|
878
1000
|
}
|
|
879
1001
|
|
|
880
1002
|
// src/data/manifest.ts
|
|
881
|
-
async function fetchFoirManifest(config) {
|
|
882
|
-
const
|
|
1003
|
+
async function fetchFoirManifest(config, options = {}) {
|
|
1004
|
+
const base = foirDataEndpoint(config.apiUrl, "/v1/manifest");
|
|
1005
|
+
const url = options.operation === void 0 ? base : `${base}?operation=${encodeURIComponent(options.operation)}`;
|
|
883
1006
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
1007
|
+
const { headers } = await resolveFoirAuth(config);
|
|
884
1008
|
let res;
|
|
885
1009
|
try {
|
|
886
1010
|
res = await doFetch(url, {
|
|
887
1011
|
method: "GET",
|
|
888
|
-
headers
|
|
889
|
-
apiKey: config.apiKey,
|
|
890
|
-
draftChannel: config.channel === "draft",
|
|
891
|
-
bearerToken: await resolveBearerToken(config),
|
|
892
|
-
orgId: config.orgId,
|
|
893
|
-
extra: config.headers
|
|
894
|
-
}),
|
|
1012
|
+
headers,
|
|
895
1013
|
...config.requestInit
|
|
896
1014
|
});
|
|
897
1015
|
} catch (err) {
|
|
@@ -963,6 +1081,7 @@ function authClientConfig(env, options) {
|
|
|
963
1081
|
channel: isFoirPreview(env) ? "draft" : "published",
|
|
964
1082
|
bearerToken: options?.bearerToken,
|
|
965
1083
|
orgId: options?.orgId,
|
|
1084
|
+
orgUrl: options?.orgUrl,
|
|
966
1085
|
headers: options?.headers,
|
|
967
1086
|
fetch: options?.fetch
|
|
968
1087
|
};
|
|
@@ -1083,6 +1202,9 @@ async function registerOrganization(env, vars, options) {
|
|
|
1083
1202
|
async function saveSession(adapter, result) {
|
|
1084
1203
|
await adapter.set(TOKEN_KEY, result.token);
|
|
1085
1204
|
await adapter.set(REFRESH_KEY, result.refreshToken);
|
|
1205
|
+
if (!result.customerId) {
|
|
1206
|
+
return await getSessionCustomer(adapter) ?? { id: "", email: "" };
|
|
1207
|
+
}
|
|
1086
1208
|
await adapter.set(CUSTOMER_ID_KEY, result.customerId);
|
|
1087
1209
|
await adapter.set(EMAIL_KEY, result.email);
|
|
1088
1210
|
return { id: result.customerId, email: result.email };
|
|
@@ -1143,35 +1265,68 @@ function isAuthExpiredError(err) {
|
|
|
1143
1265
|
(e) => /authentication required/i.test(e.message)
|
|
1144
1266
|
);
|
|
1145
1267
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1268
|
+
var refreshing = /* @__PURE__ */ new WeakMap();
|
|
1269
|
+
function refreshSessionOnce(adapter, env) {
|
|
1270
|
+
const running = refreshing.get(adapter);
|
|
1271
|
+
if (running) return running;
|
|
1272
|
+
const next = refreshSession(adapter, env).finally(() => refreshing.delete(adapter));
|
|
1273
|
+
refreshing.set(adapter, next);
|
|
1274
|
+
return next;
|
|
1275
|
+
}
|
|
1276
|
+
function sessionLapse(err) {
|
|
1277
|
+
if (err instanceof FoirAuthDowngradedError) {
|
|
1278
|
+
switch (err.downgrade.reason) {
|
|
1279
|
+
case "expired":
|
|
1280
|
+
return "refresh";
|
|
1281
|
+
case "invalid":
|
|
1282
|
+
return "expired";
|
|
1283
|
+
default:
|
|
1284
|
+
return null;
|
|
1285
|
+
}
|
|
1159
1286
|
}
|
|
1287
|
+
return isAuthExpiredError(err) ? "refresh" : null;
|
|
1160
1288
|
}
|
|
1161
|
-
async function
|
|
1289
|
+
async function withSessionRefresh(adapter, env, run) {
|
|
1162
1290
|
const token = await getSessionToken(adapter);
|
|
1163
1291
|
try {
|
|
1164
|
-
return await
|
|
1292
|
+
return await run(token ?? void 0);
|
|
1165
1293
|
} catch (err) {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
if (
|
|
1169
|
-
|
|
1294
|
+
const lapse = sessionLapse(err);
|
|
1295
|
+
if (!lapse) throw err;
|
|
1296
|
+
if (lapse === "expired") throw new FoirSessionExpiredError({ cause: err });
|
|
1297
|
+
const refreshed = await refreshSessionOnce(adapter, env);
|
|
1298
|
+
if (!refreshed) {
|
|
1299
|
+
if (err instanceof FoirAuthDowngradedError) throw new FoirSessionExpiredError({ cause: err });
|
|
1300
|
+
throw err;
|
|
1301
|
+
}
|
|
1302
|
+
try {
|
|
1303
|
+
return await run(refreshed.token);
|
|
1304
|
+
} catch (again) {
|
|
1305
|
+
if (again instanceof FoirAuthDowngradedError) throw new FoirSessionExpiredError({ cause: again });
|
|
1306
|
+
throw again;
|
|
1307
|
+
}
|
|
1170
1308
|
}
|
|
1171
1309
|
}
|
|
1310
|
+
async function authedRequest(adapter, env, document, variables) {
|
|
1311
|
+
return withSessionRefresh(
|
|
1312
|
+
adapter,
|
|
1313
|
+
env,
|
|
1314
|
+
(bearerToken) => foirRequest(env, document, variables, { bearerToken, rejectDowngrade: true })
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
async function authedOp(adapter, env, op, args) {
|
|
1318
|
+
return withSessionRefresh(
|
|
1319
|
+
adapter,
|
|
1320
|
+
env,
|
|
1321
|
+
(bearerToken) => authOp(env, op, args, { bearerToken })
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1172
1324
|
async function requireAuth(adapter, options) {
|
|
1173
1325
|
const token = await getSessionToken(adapter);
|
|
1174
|
-
|
|
1326
|
+
let customer = await getSessionCustomer(adapter);
|
|
1327
|
+
if (token && !customer && options.env) {
|
|
1328
|
+
customer = await recoverSessionCustomer(adapter, options.env, token);
|
|
1329
|
+
}
|
|
1175
1330
|
if (!token || !customer) {
|
|
1176
1331
|
await options.onUnauthenticated();
|
|
1177
1332
|
throw new Error("[foir] onUnauthenticated must not return");
|
|
@@ -1181,6 +1336,18 @@ async function requireAuth(adapter, options) {
|
|
|
1181
1336
|
}
|
|
1182
1337
|
return { id: customer.id, email: customer.email, isAdmin: false };
|
|
1183
1338
|
}
|
|
1339
|
+
async function recoverSessionCustomer(adapter, env, token) {
|
|
1340
|
+
let me;
|
|
1341
|
+
try {
|
|
1342
|
+
me = await currentUser(env, { bearerToken: token });
|
|
1343
|
+
} catch {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
if (!me?.id) return null;
|
|
1347
|
+
await adapter.set(CUSTOMER_ID_KEY, me.id);
|
|
1348
|
+
await adapter.set(EMAIL_KEY, me.email);
|
|
1349
|
+
return { id: me.id, email: me.email };
|
|
1350
|
+
}
|
|
1184
1351
|
async function requireAdmin(adapter, options) {
|
|
1185
1352
|
const user = await requireAuth(adapter, options);
|
|
1186
1353
|
if (!user.isAdmin) {
|
|
@@ -1400,6 +1567,8 @@ function trimSlash(s) {
|
|
|
1400
1567
|
}
|
|
1401
1568
|
export {
|
|
1402
1569
|
AUTH_OPS,
|
|
1570
|
+
FoirAuthDowngradedError,
|
|
1571
|
+
FoirSessionExpiredError,
|
|
1403
1572
|
authedOp,
|
|
1404
1573
|
authedRequest,
|
|
1405
1574
|
clearSession,
|
|
@@ -1434,6 +1603,7 @@ export {
|
|
|
1434
1603
|
myOrganizations,
|
|
1435
1604
|
myProviderToken,
|
|
1436
1605
|
refreshSession,
|
|
1606
|
+
refreshSessionOnce,
|
|
1437
1607
|
registerOrganization,
|
|
1438
1608
|
registerWithPassword,
|
|
1439
1609
|
requireAdmin,
|
package/dist/client.js
CHANGED
|
@@ -68,7 +68,12 @@ var FoirRequestError = class extends Error {
|
|
|
68
68
|
* failed on the first attempt for an hour.
|
|
69
69
|
*/
|
|
70
70
|
get retryable() {
|
|
71
|
-
if (this.status === void 0)
|
|
71
|
+
if (this.status === void 0) {
|
|
72
|
+
if (this.graphQLErrors?.length || this.prunedSelections?.length) {
|
|
73
|
+
return this.code === "RATE_LIMITED" || this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
72
77
|
if (this.status === 429 || this.code === "RATE_LIMITED") return true;
|
|
73
78
|
if (this.status >= 500) return true;
|
|
74
79
|
return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
@@ -115,19 +120,50 @@ function readFoirRetryAfterMs(headers) {
|
|
|
115
120
|
const seconds = Number(headers.get("Retry-After"));
|
|
116
121
|
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
117
122
|
}
|
|
123
|
+
var DISPATCH_RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
124
|
+
"x-api-key",
|
|
125
|
+
"authorization",
|
|
126
|
+
"x-foir-token",
|
|
127
|
+
"x-foir-context"
|
|
128
|
+
]);
|
|
129
|
+
function refuseCredentialHeaders(extra) {
|
|
130
|
+
if (!extra) return;
|
|
131
|
+
for (const name of Object.keys(extra)) {
|
|
132
|
+
if (DISPATCH_RESERVED_HEADERS.has(name.toLowerCase())) {
|
|
133
|
+
throw new FoirRequestError(
|
|
134
|
+
`[foir] a client built from a dispatch token cannot send "${name}" in \`headers\`. The dispatch token is the credential, and an api key or a bearer beside it would make the request two actors at once. Build a separate client for the other credential.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
118
140
|
function buildFoirHeaders(input) {
|
|
119
141
|
const headers = {
|
|
120
|
-
"Content-Type": "application/json"
|
|
121
|
-
"x-api-key": input.apiKey
|
|
142
|
+
"Content-Type": "application/json"
|
|
122
143
|
};
|
|
123
|
-
if (input.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
headers["
|
|
144
|
+
if (input.dispatchToken !== void 0) {
|
|
145
|
+
refuseCredentialHeaders(input.extra);
|
|
146
|
+
headers["X-Foir-Token"] = input.dispatchToken;
|
|
147
|
+
if (input.dispatchContext) {
|
|
148
|
+
headers["X-Foir-Context"] = input.dispatchContext;
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
headers["x-api-key"] = input.apiKey;
|
|
152
|
+
if (input.draftChannel) {
|
|
153
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
154
|
+
}
|
|
155
|
+
if (input.bearerToken) {
|
|
156
|
+
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
157
|
+
}
|
|
158
|
+
if (input.orgId) {
|
|
159
|
+
headers["X-Org-Id"] = input.orgId;
|
|
160
|
+
}
|
|
161
|
+
if (input.orgUrl) {
|
|
162
|
+
headers["X-Org-Url"] = input.orgUrl;
|
|
163
|
+
}
|
|
164
|
+
if (input.visitorAddress) {
|
|
165
|
+
headers[FOIR_VISITOR_IP_HEADER] = input.visitorAddress;
|
|
166
|
+
}
|
|
131
167
|
}
|
|
132
168
|
if (input.extra) {
|
|
133
169
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -136,6 +172,26 @@ function buildFoirHeaders(input) {
|
|
|
136
172
|
}
|
|
137
173
|
return headers;
|
|
138
174
|
}
|
|
175
|
+
var FoirAuthDowngradedError = class extends FoirRequestError {
|
|
176
|
+
downgrade;
|
|
177
|
+
constructor(downgrade) {
|
|
178
|
+
super(
|
|
179
|
+
`[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}), so the request was served as ${downgrade.servedAs}`,
|
|
180
|
+
{ graphQLErrors: [{ message: "customer token rejected", extensions: { code: "AUTH_DOWNGRADED" } }] }
|
|
181
|
+
);
|
|
182
|
+
this.name = "FoirAuthDowngradedError";
|
|
183
|
+
this.downgrade = downgrade;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
var FoirSessionExpiredError = class extends FoirRequestError {
|
|
187
|
+
constructor(options) {
|
|
188
|
+
super("[foir] the customer session has expired and could not be refreshed: sign in again", {
|
|
189
|
+
cause: options?.cause,
|
|
190
|
+
graphQLErrors: [{ message: "session expired", extensions: { code: "SESSION_EXPIRED" } }]
|
|
191
|
+
});
|
|
192
|
+
this.name = "FoirSessionExpiredError";
|
|
193
|
+
}
|
|
194
|
+
};
|
|
139
195
|
var warnedDowngrade = null;
|
|
140
196
|
function reportAuthDowngrade(envelope) {
|
|
141
197
|
const downgrade = envelope.extensions?.auth;
|
|
@@ -164,12 +220,18 @@ function refusePrunedDocument(json, options) {
|
|
|
164
220
|
`[foir] selections stripped from this query: ${selections.map(describe).join(", ")}`
|
|
165
221
|
);
|
|
166
222
|
}
|
|
223
|
+
function answeredAnything(data) {
|
|
224
|
+
return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
|
|
225
|
+
}
|
|
167
226
|
function readFoirEnvelope(json, options) {
|
|
227
|
+
if (options?.rejectDowngrade && json.extensions?.auth) {
|
|
228
|
+
throw new FoirAuthDowngradedError(json.extensions.auth);
|
|
229
|
+
}
|
|
168
230
|
reportAuthDowngrade(json);
|
|
169
231
|
refusePrunedDocument(json, options);
|
|
170
232
|
if (json.errors?.length) {
|
|
171
233
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
172
|
-
if (json.data && !options?.strict) {
|
|
234
|
+
if (answeredAnything(json.data) && !options?.strict) {
|
|
173
235
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
174
236
|
} else {
|
|
175
237
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
@@ -271,12 +333,13 @@ async function foirRequest(env, document, variables, options) {
|
|
|
271
333
|
apiKey,
|
|
272
334
|
draftChannel: isFoirPreview(env),
|
|
273
335
|
bearerToken: options?.bearerToken,
|
|
274
|
-
orgId: options?.orgId
|
|
336
|
+
orgId: options?.orgId,
|
|
337
|
+
orgUrl: options?.orgUrl
|
|
275
338
|
});
|
|
276
339
|
const documentText = print2(document);
|
|
277
340
|
let res;
|
|
278
341
|
try {
|
|
279
|
-
if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId)) {
|
|
342
|
+
if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId ?? options?.orgUrl)) {
|
|
280
343
|
const hash = await documentHash(documentText);
|
|
281
344
|
let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
|
|
282
345
|
method: "GET",
|
|
@@ -322,7 +385,7 @@ async function foirRequest(env, document, variables, options) {
|
|
|
322
385
|
);
|
|
323
386
|
}
|
|
324
387
|
const json = await res.json();
|
|
325
|
-
return readFoirEnvelope(json);
|
|
388
|
+
return readFoirEnvelope(json, { rejectDowngrade: options?.rejectDowngrade });
|
|
326
389
|
}
|
|
327
390
|
async function readErrorEnvelope(res) {
|
|
328
391
|
let errors;
|
|
@@ -375,7 +438,9 @@ function createFoirContentClient(config) {
|
|
|
375
438
|
}
|
|
376
439
|
export {
|
|
377
440
|
FOIR_CDN_BASE,
|
|
441
|
+
FoirAuthDowngradedError,
|
|
378
442
|
FoirRequestError,
|
|
443
|
+
FoirSessionExpiredError,
|
|
379
444
|
absolutiseFoirMediaUrl,
|
|
380
445
|
createFoirContentClient,
|
|
381
446
|
foirRequest,
|