@foir/sdk 0.8.0 → 0.10.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 +550 -28
- package/README.md +111 -0
- package/dist/auth/auth.d.ts +72 -1
- package/dist/auth/hosted.d.ts +93 -4
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/operations.d.ts +84 -0
- package/dist/auth/ops.d.ts +10 -0
- package/dist/auth.js +138 -19
- package/dist/client.js +63 -6
- package/dist/codegen/block-registry.d.ts +1 -1
- package/dist/codegen/block-scaffold.d.ts +1 -1
- package/dist/codegen/index.d.ts +1 -1
- package/dist/codegen/item-registry.d.ts +1 -1
- package/dist/codegen/item-scaffold.d.ts +1 -1
- package/dist/content/content-renderer.d.ts +4 -5
- package/dist/content/index.d.ts +1 -1
- package/dist/content/richtext.d.ts +1 -1
- package/dist/data/builtins.d.ts +131 -12
- package/dist/data/client.d.ts +82 -1
- package/dist/data/decimal.d.ts +212 -0
- package/dist/data/document.d.ts +1 -1
- package/dist/data/index.d.ts +11 -9
- package/dist/data/manifest.d.ts +33 -6
- package/dist/data/schema.d.ts +2 -2
- package/dist/data/subscriptions.d.ts +36 -10
- package/dist/data/transport.d.ts +120 -3
- package/dist/data.js +377 -16
- package/dist/graphql/cdn-route.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/live/coalesce.d.ts +57 -0
- package/dist/live/drift.d.ts +59 -0
- package/dist/live/events.d.ts +13 -0
- package/dist/live/headers.d.ts +53 -0
- package/dist/live/index.d.ts +54 -0
- package/dist/live/keys.d.ts +72 -0
- package/dist/live/manager.d.ts +77 -0
- package/dist/live/react/index.d.ts +143 -0
- package/dist/live/reconcile.d.ts +82 -0
- package/dist/live/resync.d.ts +59 -0
- package/dist/live/store.d.ts +134 -0
- package/dist/live/tagIndex.d.ts +29 -0
- package/dist/live/tanstack.d.ts +127 -0
- package/dist/live/writetoken.d.ts +86 -0
- package/dist/live-react.js +395 -0
- package/dist/live.js +845 -0
- package/dist/next/cacheTags.d.ts +245 -0
- package/dist/next/index.d.ts +7 -0
- package/dist/next.js +179 -6
- package/dist/push.js +81 -8
- package/dist/react.js +76 -5
- package/dist/realtime/index.d.ts +4 -5
- package/dist/server/events-client.d.ts +54 -4
- package/dist/server/index.d.ts +3 -2
- package/dist/server/require-scoped-token.d.ts +1 -1
- package/dist/server.js +454 -2
- package/dist/transport/foir-http.d.ts +113 -10
- package/package.json +10 -2
package/dist/auth.js
CHANGED
|
@@ -11,11 +11,30 @@ function isFoirPreview(env) {
|
|
|
11
11
|
function resolveFoirApiKey(env) {
|
|
12
12
|
return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
|
|
13
13
|
}
|
|
14
|
+
var MALFORMED_PRUNE_REASONS = /* @__PURE__ */ new Set([
|
|
15
|
+
"unknown_field",
|
|
16
|
+
"subselections_on_scalar",
|
|
17
|
+
"missing_subselections"
|
|
18
|
+
]);
|
|
14
19
|
var FoirRequestError = class extends Error {
|
|
15
20
|
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
16
21
|
status;
|
|
17
22
|
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
18
23
|
graphQLErrors;
|
|
24
|
+
/**
|
|
25
|
+
* The `data` the server did return, when a STRICT read refused a partial
|
|
26
|
+
* response. Present only on that path: without it, opting into strictness
|
|
27
|
+
* would trade a silent wrong answer for a thrown one that discards the answer,
|
|
28
|
+
* and a caller that wants to log what it nearly rendered could not.
|
|
29
|
+
*/
|
|
30
|
+
partialData;
|
|
31
|
+
/**
|
|
32
|
+
* The selections the server stripped, when the refusal was a pruned document
|
|
33
|
+
* rather than a partial error. Same purpose as `partialData`: a caller that
|
|
34
|
+
* wants to log what it nearly rendered, or name the offending path in a
|
|
35
|
+
* developer-facing message, has it without re-parsing anything.
|
|
36
|
+
*/
|
|
37
|
+
prunedSelections;
|
|
19
38
|
cause;
|
|
20
39
|
constructor(message, options) {
|
|
21
40
|
super(message);
|
|
@@ -23,6 +42,8 @@ var FoirRequestError = class extends Error {
|
|
|
23
42
|
this.cause = options?.cause;
|
|
24
43
|
this.status = options?.status;
|
|
25
44
|
this.graphQLErrors = options?.graphQLErrors;
|
|
45
|
+
this.partialData = options?.partialData;
|
|
46
|
+
this.prunedSelections = options?.prunedSelections;
|
|
26
47
|
}
|
|
27
48
|
/**
|
|
28
49
|
* The code of the first error, if the response carried one.
|
|
@@ -93,6 +114,9 @@ function buildFoirHeaders(input) {
|
|
|
93
114
|
if (input.bearerToken) {
|
|
94
115
|
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
95
116
|
}
|
|
117
|
+
if (input.orgId) {
|
|
118
|
+
headers["X-Org-Id"] = input.orgId;
|
|
119
|
+
}
|
|
96
120
|
if (input.extra) {
|
|
97
121
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
98
122
|
headers[key] = value;
|
|
@@ -107,14 +131,45 @@ function foirApiEndpoint(apiUrl, path) {
|
|
|
107
131
|
}
|
|
108
132
|
return base + path;
|
|
109
133
|
}
|
|
110
|
-
|
|
134
|
+
var warnedDowngrade = null;
|
|
135
|
+
function reportAuthDowngrade(envelope) {
|
|
136
|
+
const downgrade = envelope.extensions?.auth;
|
|
137
|
+
if (!downgrade) {
|
|
138
|
+
warnedDowngrade = null;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (warnedDowngrade === downgrade.reason) return;
|
|
142
|
+
warnedDowngrade = downgrade.reason;
|
|
143
|
+
console.warn(
|
|
144
|
+
`[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}) \u2014 this request was served as ${downgrade.servedAs}, so auth-targeted content resolved to its anonymous variant. ` + (downgrade.reason === "expired" ? "Re-exchange the token and retry." : "Check which credential you are sending.")
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
function refusePrunedDocument(json, options) {
|
|
148
|
+
const selections = json.extensions?.pruned?.selections;
|
|
149
|
+
if (!selections?.length) return;
|
|
150
|
+
const describe = (s) => `${s.path}${s.type ? ` on ${s.type}` : ""} (${s.reason})`;
|
|
151
|
+
const malformed = selections.filter((s) => MALFORMED_PRUNE_REASONS.has(s.reason));
|
|
152
|
+
if (malformed.length && options?.strict !== false) {
|
|
153
|
+
throw new FoirRequestError(
|
|
154
|
+
`[foir] the server could not serve part of this query and returned the rest without it: ${malformed.map(describe).join(", ")}. Those selections do not exist on the schema this project is serving. Pass strict: false to get the partial answer instead of this error.`,
|
|
155
|
+
{ prunedSelections: selections }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
console.warn(
|
|
159
|
+
`[foir] selections stripped from this query: ${selections.map(describe).join(", ")}`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
function readFoirEnvelope(json, options) {
|
|
163
|
+
reportAuthDowngrade(json);
|
|
164
|
+
refusePrunedDocument(json, options);
|
|
111
165
|
if (json.errors?.length) {
|
|
112
166
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
113
|
-
if (json.data) {
|
|
167
|
+
if (json.data && !options?.strict) {
|
|
114
168
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
115
169
|
} else {
|
|
116
170
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
117
|
-
graphQLErrors: json.errors
|
|
171
|
+
graphQLErrors: json.errors,
|
|
172
|
+
partialData: json.data ?? void 0
|
|
118
173
|
});
|
|
119
174
|
}
|
|
120
175
|
}
|
|
@@ -155,8 +210,9 @@ async function hasFoirErrorCode(res, ...codes) {
|
|
|
155
210
|
}
|
|
156
211
|
|
|
157
212
|
// src/graphql/cdn-route.ts
|
|
158
|
-
function isCdnEligible(env, documentText, bearerToken) {
|
|
213
|
+
function isCdnEligible(env, documentText, bearerToken, orgId) {
|
|
159
214
|
if (bearerToken) return false;
|
|
215
|
+
if (orgId) return false;
|
|
160
216
|
if (isFoirPreview(env)) return false;
|
|
161
217
|
const apiKey = resolveFoirApiKey(env);
|
|
162
218
|
if (!apiKey?.startsWith("pk_")) return false;
|
|
@@ -175,12 +231,13 @@ async function foirRequest(env, document, variables, options) {
|
|
|
175
231
|
const headers = buildFoirHeaders({
|
|
176
232
|
apiKey,
|
|
177
233
|
draftChannel: isFoirPreview(env),
|
|
178
|
-
bearerToken: options?.bearerToken
|
|
234
|
+
bearerToken: options?.bearerToken,
|
|
235
|
+
orgId: options?.orgId
|
|
179
236
|
});
|
|
180
237
|
const documentText = print2(document);
|
|
181
238
|
let res;
|
|
182
239
|
try {
|
|
183
|
-
if (isCdnEligible(env, documentText, options?.bearerToken)) {
|
|
240
|
+
if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId)) {
|
|
184
241
|
const hash = await documentHash(documentText);
|
|
185
242
|
let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
|
|
186
243
|
method: "GET",
|
|
@@ -739,7 +796,9 @@ async function resolveBearerToken(config) {
|
|
|
739
796
|
function foirRoute(input) {
|
|
740
797
|
if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
|
|
741
798
|
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
799
|
+
if (input.orgId) return { lane: "origin", reason: "org" };
|
|
742
800
|
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
801
|
+
if (input.writeToken) return { lane: "origin", reason: "floor" };
|
|
743
802
|
if (input.channel === "draft") return { lane: "origin", reason: "channel" };
|
|
744
803
|
return { lane: "shared" };
|
|
745
804
|
}
|
|
@@ -752,7 +811,7 @@ async function executeFoirDocument(config, plan) {
|
|
|
752
811
|
}
|
|
753
812
|
return data[plan.rootField];
|
|
754
813
|
}
|
|
755
|
-
async function runFoirDocument(config, document, mutation, variables) {
|
|
814
|
+
async function runFoirDocument(config, document, mutation, variables, options) {
|
|
756
815
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
757
816
|
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
758
817
|
const bearerToken = await resolveBearerToken(config);
|
|
@@ -760,12 +819,14 @@ async function runFoirDocument(config, document, mutation, variables) {
|
|
|
760
819
|
apiKey: config.apiKey,
|
|
761
820
|
draftChannel: config.channel === "draft",
|
|
762
821
|
bearerToken,
|
|
822
|
+
orgId: config.orgId,
|
|
763
823
|
extra: config.headers
|
|
764
824
|
});
|
|
765
825
|
const hash = await documentHash(document);
|
|
766
826
|
const route = foirRoute({
|
|
767
827
|
apiKey: config.apiKey,
|
|
768
828
|
bearerToken,
|
|
829
|
+
orgId: config.orgId,
|
|
769
830
|
mutation,
|
|
770
831
|
channel: config.channel
|
|
771
832
|
});
|
|
@@ -805,7 +866,7 @@ async function runFoirDocument(config, document, mutation, variables) {
|
|
|
805
866
|
});
|
|
806
867
|
}
|
|
807
868
|
const json = await res.json();
|
|
808
|
-
return readFoirEnvelope(json);
|
|
869
|
+
return readFoirEnvelope(json, options);
|
|
809
870
|
}
|
|
810
871
|
async function readErrorBody(res) {
|
|
811
872
|
try {
|
|
@@ -828,6 +889,7 @@ async function fetchFoirManifest(config) {
|
|
|
828
889
|
apiKey: config.apiKey,
|
|
829
890
|
draftChannel: config.channel === "draft",
|
|
830
891
|
bearerToken: await resolveBearerToken(config),
|
|
892
|
+
orgId: config.orgId,
|
|
831
893
|
extra: config.headers
|
|
832
894
|
}),
|
|
833
895
|
...config.requestInit
|
|
@@ -869,6 +931,16 @@ var AUTH_OPS = {
|
|
|
869
931
|
customerUpdatePassword: "customerUpdatePassword",
|
|
870
932
|
customerVerifyEmail: "customerVerifyEmail",
|
|
871
933
|
deleteMyAccount: "deleteMyAccount",
|
|
934
|
+
// The operator's organization context. TWO ops and no third: one to read
|
|
935
|
+
// which organizations the caller may stand in, one to say which. There is
|
|
936
|
+
// deliberately no per-call org argument anywhere in this SDK — the claim the
|
|
937
|
+
// row layer reads is resolved from the caller's own membership row on every
|
|
938
|
+
// request, so an application that never names an org cannot name the wrong
|
|
939
|
+
// one.
|
|
940
|
+
myOrganizations: "myOrganizations",
|
|
941
|
+
selectMyOrganization: "selectMyOrganization",
|
|
942
|
+
// Founding one. Present in a manifest only when the project opted in.
|
|
943
|
+
registerOperatorOrganization: "registerOperatorOrganization",
|
|
872
944
|
// External identity providers.
|
|
873
945
|
customerLoginWithProvider: "customerLoginWithProvider",
|
|
874
946
|
customerProviderCallback: "customerProviderCallback",
|
|
@@ -890,6 +962,7 @@ function authClientConfig(env, options) {
|
|
|
890
962
|
apiKey,
|
|
891
963
|
channel: isFoirPreview(env) ? "draft" : "published",
|
|
892
964
|
bearerToken: options?.bearerToken,
|
|
965
|
+
orgId: options?.orgId,
|
|
893
966
|
headers: options?.headers,
|
|
894
967
|
fetch: options?.fetch
|
|
895
968
|
};
|
|
@@ -998,6 +1071,15 @@ async function customerLogout(env, options) {
|
|
|
998
1071
|
async function deleteMyAccount(env, vars, options) {
|
|
999
1072
|
return authOp(env, AUTH_OPS.deleteMyAccount, vars, options);
|
|
1000
1073
|
}
|
|
1074
|
+
async function myOrganizations(env, vars, options) {
|
|
1075
|
+
return authOp(env, AUTH_OPS.myOrganizations, vars ?? {}, options);
|
|
1076
|
+
}
|
|
1077
|
+
async function selectOrganization(env, vars, options) {
|
|
1078
|
+
return authOp(env, AUTH_OPS.selectMyOrganization, vars, options);
|
|
1079
|
+
}
|
|
1080
|
+
async function registerOrganization(env, vars, options) {
|
|
1081
|
+
return authOp(env, AUTH_OPS.registerOperatorOrganization, vars, options);
|
|
1082
|
+
}
|
|
1001
1083
|
async function saveSession(adapter, result) {
|
|
1002
1084
|
await adapter.set(TOKEN_KEY, result.token);
|
|
1003
1085
|
await adapter.set(REFRESH_KEY, result.refreshToken);
|
|
@@ -1111,6 +1193,7 @@ async function requireAdmin(adapter, options) {
|
|
|
1111
1193
|
// src/auth/hosted.ts
|
|
1112
1194
|
var DEFAULT_COOKIE_NAME = "foir_customer_session";
|
|
1113
1195
|
var DEFAULT_SESSION_PATH = "/session/token";
|
|
1196
|
+
var DEFAULT_END_SESSION_PATH = "/session/end-session";
|
|
1114
1197
|
function hostedLoginUrl(config, returnTo) {
|
|
1115
1198
|
const dest = new URL(config.loginOrigin);
|
|
1116
1199
|
if (returnTo && returnTo !== "/") {
|
|
@@ -1118,6 +1201,15 @@ function hostedLoginUrl(config, returnTo) {
|
|
|
1118
1201
|
}
|
|
1119
1202
|
return dest.toString();
|
|
1120
1203
|
}
|
|
1204
|
+
function hostedLogoutUrl(config, clientId, returnTo) {
|
|
1205
|
+
const path = config.endSessionPath ?? DEFAULT_END_SESSION_PATH;
|
|
1206
|
+
const dest = new URL(path, config.loginOrigin);
|
|
1207
|
+
dest.searchParams.set("client_id", clientId);
|
|
1208
|
+
if (returnTo) {
|
|
1209
|
+
dest.searchParams.set("post_logout_redirect_uri", returnTo);
|
|
1210
|
+
}
|
|
1211
|
+
return dest.toString();
|
|
1212
|
+
}
|
|
1121
1213
|
async function fetchHostedSession(request, config) {
|
|
1122
1214
|
const cookieName = config.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
1123
1215
|
const sessionPath = config.sessionPath ?? DEFAULT_SESSION_PATH;
|
|
@@ -1125,25 +1217,48 @@ async function fetchHostedSession(request, config) {
|
|
|
1125
1217
|
const match = cookie.match(
|
|
1126
1218
|
new RegExp(`(?:^|;\\s*)${escapeRegExp(cookieName)}=([^;]+)`)
|
|
1127
1219
|
);
|
|
1128
|
-
if (!match) return
|
|
1220
|
+
if (!match) return { outcome: "signed-out", reason: "no-cookie" };
|
|
1221
|
+
let res;
|
|
1129
1222
|
try {
|
|
1130
|
-
|
|
1223
|
+
res = await fetch(`${config.loginOrigin}${sessionPath}`, {
|
|
1131
1224
|
headers: {
|
|
1132
1225
|
Cookie: `${cookieName}=${match[1]}`,
|
|
1133
1226
|
"X-Foir-Session": "1"
|
|
1134
1227
|
}
|
|
1135
1228
|
});
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1229
|
+
} catch (cause) {
|
|
1230
|
+
return { outcome: "unavailable", reason: "network", cause };
|
|
1231
|
+
}
|
|
1232
|
+
if (!res.ok) {
|
|
1233
|
+
if (res.status === 401) {
|
|
1234
|
+
return { outcome: "signed-out", reason: "rejected" };
|
|
1235
|
+
}
|
|
1236
|
+
if (res.status === 403) {
|
|
1237
|
+
return { outcome: "unavailable", reason: "forbidden", httpStatus: res.status };
|
|
1238
|
+
}
|
|
1239
|
+
if (res.status === 429) {
|
|
1240
|
+
return { outcome: "unavailable", reason: "rate-limited", httpStatus: res.status };
|
|
1241
|
+
}
|
|
1242
|
+
return { outcome: "unavailable", reason: "server-error", httpStatus: res.status };
|
|
1243
|
+
}
|
|
1244
|
+
let json;
|
|
1245
|
+
try {
|
|
1246
|
+
json = await res.json();
|
|
1144
1247
|
} catch {
|
|
1145
|
-
return
|
|
1248
|
+
return { outcome: "unavailable", reason: "malformed", httpStatus: res.status };
|
|
1146
1249
|
}
|
|
1250
|
+
if (!json.access_token || !json.customer_id) {
|
|
1251
|
+
return { outcome: "unavailable", reason: "malformed", httpStatus: res.status };
|
|
1252
|
+
}
|
|
1253
|
+
return {
|
|
1254
|
+
outcome: "active",
|
|
1255
|
+
session: {
|
|
1256
|
+
token: json.access_token,
|
|
1257
|
+
customerId: json.customer_id,
|
|
1258
|
+
email: json.email ?? "",
|
|
1259
|
+
expiresIn: typeof json.expires_in === "number" ? json.expires_in : 0
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1147
1262
|
}
|
|
1148
1263
|
function escapeRegExp(s) {
|
|
1149
1264
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1313,14 +1428,18 @@ export {
|
|
|
1313
1428
|
getSessionCustomer,
|
|
1314
1429
|
getSessionToken,
|
|
1315
1430
|
hostedLoginUrl,
|
|
1431
|
+
hostedLogoutUrl,
|
|
1316
1432
|
loginWithOTP,
|
|
1317
1433
|
loginWithPassword,
|
|
1434
|
+
myOrganizations,
|
|
1318
1435
|
myProviderToken,
|
|
1319
1436
|
refreshSession,
|
|
1437
|
+
registerOrganization,
|
|
1320
1438
|
registerWithPassword,
|
|
1321
1439
|
requireAdmin,
|
|
1322
1440
|
requireAuth,
|
|
1323
1441
|
resetPasswordAndSignIn,
|
|
1324
1442
|
saveSession,
|
|
1443
|
+
selectOrganization,
|
|
1325
1444
|
signOut
|
|
1326
1445
|
};
|
package/dist/client.js
CHANGED
|
@@ -11,11 +11,30 @@ function isFoirPreview(env) {
|
|
|
11
11
|
function resolveFoirApiKey(env) {
|
|
12
12
|
return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
|
|
13
13
|
}
|
|
14
|
+
var MALFORMED_PRUNE_REASONS = /* @__PURE__ */ new Set([
|
|
15
|
+
"unknown_field",
|
|
16
|
+
"subselections_on_scalar",
|
|
17
|
+
"missing_subselections"
|
|
18
|
+
]);
|
|
14
19
|
var FoirRequestError = class extends Error {
|
|
15
20
|
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
16
21
|
status;
|
|
17
22
|
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
18
23
|
graphQLErrors;
|
|
24
|
+
/**
|
|
25
|
+
* The `data` the server did return, when a STRICT read refused a partial
|
|
26
|
+
* response. Present only on that path: without it, opting into strictness
|
|
27
|
+
* would trade a silent wrong answer for a thrown one that discards the answer,
|
|
28
|
+
* and a caller that wants to log what it nearly rendered could not.
|
|
29
|
+
*/
|
|
30
|
+
partialData;
|
|
31
|
+
/**
|
|
32
|
+
* The selections the server stripped, when the refusal was a pruned document
|
|
33
|
+
* rather than a partial error. Same purpose as `partialData`: a caller that
|
|
34
|
+
* wants to log what it nearly rendered, or name the offending path in a
|
|
35
|
+
* developer-facing message, has it without re-parsing anything.
|
|
36
|
+
*/
|
|
37
|
+
prunedSelections;
|
|
19
38
|
cause;
|
|
20
39
|
constructor(message, options) {
|
|
21
40
|
super(message);
|
|
@@ -23,6 +42,8 @@ var FoirRequestError = class extends Error {
|
|
|
23
42
|
this.cause = options?.cause;
|
|
24
43
|
this.status = options?.status;
|
|
25
44
|
this.graphQLErrors = options?.graphQLErrors;
|
|
45
|
+
this.partialData = options?.partialData;
|
|
46
|
+
this.prunedSelections = options?.prunedSelections;
|
|
26
47
|
}
|
|
27
48
|
/**
|
|
28
49
|
* The code of the first error, if the response carried one.
|
|
@@ -105,6 +126,9 @@ function buildFoirHeaders(input) {
|
|
|
105
126
|
if (input.bearerToken) {
|
|
106
127
|
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
107
128
|
}
|
|
129
|
+
if (input.orgId) {
|
|
130
|
+
headers["X-Org-Id"] = input.orgId;
|
|
131
|
+
}
|
|
108
132
|
if (input.extra) {
|
|
109
133
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
110
134
|
headers[key] = value;
|
|
@@ -112,14 +136,45 @@ function buildFoirHeaders(input) {
|
|
|
112
136
|
}
|
|
113
137
|
return headers;
|
|
114
138
|
}
|
|
115
|
-
|
|
139
|
+
var warnedDowngrade = null;
|
|
140
|
+
function reportAuthDowngrade(envelope) {
|
|
141
|
+
const downgrade = envelope.extensions?.auth;
|
|
142
|
+
if (!downgrade) {
|
|
143
|
+
warnedDowngrade = null;
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (warnedDowngrade === downgrade.reason) return;
|
|
147
|
+
warnedDowngrade = downgrade.reason;
|
|
148
|
+
console.warn(
|
|
149
|
+
`[foir] your customer token was ${downgrade.customerToken} (${downgrade.reason}) \u2014 this request was served as ${downgrade.servedAs}, so auth-targeted content resolved to its anonymous variant. ` + (downgrade.reason === "expired" ? "Re-exchange the token and retry." : "Check which credential you are sending.")
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
function refusePrunedDocument(json, options) {
|
|
153
|
+
const selections = json.extensions?.pruned?.selections;
|
|
154
|
+
if (!selections?.length) return;
|
|
155
|
+
const describe = (s) => `${s.path}${s.type ? ` on ${s.type}` : ""} (${s.reason})`;
|
|
156
|
+
const malformed = selections.filter((s) => MALFORMED_PRUNE_REASONS.has(s.reason));
|
|
157
|
+
if (malformed.length && options?.strict !== false) {
|
|
158
|
+
throw new FoirRequestError(
|
|
159
|
+
`[foir] the server could not serve part of this query and returned the rest without it: ${malformed.map(describe).join(", ")}. Those selections do not exist on the schema this project is serving. Pass strict: false to get the partial answer instead of this error.`,
|
|
160
|
+
{ prunedSelections: selections }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
console.warn(
|
|
164
|
+
`[foir] selections stripped from this query: ${selections.map(describe).join(", ")}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
function readFoirEnvelope(json, options) {
|
|
168
|
+
reportAuthDowngrade(json);
|
|
169
|
+
refusePrunedDocument(json, options);
|
|
116
170
|
if (json.errors?.length) {
|
|
117
171
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
118
|
-
if (json.data) {
|
|
172
|
+
if (json.data && !options?.strict) {
|
|
119
173
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
120
174
|
} else {
|
|
121
175
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
122
|
-
graphQLErrors: json.errors
|
|
176
|
+
graphQLErrors: json.errors,
|
|
177
|
+
partialData: json.data ?? void 0
|
|
123
178
|
});
|
|
124
179
|
}
|
|
125
180
|
}
|
|
@@ -160,8 +215,9 @@ async function hasFoirErrorCode(res, ...codes) {
|
|
|
160
215
|
}
|
|
161
216
|
|
|
162
217
|
// src/graphql/cdn-route.ts
|
|
163
|
-
function isCdnEligible(env, documentText, bearerToken) {
|
|
218
|
+
function isCdnEligible(env, documentText, bearerToken, orgId) {
|
|
164
219
|
if (bearerToken) return false;
|
|
220
|
+
if (orgId) return false;
|
|
165
221
|
if (isFoirPreview(env)) return false;
|
|
166
222
|
const apiKey = resolveFoirApiKey(env);
|
|
167
223
|
if (!apiKey?.startsWith("pk_")) return false;
|
|
@@ -214,12 +270,13 @@ async function foirRequest(env, document, variables, options) {
|
|
|
214
270
|
const headers = buildFoirHeaders({
|
|
215
271
|
apiKey,
|
|
216
272
|
draftChannel: isFoirPreview(env),
|
|
217
|
-
bearerToken: options?.bearerToken
|
|
273
|
+
bearerToken: options?.bearerToken,
|
|
274
|
+
orgId: options?.orgId
|
|
218
275
|
});
|
|
219
276
|
const documentText = print2(document);
|
|
220
277
|
let res;
|
|
221
278
|
try {
|
|
222
|
-
if (isCdnEligible(env, documentText, options?.bearerToken)) {
|
|
279
|
+
if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId)) {
|
|
223
280
|
const hash = await documentHash(documentText);
|
|
224
281
|
let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
|
|
225
282
|
method: "GET",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `@foir/sdk/codegen/block-registry` — DEPRECATED alias.
|
|
3
3
|
*
|
|
4
4
|
* `block` was one project's name for a flexible field, never a platform
|
|
5
|
-
* concept
|
|
5
|
+
* concept. The plugin moved to `@foir/sdk/codegen/item-registry`;
|
|
6
6
|
* this path re-exports it unchanged so an existing `codegen.ts` keeps working.
|
|
7
7
|
* Repoint the `plugins:` entry when convenient — this will be removed in a
|
|
8
8
|
* future major.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `@foir/sdk/codegen/block-scaffold` — DEPRECATED alias.
|
|
3
3
|
*
|
|
4
4
|
* `block` was one project's name for a flexible field, never a platform
|
|
5
|
-
* concept
|
|
5
|
+
* concept. The plugin moved to `@foir/sdk/codegen/item-scaffold`;
|
|
6
6
|
* this path re-exports it unchanged so an existing `codegen.ts` keeps working.
|
|
7
7
|
* Repoint the `plugins:` entry when convenient — this will be removed in a
|
|
8
8
|
* future major.
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* and item-scaffold plugins are wired by the preset and also available at
|
|
7
7
|
* `@foir/sdk/codegen/item-registry` / `@foir/sdk/codegen/item-scaffold`.
|
|
8
8
|
*
|
|
9
|
-
* The pre-
|
|
9
|
+
* The pre-rename paths `@foir/sdk/codegen/block-registry` and
|
|
10
10
|
* `/block-scaffold` still resolve, re-exporting those modules unchanged.
|
|
11
11
|
*/
|
|
12
12
|
export { foirCodegen, foirScaffold } from './preset';
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* types from the codegen output (`./graphql`) and `ItemComponent` from
|
|
14
14
|
* `@foir/sdk/content`, so it pairs directly with `createContentRenderer`.
|
|
15
15
|
*
|
|
16
|
-
* Reachable at the pre-
|
|
16
|
+
* Reachable at the pre-rename path `@foir/sdk/codegen/block-registry` too,
|
|
17
17
|
* which re-exports this module unchanged.
|
|
18
18
|
*/
|
|
19
19
|
import type { GraphQLSchema } from 'graphql';
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* in. Adding a new item type later surfaces as a compile error on
|
|
11
11
|
* `itemRegistry` (via the regenerated `ItemRegistry` type), not here.
|
|
12
12
|
*
|
|
13
|
-
* Reachable at the pre-
|
|
13
|
+
* Reachable at the pre-rename path `@foir/sdk/codegen/block-scaffold` too,
|
|
14
14
|
* which re-exports this module unchanged.
|
|
15
15
|
*/
|
|
16
16
|
import type { GraphQLSchema } from 'graphql';
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* The "switch" is a typed object lookup. Exhaustiveness is enforced where the
|
|
17
17
|
* registry is BUILT — against the exhaustive `ItemRegistry` type the codegen
|
|
18
|
-
* plugin
|
|
18
|
+
* plugin generates per project, so omitting an item type is a compile
|
|
19
19
|
* error there, not a runtime surprise here. The consumer can supply different
|
|
20
20
|
* registries per surface (e.g. web vs email).
|
|
21
21
|
*
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
* A prose field holds ENTRIES. A modeller may name the field `blocks`,
|
|
25
25
|
* `sections`, `modules` or anything else, and this renderer must not care:
|
|
26
26
|
* dispatch is on `__typename` and never on a field name. The `Block*` spellings
|
|
27
|
-
* below are deprecated aliases kept so existing consumers compile
|
|
28
|
-
* TTS-1391.
|
|
27
|
+
* below are deprecated aliases kept so existing consumers compile.
|
|
29
28
|
*/
|
|
30
29
|
import { type ReactNode } from 'react';
|
|
31
30
|
import { type PassageLike } from './richtext';
|
|
@@ -41,7 +40,7 @@ export interface ItemComponentProps<TItem, TCtx = unknown> {
|
|
|
41
40
|
/**
|
|
42
41
|
* The same value as `item`.
|
|
43
42
|
*
|
|
44
|
-
* @deprecated Use `item`. Kept so components scaffolded before
|
|
43
|
+
* @deprecated Use `item`. Kept so components scaffolded before the rename
|
|
45
44
|
* keep compiling; it is passed on every render and will be removed in a
|
|
46
45
|
* future major.
|
|
47
46
|
*/
|
|
@@ -67,7 +66,7 @@ export interface EntriesLike {
|
|
|
67
66
|
* The prose field's entries — the projection wrapper (`{ entries }`), or a
|
|
68
67
|
* plain array — plus the context threaded to every component.
|
|
69
68
|
*
|
|
70
|
-
* `items` is canonical. `blocks` is the pre-
|
|
69
|
+
* `items` is canonical. `blocks` is the pre-rename spelling and still
|
|
71
70
|
* accepted; passing both is a type error rather than a silent precedence rule,
|
|
72
71
|
* because a caller who set both cannot be told which one this honoured.
|
|
73
72
|
*/
|
package/dist/content/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export { createContentRenderer } from './content-renderer';
|
|
10
10
|
export type { ItemLike, EntriesLike, ItemComponent, ItemComponentProps, ItemRegistry, ContentRendererProps, ContentRendererOptions, PassageRunProps, } from './content-renderer';
|
|
11
11
|
/**
|
|
12
|
-
* Pre-
|
|
12
|
+
* Pre-rename spellings. `Block` was one project's name for a flexible field,
|
|
13
13
|
* never a platform concept; these are kept so existing consumers compile and
|
|
14
14
|
* will be removed in a future major.
|
|
15
15
|
*/
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
import { type ReactNode, type JSX } from 'react';
|
|
26
26
|
/**
|
|
27
27
|
* safeHref allow-lists link schemes so stored prose can't smuggle a
|
|
28
|
-
* `javascript:` (or `data:`/`vbscript:`) URL that executes on click
|
|
28
|
+
* `javascript:` (or `data:`/`vbscript:`) URL that executes on click.
|
|
29
29
|
* Relative URLs, anchors, and the http(s)/mailto/tel schemes pass; anything
|
|
30
30
|
* else collapses to '#'. Editors are not fully trusted, so this runs on the
|
|
31
31
|
* structured render path, not only the html fallback.
|