@foir/sdk 0.9.0 → 0.11.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 +719 -27
- package/README.md +121 -2
- 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 +18 -0
- package/dist/auth.js +209 -47
- package/dist/client.js +101 -13
- 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/codegen/preset.d.ts +3 -2
- 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 +81 -13
- package/dist/data/client.d.ts +92 -9
- package/dist/data/decimal.d.ts +212 -0
- package/dist/data/document.d.ts +14 -1
- package/dist/data/index.d.ts +22 -10
- package/dist/data/manifest.d.ts +40 -4
- package/dist/data/schema.d.ts +18 -11
- package/dist/data/selection.d.ts +15 -0
- package/dist/data/subscriptions.d.ts +36 -10
- package/dist/data/transport.d.ts +363 -24
- package/dist/data/ws-transport.d.ts +8 -2
- package/dist/data.js +591 -64
- package/dist/graphql/cdn-route.d.ts +1 -1
- package/dist/graphql/foir-request.d.ts +1 -1
- package/dist/index.js +0 -9
- package/dist/live/coalesce.d.ts +57 -0
- package/dist/live/drift.d.ts +58 -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 +260 -34
- package/dist/push.js +163 -36
- package/dist/react.js +157 -33
- package/dist/realtime/index.d.ts +4 -5
- package/dist/server/events-client.d.ts +54 -4
- package/dist/server/index.d.ts +4 -2
- package/dist/server/operation-types.d.ts +86 -58
- package/dist/server/read-dispatch.d.ts +53 -0
- package/dist/server/require-scoped-token.d.ts +20 -1
- package/dist/server.js +602 -10
- package/dist/transport/foir-http.d.ts +168 -15
- 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.
|
|
@@ -82,16 +103,46 @@ function readFoirRetryAfterMs(headers) {
|
|
|
82
103
|
const seconds = Number(headers.get("Retry-After"));
|
|
83
104
|
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
84
105
|
}
|
|
106
|
+
var DISPATCH_RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
107
|
+
"x-api-key",
|
|
108
|
+
"authorization",
|
|
109
|
+
"x-foir-token",
|
|
110
|
+
"x-foir-context"
|
|
111
|
+
]);
|
|
112
|
+
function refuseCredentialHeaders(extra) {
|
|
113
|
+
if (!extra) return;
|
|
114
|
+
for (const name of Object.keys(extra)) {
|
|
115
|
+
if (DISPATCH_RESERVED_HEADERS.has(name.toLowerCase())) {
|
|
116
|
+
throw new FoirRequestError(
|
|
117
|
+
`[foir] a client built from a dispatch token cannot send "${name}" in \`headers\`. The dispatch token is the credential, and an api key or a bearer beside it would make the request two actors at once. Build a separate client for the other credential.`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
85
122
|
function buildFoirHeaders(input) {
|
|
86
123
|
const headers = {
|
|
87
|
-
"Content-Type": "application/json"
|
|
88
|
-
"x-api-key": input.apiKey
|
|
124
|
+
"Content-Type": "application/json"
|
|
89
125
|
};
|
|
90
|
-
if (input.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
if (input.dispatchToken !== void 0) {
|
|
127
|
+
refuseCredentialHeaders(input.extra);
|
|
128
|
+
headers["X-Foir-Token"] = input.dispatchToken;
|
|
129
|
+
if (input.dispatchContext) {
|
|
130
|
+
headers["X-Foir-Context"] = input.dispatchContext;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
headers["x-api-key"] = input.apiKey;
|
|
134
|
+
if (input.draftChannel) {
|
|
135
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
136
|
+
}
|
|
137
|
+
if (input.bearerToken) {
|
|
138
|
+
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
139
|
+
}
|
|
140
|
+
if (input.orgId) {
|
|
141
|
+
headers["X-Org-Id"] = input.orgId;
|
|
142
|
+
}
|
|
143
|
+
if (input.orgUrl) {
|
|
144
|
+
headers["X-Org-Url"] = input.orgUrl;
|
|
145
|
+
}
|
|
95
146
|
}
|
|
96
147
|
if (input.extra) {
|
|
97
148
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -107,14 +158,48 @@ function foirApiEndpoint(apiUrl, path) {
|
|
|
107
158
|
}
|
|
108
159
|
return base + path;
|
|
109
160
|
}
|
|
110
|
-
|
|
161
|
+
var warnedDowngrade = null;
|
|
162
|
+
function reportAuthDowngrade(envelope) {
|
|
163
|
+
const downgrade = envelope.extensions?.auth;
|
|
164
|
+
if (!downgrade) {
|
|
165
|
+
warnedDowngrade = null;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (warnedDowngrade === downgrade.reason) return;
|
|
169
|
+
warnedDowngrade = downgrade.reason;
|
|
170
|
+
console.warn(
|
|
171
|
+
`[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.")
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
function refusePrunedDocument(json, options) {
|
|
175
|
+
const selections = json.extensions?.pruned?.selections;
|
|
176
|
+
if (!selections?.length) return;
|
|
177
|
+
const describe = (s) => `${s.path}${s.type ? ` on ${s.type}` : ""} (${s.reason})`;
|
|
178
|
+
const malformed = selections.filter((s) => MALFORMED_PRUNE_REASONS.has(s.reason));
|
|
179
|
+
if (malformed.length && options?.strict !== false) {
|
|
180
|
+
throw new FoirRequestError(
|
|
181
|
+
`[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.`,
|
|
182
|
+
{ prunedSelections: selections }
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
console.warn(
|
|
186
|
+
`[foir] selections stripped from this query: ${selections.map(describe).join(", ")}`
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
function answeredAnything(data) {
|
|
190
|
+
return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
|
|
191
|
+
}
|
|
192
|
+
function readFoirEnvelope(json, options) {
|
|
193
|
+
reportAuthDowngrade(json);
|
|
194
|
+
refusePrunedDocument(json, options);
|
|
111
195
|
if (json.errors?.length) {
|
|
112
196
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
113
|
-
if (json.data) {
|
|
197
|
+
if (answeredAnything(json.data) && !options?.strict) {
|
|
114
198
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
115
199
|
} else {
|
|
116
200
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
117
|
-
graphQLErrors: json.errors
|
|
201
|
+
graphQLErrors: json.errors,
|
|
202
|
+
partialData: json.data ?? void 0
|
|
118
203
|
});
|
|
119
204
|
}
|
|
120
205
|
}
|
|
@@ -155,8 +240,9 @@ async function hasFoirErrorCode(res, ...codes) {
|
|
|
155
240
|
}
|
|
156
241
|
|
|
157
242
|
// src/graphql/cdn-route.ts
|
|
158
|
-
function isCdnEligible(env, documentText, bearerToken) {
|
|
243
|
+
function isCdnEligible(env, documentText, bearerToken, orgId) {
|
|
159
244
|
if (bearerToken) return false;
|
|
245
|
+
if (orgId) return false;
|
|
160
246
|
if (isFoirPreview(env)) return false;
|
|
161
247
|
const apiKey = resolveFoirApiKey(env);
|
|
162
248
|
if (!apiKey?.startsWith("pk_")) return false;
|
|
@@ -175,12 +261,14 @@ async function foirRequest(env, document, variables, options) {
|
|
|
175
261
|
const headers = buildFoirHeaders({
|
|
176
262
|
apiKey,
|
|
177
263
|
draftChannel: isFoirPreview(env),
|
|
178
|
-
bearerToken: options?.bearerToken
|
|
264
|
+
bearerToken: options?.bearerToken,
|
|
265
|
+
orgId: options?.orgId,
|
|
266
|
+
orgUrl: options?.orgUrl
|
|
179
267
|
});
|
|
180
268
|
const documentText = print2(document);
|
|
181
269
|
let res;
|
|
182
270
|
try {
|
|
183
|
-
if (isCdnEligible(env, documentText, options?.bearerToken)) {
|
|
271
|
+
if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId ?? options?.orgUrl)) {
|
|
184
272
|
const hash = await documentHash(documentText);
|
|
185
273
|
let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
|
|
186
274
|
method: "GET",
|
|
@@ -736,10 +824,44 @@ async function resolveBearerToken(config) {
|
|
|
736
824
|
if (typeof source === "function") return await source() ?? void 0;
|
|
737
825
|
return source ?? void 0;
|
|
738
826
|
}
|
|
827
|
+
async function resolveDispatchToken(dispatch) {
|
|
828
|
+
const token = (await dispatch.token())?.trim();
|
|
829
|
+
if (!token) {
|
|
830
|
+
throw new FoirRequestError(
|
|
831
|
+
"[foir] the dispatch token provider returned no token. A client built from a dispatch calls Foir as that operation and holds no other credential. Build it from the token the endpoint was handed: `(await requireScopedToken(request)).dispatch`."
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return token;
|
|
835
|
+
}
|
|
836
|
+
async function resolveFoirAuth(config) {
|
|
837
|
+
if (config.dispatch !== void 0) {
|
|
838
|
+
return {
|
|
839
|
+
headers: buildFoirHeaders({
|
|
840
|
+
dispatchToken: await resolveDispatchToken(config.dispatch),
|
|
841
|
+
dispatchContext: config.dispatch.context ?? void 0,
|
|
842
|
+
extra: config.headers
|
|
843
|
+
})
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
const bearerToken = await resolveBearerToken(config);
|
|
847
|
+
return {
|
|
848
|
+
headers: buildFoirHeaders({
|
|
849
|
+
apiKey: config.apiKey,
|
|
850
|
+
draftChannel: config.channel === "draft",
|
|
851
|
+
bearerToken,
|
|
852
|
+
orgId: config.orgId,
|
|
853
|
+
orgUrl: config.orgUrl,
|
|
854
|
+
extra: config.headers
|
|
855
|
+
}),
|
|
856
|
+
key: { apiKey: config.apiKey, bearerToken }
|
|
857
|
+
};
|
|
858
|
+
}
|
|
739
859
|
function foirRoute(input) {
|
|
740
860
|
if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
|
|
741
861
|
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
862
|
+
if (input.orgId || input.orgUrl) return { lane: "origin", reason: "org" };
|
|
742
863
|
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
864
|
+
if (input.writeToken) return { lane: "origin", reason: "floor" };
|
|
743
865
|
if (input.channel === "draft") return { lane: "origin", reason: "channel" };
|
|
744
866
|
return { lane: "shared" };
|
|
745
867
|
}
|
|
@@ -752,23 +874,19 @@ async function executeFoirDocument(config, plan) {
|
|
|
752
874
|
}
|
|
753
875
|
return data[plan.rootField];
|
|
754
876
|
}
|
|
755
|
-
async function runFoirDocument(config, document, mutation, variables) {
|
|
877
|
+
async function runFoirDocument(config, document, mutation, variables, options) {
|
|
756
878
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
757
879
|
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
758
|
-
const
|
|
759
|
-
const headers = buildFoirHeaders({
|
|
760
|
-
apiKey: config.apiKey,
|
|
761
|
-
draftChannel: config.channel === "draft",
|
|
762
|
-
bearerToken,
|
|
763
|
-
extra: config.headers
|
|
764
|
-
});
|
|
880
|
+
const { headers, key } = await resolveFoirAuth(config);
|
|
765
881
|
const hash = await documentHash(document);
|
|
766
|
-
const
|
|
767
|
-
apiKey:
|
|
768
|
-
bearerToken,
|
|
882
|
+
const sharedKey = key !== void 0 && foirRoute({
|
|
883
|
+
apiKey: key.apiKey,
|
|
884
|
+
bearerToken: key.bearerToken,
|
|
885
|
+
orgId: config.orgId,
|
|
886
|
+
orgUrl: config.orgUrl,
|
|
769
887
|
mutation,
|
|
770
888
|
channel: config.channel
|
|
771
|
-
});
|
|
889
|
+
}).lane === "shared" ? key.apiKey : void 0;
|
|
772
890
|
const post = (body) => doFetch(endpoint, {
|
|
773
891
|
method: "POST",
|
|
774
892
|
headers,
|
|
@@ -778,8 +896,8 @@ async function runFoirDocument(config, document, mutation, variables) {
|
|
|
778
896
|
const extensions = persistedQueryExtension(hash);
|
|
779
897
|
let res;
|
|
780
898
|
try {
|
|
781
|
-
if (
|
|
782
|
-
res = await doFetch(buildCdnUrl(endpoint,
|
|
899
|
+
if (sharedKey !== void 0) {
|
|
900
|
+
res = await doFetch(buildCdnUrl(endpoint, sharedKey, hash, variables), {
|
|
783
901
|
method: "GET",
|
|
784
902
|
headers,
|
|
785
903
|
...config.requestInit
|
|
@@ -805,7 +923,7 @@ async function runFoirDocument(config, document, mutation, variables) {
|
|
|
805
923
|
});
|
|
806
924
|
}
|
|
807
925
|
const json = await res.json();
|
|
808
|
-
return readFoirEnvelope(json);
|
|
926
|
+
return readFoirEnvelope(json, options);
|
|
809
927
|
}
|
|
810
928
|
async function readErrorBody(res) {
|
|
811
929
|
try {
|
|
@@ -817,19 +935,16 @@ async function readErrorBody(res) {
|
|
|
817
935
|
}
|
|
818
936
|
|
|
819
937
|
// src/data/manifest.ts
|
|
820
|
-
async function fetchFoirManifest(config) {
|
|
821
|
-
const
|
|
938
|
+
async function fetchFoirManifest(config, options = {}) {
|
|
939
|
+
const base = foirDataEndpoint(config.apiUrl, "/v1/manifest");
|
|
940
|
+
const url = options.operation === void 0 ? base : `${base}?operation=${encodeURIComponent(options.operation)}`;
|
|
822
941
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
942
|
+
const { headers } = await resolveFoirAuth(config);
|
|
823
943
|
let res;
|
|
824
944
|
try {
|
|
825
945
|
res = await doFetch(url, {
|
|
826
946
|
method: "GET",
|
|
827
|
-
headers
|
|
828
|
-
apiKey: config.apiKey,
|
|
829
|
-
draftChannel: config.channel === "draft",
|
|
830
|
-
bearerToken: await resolveBearerToken(config),
|
|
831
|
-
extra: config.headers
|
|
832
|
-
}),
|
|
947
|
+
headers,
|
|
833
948
|
...config.requestInit
|
|
834
949
|
});
|
|
835
950
|
} catch (err) {
|
|
@@ -869,6 +984,16 @@ var AUTH_OPS = {
|
|
|
869
984
|
customerUpdatePassword: "customerUpdatePassword",
|
|
870
985
|
customerVerifyEmail: "customerVerifyEmail",
|
|
871
986
|
deleteMyAccount: "deleteMyAccount",
|
|
987
|
+
// The operator's organization context. TWO ops and no third: one to read
|
|
988
|
+
// which organizations the caller may stand in, one to say which. There is
|
|
989
|
+
// deliberately no per-call org argument anywhere in this SDK — the claim the
|
|
990
|
+
// row layer reads is resolved from the caller's own membership row on every
|
|
991
|
+
// request, so an application that never names an org cannot name the wrong
|
|
992
|
+
// one.
|
|
993
|
+
myOrganizations: "myOrganizations",
|
|
994
|
+
selectMyOrganization: "selectMyOrganization",
|
|
995
|
+
// Founding one. Present in a manifest only when the project opted in.
|
|
996
|
+
registerOperatorOrganization: "registerOperatorOrganization",
|
|
872
997
|
// External identity providers.
|
|
873
998
|
customerLoginWithProvider: "customerLoginWithProvider",
|
|
874
999
|
customerProviderCallback: "customerProviderCallback",
|
|
@@ -890,6 +1015,8 @@ function authClientConfig(env, options) {
|
|
|
890
1015
|
apiKey,
|
|
891
1016
|
channel: isFoirPreview(env) ? "draft" : "published",
|
|
892
1017
|
bearerToken: options?.bearerToken,
|
|
1018
|
+
orgId: options?.orgId,
|
|
1019
|
+
orgUrl: options?.orgUrl,
|
|
893
1020
|
headers: options?.headers,
|
|
894
1021
|
fetch: options?.fetch
|
|
895
1022
|
};
|
|
@@ -998,6 +1125,15 @@ async function customerLogout(env, options) {
|
|
|
998
1125
|
async function deleteMyAccount(env, vars, options) {
|
|
999
1126
|
return authOp(env, AUTH_OPS.deleteMyAccount, vars, options);
|
|
1000
1127
|
}
|
|
1128
|
+
async function myOrganizations(env, vars, options) {
|
|
1129
|
+
return authOp(env, AUTH_OPS.myOrganizations, vars ?? {}, options);
|
|
1130
|
+
}
|
|
1131
|
+
async function selectOrganization(env, vars, options) {
|
|
1132
|
+
return authOp(env, AUTH_OPS.selectMyOrganization, vars, options);
|
|
1133
|
+
}
|
|
1134
|
+
async function registerOrganization(env, vars, options) {
|
|
1135
|
+
return authOp(env, AUTH_OPS.registerOperatorOrganization, vars, options);
|
|
1136
|
+
}
|
|
1001
1137
|
async function saveSession(adapter, result) {
|
|
1002
1138
|
await adapter.set(TOKEN_KEY, result.token);
|
|
1003
1139
|
await adapter.set(REFRESH_KEY, result.refreshToken);
|
|
@@ -1135,25 +1271,48 @@ async function fetchHostedSession(request, config) {
|
|
|
1135
1271
|
const match = cookie.match(
|
|
1136
1272
|
new RegExp(`(?:^|;\\s*)${escapeRegExp(cookieName)}=([^;]+)`)
|
|
1137
1273
|
);
|
|
1138
|
-
if (!match) return
|
|
1274
|
+
if (!match) return { outcome: "signed-out", reason: "no-cookie" };
|
|
1275
|
+
let res;
|
|
1139
1276
|
try {
|
|
1140
|
-
|
|
1277
|
+
res = await fetch(`${config.loginOrigin}${sessionPath}`, {
|
|
1141
1278
|
headers: {
|
|
1142
1279
|
Cookie: `${cookieName}=${match[1]}`,
|
|
1143
1280
|
"X-Foir-Session": "1"
|
|
1144
1281
|
}
|
|
1145
1282
|
});
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1283
|
+
} catch (cause) {
|
|
1284
|
+
return { outcome: "unavailable", reason: "network", cause };
|
|
1285
|
+
}
|
|
1286
|
+
if (!res.ok) {
|
|
1287
|
+
if (res.status === 401) {
|
|
1288
|
+
return { outcome: "signed-out", reason: "rejected" };
|
|
1289
|
+
}
|
|
1290
|
+
if (res.status === 403) {
|
|
1291
|
+
return { outcome: "unavailable", reason: "forbidden", httpStatus: res.status };
|
|
1292
|
+
}
|
|
1293
|
+
if (res.status === 429) {
|
|
1294
|
+
return { outcome: "unavailable", reason: "rate-limited", httpStatus: res.status };
|
|
1295
|
+
}
|
|
1296
|
+
return { outcome: "unavailable", reason: "server-error", httpStatus: res.status };
|
|
1297
|
+
}
|
|
1298
|
+
let json;
|
|
1299
|
+
try {
|
|
1300
|
+
json = await res.json();
|
|
1154
1301
|
} catch {
|
|
1155
|
-
return
|
|
1302
|
+
return { outcome: "unavailable", reason: "malformed", httpStatus: res.status };
|
|
1156
1303
|
}
|
|
1304
|
+
if (!json.access_token || !json.customer_id) {
|
|
1305
|
+
return { outcome: "unavailable", reason: "malformed", httpStatus: res.status };
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
outcome: "active",
|
|
1309
|
+
session: {
|
|
1310
|
+
token: json.access_token,
|
|
1311
|
+
customerId: json.customer_id,
|
|
1312
|
+
email: json.email ?? "",
|
|
1313
|
+
expiresIn: typeof json.expires_in === "number" ? json.expires_in : 0
|
|
1314
|
+
}
|
|
1315
|
+
};
|
|
1157
1316
|
}
|
|
1158
1317
|
function escapeRegExp(s) {
|
|
1159
1318
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -1326,12 +1485,15 @@ export {
|
|
|
1326
1485
|
hostedLogoutUrl,
|
|
1327
1486
|
loginWithOTP,
|
|
1328
1487
|
loginWithPassword,
|
|
1488
|
+
myOrganizations,
|
|
1329
1489
|
myProviderToken,
|
|
1330
1490
|
refreshSession,
|
|
1491
|
+
registerOrganization,
|
|
1331
1492
|
registerWithPassword,
|
|
1332
1493
|
requireAdmin,
|
|
1333
1494
|
requireAuth,
|
|
1334
1495
|
resetPasswordAndSignIn,
|
|
1335
1496
|
saveSession,
|
|
1497
|
+
selectOrganization,
|
|
1336
1498
|
signOut
|
|
1337
1499
|
};
|
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.
|
|
@@ -94,16 +115,46 @@ function readFoirRetryAfterMs(headers) {
|
|
|
94
115
|
const seconds = Number(headers.get("Retry-After"));
|
|
95
116
|
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
96
117
|
}
|
|
118
|
+
var DISPATCH_RESERVED_HEADERS = /* @__PURE__ */ new Set([
|
|
119
|
+
"x-api-key",
|
|
120
|
+
"authorization",
|
|
121
|
+
"x-foir-token",
|
|
122
|
+
"x-foir-context"
|
|
123
|
+
]);
|
|
124
|
+
function refuseCredentialHeaders(extra) {
|
|
125
|
+
if (!extra) return;
|
|
126
|
+
for (const name of Object.keys(extra)) {
|
|
127
|
+
if (DISPATCH_RESERVED_HEADERS.has(name.toLowerCase())) {
|
|
128
|
+
throw new FoirRequestError(
|
|
129
|
+
`[foir] a client built from a dispatch token cannot send "${name}" in \`headers\`. The dispatch token is the credential, and an api key or a bearer beside it would make the request two actors at once. Build a separate client for the other credential.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
97
134
|
function buildFoirHeaders(input) {
|
|
98
135
|
const headers = {
|
|
99
|
-
"Content-Type": "application/json"
|
|
100
|
-
"x-api-key": input.apiKey
|
|
136
|
+
"Content-Type": "application/json"
|
|
101
137
|
};
|
|
102
|
-
if (input.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
if (input.dispatchToken !== void 0) {
|
|
139
|
+
refuseCredentialHeaders(input.extra);
|
|
140
|
+
headers["X-Foir-Token"] = input.dispatchToken;
|
|
141
|
+
if (input.dispatchContext) {
|
|
142
|
+
headers["X-Foir-Context"] = input.dispatchContext;
|
|
143
|
+
}
|
|
144
|
+
} else {
|
|
145
|
+
headers["x-api-key"] = input.apiKey;
|
|
146
|
+
if (input.draftChannel) {
|
|
147
|
+
headers["X-Foir-Schema-Channel"] = "draft";
|
|
148
|
+
}
|
|
149
|
+
if (input.bearerToken) {
|
|
150
|
+
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
151
|
+
}
|
|
152
|
+
if (input.orgId) {
|
|
153
|
+
headers["X-Org-Id"] = input.orgId;
|
|
154
|
+
}
|
|
155
|
+
if (input.orgUrl) {
|
|
156
|
+
headers["X-Org-Url"] = input.orgUrl;
|
|
157
|
+
}
|
|
107
158
|
}
|
|
108
159
|
if (input.extra) {
|
|
109
160
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
@@ -112,14 +163,48 @@ function buildFoirHeaders(input) {
|
|
|
112
163
|
}
|
|
113
164
|
return headers;
|
|
114
165
|
}
|
|
115
|
-
|
|
166
|
+
var warnedDowngrade = null;
|
|
167
|
+
function reportAuthDowngrade(envelope) {
|
|
168
|
+
const downgrade = envelope.extensions?.auth;
|
|
169
|
+
if (!downgrade) {
|
|
170
|
+
warnedDowngrade = null;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (warnedDowngrade === downgrade.reason) return;
|
|
174
|
+
warnedDowngrade = downgrade.reason;
|
|
175
|
+
console.warn(
|
|
176
|
+
`[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.")
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
function refusePrunedDocument(json, options) {
|
|
180
|
+
const selections = json.extensions?.pruned?.selections;
|
|
181
|
+
if (!selections?.length) return;
|
|
182
|
+
const describe = (s) => `${s.path}${s.type ? ` on ${s.type}` : ""} (${s.reason})`;
|
|
183
|
+
const malformed = selections.filter((s) => MALFORMED_PRUNE_REASONS.has(s.reason));
|
|
184
|
+
if (malformed.length && options?.strict !== false) {
|
|
185
|
+
throw new FoirRequestError(
|
|
186
|
+
`[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.`,
|
|
187
|
+
{ prunedSelections: selections }
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
console.warn(
|
|
191
|
+
`[foir] selections stripped from this query: ${selections.map(describe).join(", ")}`
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
function answeredAnything(data) {
|
|
195
|
+
return typeof data === "object" && data !== null && Object.values(data).some((value) => value !== null && value !== void 0);
|
|
196
|
+
}
|
|
197
|
+
function readFoirEnvelope(json, options) {
|
|
198
|
+
reportAuthDowngrade(json);
|
|
199
|
+
refusePrunedDocument(json, options);
|
|
116
200
|
if (json.errors?.length) {
|
|
117
201
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
118
|
-
if (json.data) {
|
|
202
|
+
if (answeredAnything(json.data) && !options?.strict) {
|
|
119
203
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
120
204
|
} else {
|
|
121
205
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
122
|
-
graphQLErrors: json.errors
|
|
206
|
+
graphQLErrors: json.errors,
|
|
207
|
+
partialData: json.data ?? void 0
|
|
123
208
|
});
|
|
124
209
|
}
|
|
125
210
|
}
|
|
@@ -160,8 +245,9 @@ async function hasFoirErrorCode(res, ...codes) {
|
|
|
160
245
|
}
|
|
161
246
|
|
|
162
247
|
// src/graphql/cdn-route.ts
|
|
163
|
-
function isCdnEligible(env, documentText, bearerToken) {
|
|
248
|
+
function isCdnEligible(env, documentText, bearerToken, orgId) {
|
|
164
249
|
if (bearerToken) return false;
|
|
250
|
+
if (orgId) return false;
|
|
165
251
|
if (isFoirPreview(env)) return false;
|
|
166
252
|
const apiKey = resolveFoirApiKey(env);
|
|
167
253
|
if (!apiKey?.startsWith("pk_")) return false;
|
|
@@ -214,12 +300,14 @@ async function foirRequest(env, document, variables, options) {
|
|
|
214
300
|
const headers = buildFoirHeaders({
|
|
215
301
|
apiKey,
|
|
216
302
|
draftChannel: isFoirPreview(env),
|
|
217
|
-
bearerToken: options?.bearerToken
|
|
303
|
+
bearerToken: options?.bearerToken,
|
|
304
|
+
orgId: options?.orgId,
|
|
305
|
+
orgUrl: options?.orgUrl
|
|
218
306
|
});
|
|
219
307
|
const documentText = print2(document);
|
|
220
308
|
let res;
|
|
221
309
|
try {
|
|
222
|
-
if (isCdnEligible(env, documentText, options?.bearerToken)) {
|
|
310
|
+
if (isCdnEligible(env, documentText, options?.bearerToken, options?.orgId ?? options?.orgUrl)) {
|
|
223
311
|
const hash = await documentHash(documentText);
|
|
224
312
|
let shared = await fetch(buildCdnUrl(endpoint, apiKey, hash, variables), {
|
|
225
313
|
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';
|
package/dist/codegen/preset.d.ts
CHANGED
|
@@ -21,9 +21,10 @@ export interface FoirCodegenOptions {
|
|
|
21
21
|
*/
|
|
22
22
|
apiKey?: string;
|
|
23
23
|
/**
|
|
24
|
-
* Preview
|
|
24
|
+
* Preview key holding `drafts:read`. Reads the DRAFT schema channel so
|
|
25
25
|
* freshly pushed (un-promoted) model changes are visible to codegen.
|
|
26
|
-
* Strongly preferred
|
|
26
|
+
* Strongly preferred: a key without `drafts:read` only sees the published
|
|
27
|
+
* schema.
|
|
27
28
|
*/
|
|
28
29
|
previewKey?: string;
|
|
29
30
|
/** Output directory for the generated client types. Default `./app/gql/foir/`. */
|