@foir/sdk 0.9.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 +497 -27
- package/README.md +111 -0
- package/dist/auth/auth.d.ts +72 -1
- package/dist/auth/hosted.d.ts +71 -4
- package/dist/auth/index.d.ts +3 -3
- package/dist/auth/operations.d.ts +84 -0
- package/dist/auth/ops.d.ts +10 -0
- package/dist/auth.js +127 -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 +64 -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 +10 -8
- package/dist/data/manifest.d.ts +9 -1
- 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);
|
|
@@ -1135,25 +1217,48 @@ async function fetchHostedSession(request, config) {
|
|
|
1135
1217
|
const match = cookie.match(
|
|
1136
1218
|
new RegExp(`(?:^|;\\s*)${escapeRegExp(cookieName)}=([^;]+)`)
|
|
1137
1219
|
);
|
|
1138
|
-
if (!match) return
|
|
1220
|
+
if (!match) return { outcome: "signed-out", reason: "no-cookie" };
|
|
1221
|
+
let res;
|
|
1139
1222
|
try {
|
|
1140
|
-
|
|
1223
|
+
res = await fetch(`${config.loginOrigin}${sessionPath}`, {
|
|
1141
1224
|
headers: {
|
|
1142
1225
|
Cookie: `${cookieName}=${match[1]}`,
|
|
1143
1226
|
"X-Foir-Session": "1"
|
|
1144
1227
|
}
|
|
1145
1228
|
});
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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();
|
|
1154
1247
|
} catch {
|
|
1155
|
-
return
|
|
1248
|
+
return { outcome: "unavailable", reason: "malformed", httpStatus: res.status };
|
|
1156
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
|
+
};
|
|
1157
1262
|
}
|
|
1158
1263
|
function escapeRegExp(s) {
|
|
1159
1264
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1326,12 +1431,15 @@ export {
|
|
|
1326
1431
|
hostedLogoutUrl,
|
|
1327
1432
|
loginWithOTP,
|
|
1328
1433
|
loginWithPassword,
|
|
1434
|
+
myOrganizations,
|
|
1329
1435
|
myProviderToken,
|
|
1330
1436
|
refreshSession,
|
|
1437
|
+
registerOrganization,
|
|
1331
1438
|
registerWithPassword,
|
|
1332
1439
|
requireAdmin,
|
|
1333
1440
|
requireAuth,
|
|
1334
1441
|
resetPasswordAndSignIn,
|
|
1335
1442
|
saveSession,
|
|
1443
|
+
selectOrganization,
|
|
1336
1444
|
signOut
|
|
1337
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.
|
package/dist/data/builtins.d.ts
CHANGED
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
* `models[key]` would skip them (an inexhaustive union, silently wrong at
|
|
21
21
|
* runtime) or emit `never`.
|
|
22
22
|
*
|
|
23
|
-
* # These are ONE set, not two
|
|
23
|
+
* # These are ONE set, not two
|
|
24
24
|
*
|
|
25
25
|
* They used to be two: a value object for a plain field, and a separate
|
|
26
|
-
* `<Name>Block` wrapper for the same thing inside a flexible field.
|
|
26
|
+
* `<Name>Block` wrapper for the same thing inside a flexible field. The rename
|
|
27
27
|
* removed the wrapper — an item IS its value — so a plain `image` field and an
|
|
28
28
|
* `image` inside a flexible field are the SAME GraphQL type, `Image`. There is
|
|
29
29
|
* no `ImageValue` in a real schema and never will be again; the only difference
|
|
@@ -78,7 +78,7 @@ export interface Image {
|
|
|
78
78
|
* `String` on the wire, not `String!`.
|
|
79
79
|
*
|
|
80
80
|
* It was non-null here and nullable on the flexible-field member until
|
|
81
|
-
*
|
|
81
|
+
* the rename, which is a §5.3.2 response-shape conflict the moment both are one
|
|
82
82
|
* type. The platform relaxed rather than tightened, because nullable is the
|
|
83
83
|
* honest form — its own public `File` asset type already declares
|
|
84
84
|
* `url: String` — so this follows.
|
|
@@ -215,7 +215,7 @@ export interface RecordAncestor {
|
|
|
215
215
|
* 193 stored reference values carry an id and 92 carry a natural key and no id
|
|
216
216
|
* — so a consumer must handle either.
|
|
217
217
|
*
|
|
218
|
-
* `_id` and `_type` are NOT here, and their absence is the
|
|
218
|
+
* `_id` and `_type` are NOT here, and their absence is the correction
|
|
219
219
|
* rather than an omission. They were the wrapper's own metadata: `_id` was the
|
|
220
220
|
* ITEM's id (never the target's, which is why `_targetId` exists separately) and
|
|
221
221
|
* `_type` was the union discriminator, reading `"reference"` on every reference
|
|
@@ -286,10 +286,10 @@ export interface CollabInfo {
|
|
|
286
286
|
* The three wrappers below have no replacement, because the things they
|
|
287
287
|
* described no longer exist:
|
|
288
288
|
*
|
|
289
|
-
* - `MediaBlock` — `media` stopped being a primitive
|
|
289
|
+
* - `MediaBlock` — `media` stopped being a primitive. It was
|
|
290
290
|
* never in the proto FieldType enum, and it is the one primitive name a
|
|
291
291
|
* project had also used as a model key.
|
|
292
|
-
* - `TextBlock` —
|
|
292
|
+
* - `TextBlock` — removed as well. GraphQL requires a union member to be
|
|
293
293
|
* an object type and a text item's value is a bare scalar, which is the
|
|
294
294
|
* whole reason a wrapper existed. An inline model with a text field is how
|
|
295
295
|
* a text slot is authored.
|
|
@@ -367,7 +367,7 @@ export interface FoirDegraded<TTypename extends string = string> {
|
|
|
367
367
|
* Four because depth is a budget against the server's 10,000-field ceiling and
|
|
368
368
|
* four is past what a generated module usefully describes: on the live dev
|
|
369
369
|
* project `product.findUnique` measures 318 fields at the default, 468 at 2 and
|
|
370
|
-
* 618 at 3. Those levels all fit
|
|
370
|
+
* 618 at 3. Those levels all fit now — they read 1,203 / 4,308 /
|
|
371
371
|
* 13,623 when a union position was charged for every member — so the bound here
|
|
372
372
|
* is about how much generated type surface is worth emitting, not about what
|
|
373
373
|
* the server will run. A call
|
|
@@ -429,7 +429,8 @@ export interface FoirUpdateManyPayload {
|
|
|
429
429
|
ids: string[];
|
|
430
430
|
}
|
|
431
431
|
/**
|
|
432
|
-
* What a note
|
|
432
|
+
* What a customer's note can be ABOUT: the customer-plane subset of the closed
|
|
433
|
+
* set the platform's write path enforces.
|
|
433
434
|
*
|
|
434
435
|
* On the wire it is `String!`, not a GraphQL enum, so the manifest publishes no
|
|
435
436
|
* `enumValues` for it and `foir types` cannot generate this union. It is here
|
|
@@ -440,10 +441,14 @@ export interface FoirUpdateManyPayload {
|
|
|
440
441
|
* with a model keyed `file` would make one value mean two things; a note on a
|
|
441
442
|
* record is `'record'` plus the model key on `subjectModelKey`.
|
|
442
443
|
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
444
|
+
* The platform set also holds `project` and `organization`, and this union
|
|
445
|
+
* deliberately does not. Both are operator constructs: the platform answers a
|
|
446
|
+
* note about either through a row policy no customer claim passes, so no note
|
|
447
|
+
* this surface reads or writes can carry them. This mirrors the part of the
|
|
448
|
+
* platform set a customer can reach, and the obligation that comes with a mirror
|
|
449
|
+
* (see the file header) applies: a kind the platform opens to customers widens
|
|
450
|
+
* this union in a minor release, and until then a value outside it is a row no
|
|
451
|
+
* customer writer could have produced.
|
|
447
452
|
*/
|
|
448
453
|
export type FoirNoteSubjectType = 'record' | 'file' | 'model' | 'operation' | 'schedule' | 'customer';
|
|
449
454
|
/**
|
|
@@ -550,6 +555,53 @@ export interface FoirCreateMyNoteArgs {
|
|
|
550
555
|
*/
|
|
551
556
|
attachmentFileIds?: string[];
|
|
552
557
|
}
|
|
558
|
+
/**
|
|
559
|
+
* `submitAnonymousNote` arguments — feedback from a visitor with no account.
|
|
560
|
+
*
|
|
561
|
+
* Present on the project's schema only while the project has opted in
|
|
562
|
+
* (Notes settings → "Accept anonymous notes"). The feedback widget does NOT
|
|
563
|
+
* use this: a signed-out visitor there is still asked to sign in, so this
|
|
564
|
+
* mutation is the only path a visitor's note can take.
|
|
565
|
+
*
|
|
566
|
+
* WRITE-ONLY. Nothing reads an anonymous note back on the public plane, which
|
|
567
|
+
* is why the result is a receipt rather than the note.
|
|
568
|
+
*/
|
|
569
|
+
export interface FoirSubmitAnonymousNoteArgs {
|
|
570
|
+
/**
|
|
571
|
+
* Must be `'record'` — the only thing a visitor may write a note about.
|
|
572
|
+
* Narrower than `FoirCreateMyNoteArgs`, where a customer may also address a
|
|
573
|
+
* file or a model.
|
|
574
|
+
*/
|
|
575
|
+
subjectType: 'record';
|
|
576
|
+
subjectId: string;
|
|
577
|
+
/**
|
|
578
|
+
* The record's model key. REQUIRED here, unlike on `createMyNote` where it is
|
|
579
|
+
* conditional: the subject is always a record on this lane, so the condition
|
|
580
|
+
* is always met.
|
|
581
|
+
*/
|
|
582
|
+
subjectModelKey: string;
|
|
583
|
+
body: string;
|
|
584
|
+
/**
|
|
585
|
+
* Optional. Where the operators may reply.
|
|
586
|
+
*
|
|
587
|
+
* Captured as typed and NEVER verified — it is not treated as proof of who
|
|
588
|
+
* wrote the note, and nothing is sent to it to confirm it. Do not build a
|
|
589
|
+
* flow that assumes the address is reachable.
|
|
590
|
+
*/
|
|
591
|
+
contactEmail?: string;
|
|
592
|
+
}
|
|
593
|
+
/** What `submitAnonymousNote` returns. */
|
|
594
|
+
export interface FoirAnonymousNoteReceipt {
|
|
595
|
+
/**
|
|
596
|
+
* A reference for this submission, to quote when following it up.
|
|
597
|
+
*
|
|
598
|
+
* NOT a handle: there is no read that takes it. Showing it to the visitor is
|
|
599
|
+
* the whole of its purpose.
|
|
600
|
+
*/
|
|
601
|
+
reference: string;
|
|
602
|
+
/** When the platform recorded the submission. */
|
|
603
|
+
submittedAt: string;
|
|
604
|
+
}
|
|
553
605
|
export type FoirDeviceType = 'DESKTOP' | 'MOBILE' | 'TABLET';
|
|
554
606
|
export type FoirPlatformType = 'IOS' | 'ANDROID' | 'WEB';
|
|
555
607
|
/**
|