@foir/sdk 0.11.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 +131 -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.js +139 -23
- package/dist/client.js +36 -2
- package/dist/data/index.d.ts +2 -2
- package/dist/data/transport.d.ts +34 -2
- package/dist/data.js +77 -2
- package/dist/graphql/client.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/next.js +70 -2
- package/dist/placement/index.d.ts +129 -0
- package/dist/placement.js +144 -0
- package/dist/push.js +121 -13
- package/dist/react.js +70 -2
- package/dist/server/callback-client.d.ts +30 -9
- package/dist/server/continue-run.d.ts +56 -0
- package/dist/server/index.d.ts +5 -3
- package/dist/server/operation-types.d.ts +28 -7
- package/dist/server/require-scoped-token.d.ts +52 -7
- package/dist/server/verify-scoped-token.d.ts +109 -3
- package/dist/server.js +415 -127
- package/dist/transport/foir-http.d.ts +48 -0
- 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.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";
|
|
@@ -119,6 +124,7 @@ function refuseCredentialHeaders(extra) {
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
}
|
|
127
|
+
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
122
128
|
function buildFoirHeaders(input) {
|
|
123
129
|
const headers = {
|
|
124
130
|
"Content-Type": "application/json"
|
|
@@ -143,6 +149,9 @@ function buildFoirHeaders(input) {
|
|
|
143
149
|
if (input.orgUrl) {
|
|
144
150
|
headers["X-Org-Url"] = input.orgUrl;
|
|
145
151
|
}
|
|
152
|
+
if (input.visitorAddress) {
|
|
153
|
+
headers[FOIR_VISITOR_IP_HEADER] = input.visitorAddress;
|
|
154
|
+
}
|
|
146
155
|
}
|
|
147
156
|
if (input.extra) {
|
|
148
157
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -158,6 +167,26 @@ function foirApiEndpoint(apiUrl, path) {
|
|
|
158
167
|
}
|
|
159
168
|
return base + path;
|
|
160
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
|
+
};
|
|
161
190
|
var warnedDowngrade = null;
|
|
162
191
|
function reportAuthDowngrade(envelope) {
|
|
163
192
|
const downgrade = envelope.extensions?.auth;
|
|
@@ -190,6 +219,9 @@ function answeredAnything(data) {
|
|
|
190
219
|
return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
|
|
191
220
|
}
|
|
192
221
|
function readFoirEnvelope(json, options) {
|
|
222
|
+
if (options?.rejectDowngrade && json.extensions?.auth) {
|
|
223
|
+
throw new FoirAuthDowngradedError(json.extensions.auth);
|
|
224
|
+
}
|
|
193
225
|
reportAuthDowngrade(json);
|
|
194
226
|
refusePrunedDocument(json, options);
|
|
195
227
|
if (json.errors?.length) {
|
|
@@ -314,7 +346,7 @@ async function foirRequest(env, document, variables, options) {
|
|
|
314
346
|
);
|
|
315
347
|
}
|
|
316
348
|
const json = await res.json();
|
|
317
|
-
return readFoirEnvelope(json);
|
|
349
|
+
return readFoirEnvelope(json, { rejectDowngrade: options?.rejectDowngrade });
|
|
318
350
|
}
|
|
319
351
|
async function readErrorEnvelope(res) {
|
|
320
352
|
let errors;
|
|
@@ -819,6 +851,12 @@ function operationName(scope, op) {
|
|
|
819
851
|
function foirDataEndpoint(apiUrl, path) {
|
|
820
852
|
return foirApiEndpoint(apiUrl, path);
|
|
821
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
|
+
}
|
|
822
860
|
async function resolveBearerToken(config) {
|
|
823
861
|
const source = config.bearerToken;
|
|
824
862
|
if (typeof source === "function") return await source() ?? void 0;
|
|
@@ -851,6 +889,7 @@ async function resolveFoirAuth(config) {
|
|
|
851
889
|
bearerToken,
|
|
852
890
|
orgId: config.orgId,
|
|
853
891
|
orgUrl: config.orgUrl,
|
|
892
|
+
visitorAddress: await resolveVisitorAddress(config),
|
|
854
893
|
extra: config.headers
|
|
855
894
|
}),
|
|
856
895
|
key: { apiKey: config.apiKey, bearerToken }
|
|
@@ -874,7 +913,33 @@ async function executeFoirDocument(config, plan) {
|
|
|
874
913
|
}
|
|
875
914
|
return data[plan.rootField];
|
|
876
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
|
+
}
|
|
877
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) {
|
|
878
943
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
879
944
|
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
880
945
|
const { headers, key } = await resolveFoirAuth(config);
|
|
@@ -923,7 +988,7 @@ async function runFoirDocument(config, document, mutation, variables, options) {
|
|
|
923
988
|
});
|
|
924
989
|
}
|
|
925
990
|
const json = await res.json();
|
|
926
|
-
return readFoirEnvelope(json, options);
|
|
991
|
+
return readFoirEnvelope(json, { ...options, rejectDowngrade: Boolean(key?.bearerToken) });
|
|
927
992
|
}
|
|
928
993
|
async function readErrorBody(res) {
|
|
929
994
|
try {
|
|
@@ -1137,6 +1202,9 @@ async function registerOrganization(env, vars, options) {
|
|
|
1137
1202
|
async function saveSession(adapter, result) {
|
|
1138
1203
|
await adapter.set(TOKEN_KEY, result.token);
|
|
1139
1204
|
await adapter.set(REFRESH_KEY, result.refreshToken);
|
|
1205
|
+
if (!result.customerId) {
|
|
1206
|
+
return await getSessionCustomer(adapter) ?? { id: "", email: "" };
|
|
1207
|
+
}
|
|
1140
1208
|
await adapter.set(CUSTOMER_ID_KEY, result.customerId);
|
|
1141
1209
|
await adapter.set(EMAIL_KEY, result.email);
|
|
1142
1210
|
return { id: result.customerId, email: result.email };
|
|
@@ -1197,35 +1265,68 @@ function isAuthExpiredError(err) {
|
|
|
1197
1265
|
(e) => /authentication required/i.test(e.message)
|
|
1198
1266
|
);
|
|
1199
1267
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
+
}
|
|
1213
1286
|
}
|
|
1287
|
+
return isAuthExpiredError(err) ? "refresh" : null;
|
|
1214
1288
|
}
|
|
1215
|
-
async function
|
|
1289
|
+
async function withSessionRefresh(adapter, env, run) {
|
|
1216
1290
|
const token = await getSessionToken(adapter);
|
|
1217
1291
|
try {
|
|
1218
|
-
return await
|
|
1292
|
+
return await run(token ?? void 0);
|
|
1219
1293
|
} catch (err) {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
if (
|
|
1223
|
-
|
|
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
|
+
}
|
|
1224
1308
|
}
|
|
1225
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
|
+
}
|
|
1226
1324
|
async function requireAuth(adapter, options) {
|
|
1227
1325
|
const token = await getSessionToken(adapter);
|
|
1228
|
-
|
|
1326
|
+
let customer = await getSessionCustomer(adapter);
|
|
1327
|
+
if (token && !customer && options.env) {
|
|
1328
|
+
customer = await recoverSessionCustomer(adapter, options.env, token);
|
|
1329
|
+
}
|
|
1229
1330
|
if (!token || !customer) {
|
|
1230
1331
|
await options.onUnauthenticated();
|
|
1231
1332
|
throw new Error("[foir] onUnauthenticated must not return");
|
|
@@ -1235,6 +1336,18 @@ async function requireAuth(adapter, options) {
|
|
|
1235
1336
|
}
|
|
1236
1337
|
return { id: customer.id, email: customer.email, isAdmin: false };
|
|
1237
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
|
+
}
|
|
1238
1351
|
async function requireAdmin(adapter, options) {
|
|
1239
1352
|
const user = await requireAuth(adapter, options);
|
|
1240
1353
|
if (!user.isAdmin) {
|
|
@@ -1454,6 +1567,8 @@ function trimSlash(s) {
|
|
|
1454
1567
|
}
|
|
1455
1568
|
export {
|
|
1456
1569
|
AUTH_OPS,
|
|
1570
|
+
FoirAuthDowngradedError,
|
|
1571
|
+
FoirSessionExpiredError,
|
|
1457
1572
|
authedOp,
|
|
1458
1573
|
authedRequest,
|
|
1459
1574
|
clearSession,
|
|
@@ -1488,6 +1603,7 @@ export {
|
|
|
1488
1603
|
myOrganizations,
|
|
1489
1604
|
myProviderToken,
|
|
1490
1605
|
refreshSession,
|
|
1606
|
+
refreshSessionOnce,
|
|
1491
1607
|
registerOrganization,
|
|
1492
1608
|
registerWithPassword,
|
|
1493
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";
|
|
@@ -131,6 +136,7 @@ function refuseCredentialHeaders(extra) {
|
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
}
|
|
139
|
+
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
134
140
|
function buildFoirHeaders(input) {
|
|
135
141
|
const headers = {
|
|
136
142
|
"Content-Type": "application/json"
|
|
@@ -155,6 +161,9 @@ function buildFoirHeaders(input) {
|
|
|
155
161
|
if (input.orgUrl) {
|
|
156
162
|
headers["X-Org-Url"] = input.orgUrl;
|
|
157
163
|
}
|
|
164
|
+
if (input.visitorAddress) {
|
|
165
|
+
headers[FOIR_VISITOR_IP_HEADER] = input.visitorAddress;
|
|
166
|
+
}
|
|
158
167
|
}
|
|
159
168
|
if (input.extra) {
|
|
160
169
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -163,6 +172,26 @@ function buildFoirHeaders(input) {
|
|
|
163
172
|
}
|
|
164
173
|
return headers;
|
|
165
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
|
+
};
|
|
166
195
|
var warnedDowngrade = null;
|
|
167
196
|
function reportAuthDowngrade(envelope) {
|
|
168
197
|
const downgrade = envelope.extensions?.auth;
|
|
@@ -195,6 +224,9 @@ function answeredAnything(data) {
|
|
|
195
224
|
return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
|
|
196
225
|
}
|
|
197
226
|
function readFoirEnvelope(json, options) {
|
|
227
|
+
if (options?.rejectDowngrade && json.extensions?.auth) {
|
|
228
|
+
throw new FoirAuthDowngradedError(json.extensions.auth);
|
|
229
|
+
}
|
|
198
230
|
reportAuthDowngrade(json);
|
|
199
231
|
refusePrunedDocument(json, options);
|
|
200
232
|
if (json.errors?.length) {
|
|
@@ -353,7 +385,7 @@ async function foirRequest(env, document, variables, options) {
|
|
|
353
385
|
);
|
|
354
386
|
}
|
|
355
387
|
const json = await res.json();
|
|
356
|
-
return readFoirEnvelope(json);
|
|
388
|
+
return readFoirEnvelope(json, { rejectDowngrade: options?.rejectDowngrade });
|
|
357
389
|
}
|
|
358
390
|
async function readErrorEnvelope(res) {
|
|
359
391
|
let errors;
|
|
@@ -406,7 +438,9 @@ function createFoirContentClient(config) {
|
|
|
406
438
|
}
|
|
407
439
|
export {
|
|
408
440
|
FOIR_CDN_BASE,
|
|
441
|
+
FoirAuthDowngradedError,
|
|
409
442
|
FoirRequestError,
|
|
443
|
+
FoirSessionExpiredError,
|
|
410
444
|
absolutiseFoirMediaUrl,
|
|
411
445
|
createFoirContentClient,
|
|
412
446
|
foirRequest,
|
package/dist/data/index.d.ts
CHANGED
|
@@ -81,5 +81,5 @@ export type { FoirConnection, FoirFieldsShape, FoirModelDefinition, FoirNamespac
|
|
|
81
81
|
export type { FoirInclude, FoirModelOperation, FoirNamespaceOperation, FoirOmittableKey, FoirOpResult, FoirQuery, FoirRecordResult, FoirWriteResult, } from './selection';
|
|
82
82
|
export { FoirDecimal, decimal } from './decimal';
|
|
83
83
|
export type { FoirDecimalDivOptions, FoirDecimalInput, FoirDecimalRounding, } from './decimal';
|
|
84
|
-
export { FoirRequestError } from '../transport/foir-http';
|
|
85
|
-
export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, } from '../transport/foir-http';
|
|
84
|
+
export { FoirRequestError, FoirAuthDowngradedError, FoirSessionExpiredError, FOIR_VISITOR_IP_HEADER, } from '../transport/foir-http';
|
|
85
|
+
export type { FoirEnv, FoirGraphQLErrorItem, FoirTokenProvider, FoirAuthDowngrade, FoirVisitorSource, } from '../transport/foir-http';
|
package/dist/data/transport.d.ts
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
* with more force here — this client is per-credential, so anything cached
|
|
61
61
|
* across instances is cached across tenants.
|
|
62
62
|
*/
|
|
63
|
-
import { FoirRequestError, type FoirTokenProvider } from '../transport/foir-http';
|
|
63
|
+
import { FoirRequestError, type FoirTokenProvider, type FoirVisitorSource } from '../transport/foir-http';
|
|
64
64
|
import type { FoirDocumentPlan, FoirNamespaceSelections, FoirSelections, FoirSelectionUnions } from './document';
|
|
65
65
|
import type { FoirSchemaDefinition } from './schema';
|
|
66
66
|
import type { FoirSubjectRegistry } from './subjects';
|
|
@@ -211,6 +211,18 @@ export interface FoirKeyClientConfig<S extends FoirSchemaDefinition = FoirSchema
|
|
|
211
211
|
* per-request; a bare string is used as-is.
|
|
212
212
|
*/
|
|
213
213
|
bearerToken?: string | FoirTokenProvider;
|
|
214
|
+
/**
|
|
215
|
+
* Renew the customer bearer when an answer comes back served without it
|
|
216
|
+
* because it expired. Resolve the new token, or null when the session is
|
|
217
|
+
* over. It is called at most once per request, and one call is shared by the
|
|
218
|
+
* client's requests that find the bearer lapsed at the same time.
|
|
219
|
+
*
|
|
220
|
+
* With it, such a request is asked again with the new token, and fails with
|
|
221
|
+
* `FoirSessionExpiredError` when there is none. Without it, the request fails
|
|
222
|
+
* with `FoirAuthDowngradedError`. Either way it is never handed the signed-out
|
|
223
|
+
* answer as though it were the signed-in one.
|
|
224
|
+
*/
|
|
225
|
+
refreshBearer?: () => Promise<string | null>;
|
|
214
226
|
/**
|
|
215
227
|
* The organization every request from this client acts in, sent as
|
|
216
228
|
* `X-Org-Id`.
|
|
@@ -246,6 +258,24 @@ export interface FoirKeyClientConfig<S extends FoirSchemaDefinition = FoirSchema
|
|
|
246
258
|
* appear.
|
|
247
259
|
*/
|
|
248
260
|
orgId?: string;
|
|
261
|
+
/**
|
|
262
|
+
* The address of the visitor a server rendering pages is calling for, or a
|
|
263
|
+
* function resolving it for each request, sent as `X-Foir-Visitor-IP`. The
|
|
264
|
+
* public API's per-visitor limit then applies to that visitor rather than to
|
|
265
|
+
* your server, which otherwise counts as one visitor for every page it
|
|
266
|
+
* renders.
|
|
267
|
+
*
|
|
268
|
+
* Give it the address your host guarantees: the one its edge sets and a
|
|
269
|
+
* visitor cannot. A header a visitor can write lets them name a fresh address
|
|
270
|
+
* per request, which is exactly what the limit exists to stop. The docs list
|
|
271
|
+
* the value for each common host.
|
|
272
|
+
*
|
|
273
|
+
* A secret key only, and one holding `visitors:forward`: the API refuses the
|
|
274
|
+
* header, naming the permission, from any other. A client built from a public
|
|
275
|
+
* key is refused `visitor` here, because a browser already calls as its own
|
|
276
|
+
* visitor and a public key cannot hold the permission.
|
|
277
|
+
*/
|
|
278
|
+
visitor?: FoirVisitorSource;
|
|
249
279
|
/**
|
|
250
280
|
* The organization every request from this client acts in, named by the
|
|
251
281
|
* address of the page it is for, sent as `X-Org-Url`.
|
|
@@ -483,7 +513,7 @@ export interface FoirKeyClientConfig<S extends FoirSchemaDefinition = FoirSchema
|
|
|
483
513
|
* (a customer bearer, an organization, a channel), and the subscription socket,
|
|
484
514
|
* which only a key can open.
|
|
485
515
|
*/
|
|
486
|
-
type FoirKeySideOption = 'apiKey' | 'dispatch' | 'channel' | 'bearerToken' | 'orgId' | 'orgUrl' | 'wsUrl' | 'webSocketImpl' | 'reconnect' | 'connectionParams';
|
|
516
|
+
type FoirKeySideOption = 'apiKey' | 'dispatch' | 'channel' | 'bearerToken' | 'refreshBearer' | 'orgId' | 'orgUrl' | 'visitor' | 'wsUrl' | 'webSocketImpl' | 'reconnect' | 'connectionParams';
|
|
487
517
|
/**
|
|
488
518
|
* A client that calls Foir AS an operation, from inside the endpoint the
|
|
489
519
|
* operation dispatched to. See {@link FoirDispatchCredential}.
|
|
@@ -513,8 +543,10 @@ export interface FoirDispatchClientConfig<S extends FoirSchemaDefinition = FoirS
|
|
|
513
543
|
apiKey?: never;
|
|
514
544
|
channel?: never;
|
|
515
545
|
bearerToken?: never;
|
|
546
|
+
refreshBearer?: never;
|
|
516
547
|
orgId?: never;
|
|
517
548
|
orgUrl?: never;
|
|
549
|
+
visitor?: never;
|
|
518
550
|
wsUrl?: never;
|
|
519
551
|
webSocketImpl?: never;
|
|
520
552
|
reconnect?: never;
|
package/dist/data.js
CHANGED
|
@@ -62,7 +62,12 @@ var FoirRequestError = class extends Error {
|
|
|
62
62
|
* failed on the first attempt for an hour.
|
|
63
63
|
*/
|
|
64
64
|
get retryable() {
|
|
65
|
-
if (this.status === void 0)
|
|
65
|
+
if (this.status === void 0) {
|
|
66
|
+
if (this.graphQLErrors?.length || this.prunedSelections?.length) {
|
|
67
|
+
return this.code === "RATE_LIMITED" || this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
66
71
|
if (this.status === 429 || this.code === "RATE_LIMITED") return true;
|
|
67
72
|
if (this.status >= 500) return true;
|
|
68
73
|
return this.code === "UNAVAILABLE" || this.code === "TIMEOUT";
|
|
@@ -92,6 +97,7 @@ function refuseCredentialHeaders(extra) {
|
|
|
92
97
|
}
|
|
93
98
|
}
|
|
94
99
|
}
|
|
100
|
+
var FOIR_VISITOR_IP_HEADER = "X-Foir-Visitor-IP";
|
|
95
101
|
function buildFoirHeaders(input) {
|
|
96
102
|
const headers = {
|
|
97
103
|
"Content-Type": "application/json"
|
|
@@ -116,6 +122,9 @@ function buildFoirHeaders(input) {
|
|
|
116
122
|
if (input.orgUrl) {
|
|
117
123
|
headers["X-Org-Url"] = input.orgUrl;
|
|
118
124
|
}
|
|
125
|
+
if (input.visitorAddress) {
|
|
126
|
+
headers[FOIR_VISITOR_IP_HEADER] = input.visitorAddress;
|
|
127
|
+
}
|
|
119
128
|
}
|
|
120
129
|
if (input.extra) {
|
|
121
130
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -131,6 +140,26 @@ function foirApiEndpoint(apiUrl, path) {
|
|
|
131
140
|
}
|
|
132
141
|
return base + path;
|
|
133
142
|
}
|
|
143
|
+
var FoirAuthDowngradedError = class extends FoirRequestError {
|
|
144
|
+
downgrade;
|
|
145
|
+
constructor(downgrade) {
|
|
146
|
+
super(
|
|
147
|
+
`[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}), so the request was served as ${downgrade.servedAs}`,
|
|
148
|
+
{ graphQLErrors: [{ message: "customer token rejected", extensions: { code: "AUTH_DOWNGRADED" } }] }
|
|
149
|
+
);
|
|
150
|
+
this.name = "FoirAuthDowngradedError";
|
|
151
|
+
this.downgrade = downgrade;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var FoirSessionExpiredError = class extends FoirRequestError {
|
|
155
|
+
constructor(options) {
|
|
156
|
+
super("[foir] the customer session has expired and could not be refreshed: sign in again", {
|
|
157
|
+
cause: options?.cause,
|
|
158
|
+
graphQLErrors: [{ message: "session expired", extensions: { code: "SESSION_EXPIRED" } }]
|
|
159
|
+
});
|
|
160
|
+
this.name = "FoirSessionExpiredError";
|
|
161
|
+
}
|
|
162
|
+
};
|
|
134
163
|
var warnedDowngrade = null;
|
|
135
164
|
function reportAuthDowngrade(envelope) {
|
|
136
165
|
const downgrade = envelope.extensions?.auth;
|
|
@@ -163,6 +192,9 @@ function answeredAnything(data) {
|
|
|
163
192
|
return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
|
|
164
193
|
}
|
|
165
194
|
function readFoirEnvelope(json, options) {
|
|
195
|
+
if (options?.rejectDowngrade && json.extensions?.auth) {
|
|
196
|
+
throw new FoirAuthDowngradedError(json.extensions.auth);
|
|
197
|
+
}
|
|
166
198
|
reportAuthDowngrade(json);
|
|
167
199
|
refusePrunedDocument(json, options);
|
|
168
200
|
if (json.errors?.length) {
|
|
@@ -224,6 +256,12 @@ var FoirSchemaDriftError = class extends FoirRequestError {
|
|
|
224
256
|
function foirDataEndpoint(apiUrl, path) {
|
|
225
257
|
return foirApiEndpoint(apiUrl, path);
|
|
226
258
|
}
|
|
259
|
+
async function resolveVisitorAddress(config) {
|
|
260
|
+
const source = config.visitor;
|
|
261
|
+
const value = typeof source === "function" ? await source() : source;
|
|
262
|
+
const address = value?.trim();
|
|
263
|
+
return address ? address : void 0;
|
|
264
|
+
}
|
|
227
265
|
async function resolveBearerToken(config) {
|
|
228
266
|
const source = config.bearerToken;
|
|
229
267
|
if (typeof source === "function") return await source() ?? void 0;
|
|
@@ -256,6 +294,7 @@ async function resolveFoirAuth(config) {
|
|
|
256
294
|
bearerToken,
|
|
257
295
|
orgId: config.orgId,
|
|
258
296
|
orgUrl: config.orgUrl,
|
|
297
|
+
visitorAddress: await resolveVisitorAddress(config),
|
|
259
298
|
extra: config.headers
|
|
260
299
|
}),
|
|
261
300
|
key: { apiKey: config.apiKey, bearerToken }
|
|
@@ -270,6 +309,11 @@ function assertFoirCredential(config) {
|
|
|
270
309
|
"[foir] createFoirClient needs both apiUrl and apiKey (or, inside an operation endpoint, apiUrl and dispatch)"
|
|
271
310
|
);
|
|
272
311
|
}
|
|
312
|
+
if (loose.visitor !== void 0 && !loose.apiKey.startsWith("sk_")) {
|
|
313
|
+
throw new FoirRequestError(
|
|
314
|
+
"[foir] createFoirClient: `visitor` names the visitor a server calls for, and only a secret key holding visitors:forward may send it. A public key already calls as its visitor, and cannot hold the permission. Drop `visitor`, or build this client from your server's secret key."
|
|
315
|
+
);
|
|
316
|
+
}
|
|
273
317
|
if (operation !== void 0) {
|
|
274
318
|
throw new FoirRequestError(
|
|
275
319
|
`[foir] createFoirClient: the module passed as \`generated\` was generated for operation "${operation}" (\`foir types --operation ${operation}\`), so its types are what that operation's dispatch token is served, and this client holds an api key, which is served its own surface. Generate the key's types with \`foir types\`, or build this client from the dispatch token inside the operation's endpoint.`
|
|
@@ -280,8 +324,10 @@ function assertFoirCredential(config) {
|
|
|
280
324
|
const keySide = [
|
|
281
325
|
"apiKey",
|
|
282
326
|
"bearerToken",
|
|
327
|
+
"refreshBearer",
|
|
283
328
|
"orgId",
|
|
284
329
|
"orgUrl",
|
|
330
|
+
"visitor",
|
|
285
331
|
"channel",
|
|
286
332
|
"wsUrl",
|
|
287
333
|
"webSocketImpl",
|
|
@@ -347,7 +393,33 @@ async function executeFoirDocument(config, plan) {
|
|
|
347
393
|
async function executeFoirRawDocument(config, document, mutation, variables, options) {
|
|
348
394
|
return await runFoirDocument(config, document, mutation, variables, options);
|
|
349
395
|
}
|
|
396
|
+
var renewing = /* @__PURE__ */ new WeakMap();
|
|
397
|
+
function renewBearerOnce(refresh) {
|
|
398
|
+
const running = renewing.get(refresh);
|
|
399
|
+
if (running) return running;
|
|
400
|
+
const next = refresh().finally(() => renewing.delete(refresh));
|
|
401
|
+
renewing.set(refresh, next);
|
|
402
|
+
return next;
|
|
403
|
+
}
|
|
350
404
|
async function runFoirDocument(config, document, mutation, variables, options) {
|
|
405
|
+
try {
|
|
406
|
+
return await sendFoirDocument(config, document, mutation, variables, options);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
const refresh = config.refreshBearer;
|
|
409
|
+
if (!(err instanceof FoirAuthDowngradedError) || !refresh || err.downgrade.reason === "wrong_project") {
|
|
410
|
+
throw err;
|
|
411
|
+
}
|
|
412
|
+
const token = err.downgrade.reason === "expired" ? await renewBearerOnce(refresh) : null;
|
|
413
|
+
if (!token) throw new FoirSessionExpiredError({ cause: err });
|
|
414
|
+
try {
|
|
415
|
+
return await sendFoirDocument({ ...config, bearerToken: token }, document, mutation, variables, options);
|
|
416
|
+
} catch (again) {
|
|
417
|
+
if (again instanceof FoirAuthDowngradedError) throw new FoirSessionExpiredError({ cause: again });
|
|
418
|
+
throw again;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
async function sendFoirDocument(config, document, mutation, variables, options) {
|
|
351
423
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
352
424
|
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
353
425
|
const { headers, key } = await resolveFoirAuth(config);
|
|
@@ -396,7 +468,7 @@ async function runFoirDocument(config, document, mutation, variables, options) {
|
|
|
396
468
|
});
|
|
397
469
|
}
|
|
398
470
|
const json = await res.json();
|
|
399
|
-
return readFoirEnvelope(json, options);
|
|
471
|
+
return readFoirEnvelope(json, { ...options, rejectDowngrade: Boolean(key?.bearerToken) });
|
|
400
472
|
}
|
|
401
473
|
async function readErrorBody(res) {
|
|
402
474
|
try {
|
|
@@ -2190,9 +2262,12 @@ function decimal(value) {
|
|
|
2190
2262
|
return new FoirDecimal(value);
|
|
2191
2263
|
}
|
|
2192
2264
|
export {
|
|
2265
|
+
FOIR_VISITOR_IP_HEADER,
|
|
2266
|
+
FoirAuthDowngradedError,
|
|
2193
2267
|
FoirDecimal,
|
|
2194
2268
|
FoirRequestError,
|
|
2195
2269
|
FoirSchemaDriftError,
|
|
2270
|
+
FoirSessionExpiredError,
|
|
2196
2271
|
FoirSocketTransport,
|
|
2197
2272
|
createFoirClient,
|
|
2198
2273
|
createFoirClientFromEnv,
|