@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/push.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
|
};
|
package/dist/react.js
CHANGED
|
@@ -24,11 +24,30 @@ function isFoirPreview(env) {
|
|
|
24
24
|
function resolveFoirApiKey(env) {
|
|
25
25
|
return isFoirPreview(env) ? env.PLATFORM_PREVIEW_API_KEY ?? env.PLATFORM_API_KEY : env.PLATFORM_API_KEY;
|
|
26
26
|
}
|
|
27
|
+
var MALFORMED_PRUNE_REASONS = /* @__PURE__ */ new Set([
|
|
28
|
+
"unknown_field",
|
|
29
|
+
"subselections_on_scalar",
|
|
30
|
+
"missing_subselections"
|
|
31
|
+
]);
|
|
27
32
|
var FoirRequestError = class extends Error {
|
|
28
33
|
/** HTTP status code, when the request failed at the HTTP layer. */
|
|
29
34
|
status;
|
|
30
35
|
/** GraphQL errors, when the request failed with a no-data error envelope. */
|
|
31
36
|
graphQLErrors;
|
|
37
|
+
/**
|
|
38
|
+
* The `data` the server did return, when a STRICT read refused a partial
|
|
39
|
+
* response. Present only on that path: without it, opting into strictness
|
|
40
|
+
* would trade a silent wrong answer for a thrown one that discards the answer,
|
|
41
|
+
* and a caller that wants to log what it nearly rendered could not.
|
|
42
|
+
*/
|
|
43
|
+
partialData;
|
|
44
|
+
/**
|
|
45
|
+
* The selections the server stripped, when the refusal was a pruned document
|
|
46
|
+
* rather than a partial error. Same purpose as `partialData`: a caller that
|
|
47
|
+
* wants to log what it nearly rendered, or name the offending path in a
|
|
48
|
+
* developer-facing message, has it without re-parsing anything.
|
|
49
|
+
*/
|
|
50
|
+
prunedSelections;
|
|
32
51
|
cause;
|
|
33
52
|
constructor(message, options) {
|
|
34
53
|
super(message);
|
|
@@ -36,6 +55,8 @@ var FoirRequestError = class extends Error {
|
|
|
36
55
|
this.cause = options?.cause;
|
|
37
56
|
this.status = options?.status;
|
|
38
57
|
this.graphQLErrors = options?.graphQLErrors;
|
|
58
|
+
this.partialData = options?.partialData;
|
|
59
|
+
this.prunedSelections = options?.prunedSelections;
|
|
39
60
|
}
|
|
40
61
|
/**
|
|
41
62
|
* The code of the first error, if the response carried one.
|
|
@@ -85,6 +106,9 @@ function buildFoirHeaders(input) {
|
|
|
85
106
|
if (input.bearerToken) {
|
|
86
107
|
headers["Authorization"] = `Bearer ${input.bearerToken}`;
|
|
87
108
|
}
|
|
109
|
+
if (input.orgId) {
|
|
110
|
+
headers["X-Org-Id"] = input.orgId;
|
|
111
|
+
}
|
|
88
112
|
if (input.extra) {
|
|
89
113
|
for (const [key, value] of Object.entries(input.extra)) {
|
|
90
114
|
headers[key] = value;
|
|
@@ -99,14 +123,45 @@ function foirApiEndpoint(apiUrl, path) {
|
|
|
99
123
|
}
|
|
100
124
|
return base + path;
|
|
101
125
|
}
|
|
102
|
-
|
|
126
|
+
var warnedDowngrade = null;
|
|
127
|
+
function reportAuthDowngrade(envelope) {
|
|
128
|
+
const downgrade = envelope.extensions?.auth;
|
|
129
|
+
if (!downgrade) {
|
|
130
|
+
warnedDowngrade = null;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (warnedDowngrade === downgrade.reason) return;
|
|
134
|
+
warnedDowngrade = downgrade.reason;
|
|
135
|
+
console.warn(
|
|
136
|
+
`[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.")
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
function refusePrunedDocument(json, options) {
|
|
140
|
+
const selections = json.extensions?.pruned?.selections;
|
|
141
|
+
if (!selections?.length) return;
|
|
142
|
+
const describe = (s) => `${s.path}${s.type ? ` on ${s.type}` : ""} (${s.reason})`;
|
|
143
|
+
const malformed = selections.filter((s) => MALFORMED_PRUNE_REASONS.has(s.reason));
|
|
144
|
+
if (malformed.length && options?.strict !== false) {
|
|
145
|
+
throw new FoirRequestError(
|
|
146
|
+
`[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.`,
|
|
147
|
+
{ prunedSelections: selections }
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
console.warn(
|
|
151
|
+
`[foir] selections stripped from this query: ${selections.map(describe).join(", ")}`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
function readFoirEnvelope(json, options) {
|
|
155
|
+
reportAuthDowngrade(json);
|
|
156
|
+
refusePrunedDocument(json, options);
|
|
103
157
|
if (json.errors?.length) {
|
|
104
158
|
const msg = json.errors.map((e) => e.message).join("; ");
|
|
105
|
-
if (json.data) {
|
|
159
|
+
if (json.data && !options?.strict) {
|
|
106
160
|
console.warn(`[foir] GraphQL partial errors (data returned): ${msg}`);
|
|
107
161
|
} else {
|
|
108
162
|
throw new FoirRequestError(`[foir] GraphQL errors: ${msg}`, {
|
|
109
|
-
graphQLErrors: json.errors
|
|
163
|
+
graphQLErrors: json.errors,
|
|
164
|
+
partialData: json.data ?? void 0
|
|
110
165
|
});
|
|
111
166
|
}
|
|
112
167
|
}
|
|
@@ -623,7 +678,9 @@ async function resolveBearerToken(config) {
|
|
|
623
678
|
function foirRoute(input) {
|
|
624
679
|
if (!input.apiKey.startsWith("pk_")) return { lane: "origin", reason: "key" };
|
|
625
680
|
if (input.bearerToken) return { lane: "origin", reason: "bearer" };
|
|
681
|
+
if (input.orgId) return { lane: "origin", reason: "org" };
|
|
626
682
|
if (input.mutation) return { lane: "origin", reason: "operation" };
|
|
683
|
+
if (input.writeToken) return { lane: "origin", reason: "floor" };
|
|
627
684
|
if (input.channel === "draft") return { lane: "origin", reason: "channel" };
|
|
628
685
|
return { lane: "shared" };
|
|
629
686
|
}
|
|
@@ -636,7 +693,7 @@ async function executeFoirDocument(config, plan) {
|
|
|
636
693
|
}
|
|
637
694
|
return data[plan.rootField];
|
|
638
695
|
}
|
|
639
|
-
async function runFoirDocument(config, document2, mutation, variables) {
|
|
696
|
+
async function runFoirDocument(config, document2, mutation, variables, options) {
|
|
640
697
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
641
698
|
const endpoint = foirDataEndpoint(config.apiUrl, "/graphql");
|
|
642
699
|
const bearerToken = await resolveBearerToken(config);
|
|
@@ -644,12 +701,14 @@ async function runFoirDocument(config, document2, mutation, variables) {
|
|
|
644
701
|
apiKey: config.apiKey,
|
|
645
702
|
draftChannel: config.channel === "draft",
|
|
646
703
|
bearerToken,
|
|
704
|
+
orgId: config.orgId,
|
|
647
705
|
extra: config.headers
|
|
648
706
|
});
|
|
649
707
|
const hash = await documentHash(document2);
|
|
650
708
|
const route = foirRoute({
|
|
651
709
|
apiKey: config.apiKey,
|
|
652
710
|
bearerToken,
|
|
711
|
+
orgId: config.orgId,
|
|
653
712
|
mutation,
|
|
654
713
|
channel: config.channel
|
|
655
714
|
});
|
|
@@ -689,7 +748,7 @@ async function runFoirDocument(config, document2, mutation, variables) {
|
|
|
689
748
|
});
|
|
690
749
|
}
|
|
691
750
|
const json = await res.json();
|
|
692
|
-
return readFoirEnvelope(json);
|
|
751
|
+
return readFoirEnvelope(json, options);
|
|
693
752
|
}
|
|
694
753
|
async function readErrorBody(res) {
|
|
695
754
|
try {
|
|
@@ -712,6 +771,7 @@ async function fetchFoirManifest(config) {
|
|
|
712
771
|
apiKey: config.apiKey,
|
|
713
772
|
draftChannel: config.channel === "draft",
|
|
714
773
|
bearerToken: await resolveBearerToken(config),
|
|
774
|
+
orgId: config.orgId,
|
|
715
775
|
extra: config.headers
|
|
716
776
|
}),
|
|
717
777
|
...config.requestInit
|
|
@@ -753,6 +813,16 @@ var AUTH_OPS = {
|
|
|
753
813
|
customerUpdatePassword: "customerUpdatePassword",
|
|
754
814
|
customerVerifyEmail: "customerVerifyEmail",
|
|
755
815
|
deleteMyAccount: "deleteMyAccount",
|
|
816
|
+
// The operator's organization context. TWO ops and no third: one to read
|
|
817
|
+
// which organizations the caller may stand in, one to say which. There is
|
|
818
|
+
// deliberately no per-call org argument anywhere in this SDK — the claim the
|
|
819
|
+
// row layer reads is resolved from the caller's own membership row on every
|
|
820
|
+
// request, so an application that never names an org cannot name the wrong
|
|
821
|
+
// one.
|
|
822
|
+
myOrganizations: "myOrganizations",
|
|
823
|
+
selectMyOrganization: "selectMyOrganization",
|
|
824
|
+
// Founding one. Present in a manifest only when the project opted in.
|
|
825
|
+
registerOperatorOrganization: "registerOperatorOrganization",
|
|
756
826
|
// External identity providers.
|
|
757
827
|
customerLoginWithProvider: "customerLoginWithProvider",
|
|
758
828
|
customerProviderCallback: "customerProviderCallback",
|
|
@@ -774,6 +844,7 @@ function authClientConfig(env, options) {
|
|
|
774
844
|
apiKey,
|
|
775
845
|
channel: isFoirPreview(env) ? "draft" : "published",
|
|
776
846
|
bearerToken: options?.bearerToken,
|
|
847
|
+
orgId: options?.orgId,
|
|
777
848
|
headers: options?.headers,
|
|
778
849
|
fetch: options?.fetch
|
|
779
850
|
};
|
package/dist/realtime/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @foir/sdk/realtime — optional thin web helper over y-websocket
|
|
2
|
+
* @foir/sdk/realtime — optional thin web helper over y-websocket.
|
|
3
3
|
*
|
|
4
4
|
* The apollo-link analogue for Foir realtime collaboration: sugar over the
|
|
5
5
|
* off-the-shelf `y-websocket` provider that adds exactly the wire-protocol
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* `yjs` + `y-websocket` are OPTIONAL peer deps gated to this subpath, so a
|
|
18
18
|
* consumer importing only other `@foir/sdk/*` exports pulls no Yjs.
|
|
19
19
|
*
|
|
20
|
-
* Wire-protocol contract: `docs/realtime-collab-contract.md
|
|
20
|
+
* Wire-protocol contract: `docs/realtime-collab-contract.md`.
|
|
21
21
|
*/
|
|
22
22
|
import type * as Y from 'yjs';
|
|
23
23
|
import { WebsocketProvider } from 'y-websocket';
|
|
@@ -36,13 +36,12 @@ export interface RoomAddress {
|
|
|
36
36
|
* with a 400).
|
|
37
37
|
*
|
|
38
38
|
* That segment is a KEY space because the platform compares it against the
|
|
39
|
-
* reserved token `live` to select an EPHEMERAL persistence policy
|
|
39
|
+
* reserved token `live` to select an EPHEMERAL persistence policy:
|
|
40
40
|
* live co-edit only — no `editing_state`, no autosave, doc destroyed on last
|
|
41
41
|
* leave. A cuid2 row id can never equal `live`, so sending an id puts that
|
|
42
42
|
* policy out of reach by construction. Worse, variants are themselves rows in
|
|
43
43
|
* `records`, so a variant id still passes the join gate: the connection
|
|
44
|
-
* succeeds and edits a DIFFERENT record, with nothing reported on either side
|
|
45
|
-
* (TTS-953).
|
|
44
|
+
* succeeds and edits a DIFFERENT record, with nothing reported on either side.
|
|
46
45
|
*
|
|
47
46
|
* The address to send is the one the API advertises — the `collab` field on
|
|
48
47
|
* every public record type returns `{ enabled, wsUrl, roomName }`, where
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@foir/sdk/server`'s event-log client — cursor-resumable consumption of a
|
|
3
|
-
* project's durable event history
|
|
3
|
+
* project's durable event history.
|
|
4
4
|
*
|
|
5
5
|
* ```ts
|
|
6
6
|
* import { createEventsClient } from '@foir/sdk/server';
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* OPERATOR SURFACE. This is not a customer-facing capability.
|
|
22
22
|
* ---------------------------------------------------------------------------
|
|
23
23
|
*
|
|
24
|
-
* DECIDED 2026-08-04
|
|
24
|
+
* DECIDED 2026-08-04. The event log is reachable by an `sk_` api key
|
|
25
25
|
* and by an admin session. It is NOT reachable by a customer token, there is no
|
|
26
26
|
* customer lane for it on api-public, and a `pk_` key can never be minted
|
|
27
27
|
* holding `events:list.project` at all.
|
|
@@ -73,6 +73,7 @@
|
|
|
73
73
|
* needs. It also keeps this module `graphql`-free, like `@foir/sdk/data`.
|
|
74
74
|
*/
|
|
75
75
|
import { FoirRequestError } from '../transport/foir-http';
|
|
76
|
+
import { type FoirReconnectPolicy, type FoirSubscription, type FoirSubscriptionHandlers, type FoirWebSocketFactory } from '../data/ws-transport';
|
|
76
77
|
/** The path `GET /v1/events` is mounted on, inside api-public's api-key group. */
|
|
77
78
|
export declare const FOIR_EVENTS_PATH = "/v1/events";
|
|
78
79
|
/**
|
|
@@ -109,7 +110,10 @@ export interface FoirEvent {
|
|
|
109
110
|
* comparing, never for counting.
|
|
110
111
|
*/
|
|
111
112
|
seq: number;
|
|
112
|
-
/**
|
|
113
|
+
/**
|
|
114
|
+
* The registry name, e.g. `"RECORD_CREATED"`. SCREAMING_SNAKE, never the
|
|
115
|
+
* dotted form.
|
|
116
|
+
*/
|
|
113
117
|
event: string;
|
|
114
118
|
/**
|
|
115
119
|
* The event's own flat keys, plus `state` and `stateSchema`.
|
|
@@ -171,7 +175,9 @@ export interface FoirEventsListParams {
|
|
|
171
175
|
/** Page size. The server defaults to 100 and caps at 1000. */
|
|
172
176
|
limit?: number;
|
|
173
177
|
/**
|
|
174
|
-
* Filter by event name, e.g. `['
|
|
178
|
+
* Filter by event name, e.g. `['RECORD_CREATED', 'RECORD_UPDATED']`.
|
|
179
|
+
* SCREAMING_SNAKE: a name in any other casing matches nothing and comes back
|
|
180
|
+
* as an empty page rather than an error, so a wrong spelling is silent. Omit for
|
|
175
181
|
* every event.
|
|
176
182
|
*/
|
|
177
183
|
events?: readonly string[];
|
|
@@ -213,6 +219,21 @@ export interface FoirEventsClientConfig {
|
|
|
213
219
|
* cannot be clobbered by accident.
|
|
214
220
|
*/
|
|
215
221
|
requestInit?: Omit<RequestInit, 'method' | 'headers' | 'body'>;
|
|
222
|
+
/**
|
|
223
|
+
* Override the WebSocket endpoint `subscribe()` connects to. Normally derived
|
|
224
|
+
* from `apiUrl`; set it when the socket is not on the API origin.
|
|
225
|
+
*/
|
|
226
|
+
wsUrl?: string;
|
|
227
|
+
/**
|
|
228
|
+
* WebSocket constructor, called as `(url, protocols)`. Defaults to
|
|
229
|
+
* `globalThis.WebSocket`. Node 18/20 have no global one, so a server process
|
|
230
|
+
* on those versions passes the `ws` package's.
|
|
231
|
+
*
|
|
232
|
+
* Only `subscribe()` uses it; `list()` and `stream()` are plain fetch.
|
|
233
|
+
*/
|
|
234
|
+
webSocketImpl?: FoirWebSocketFactory;
|
|
235
|
+
/** How the `subscribe()` socket behaves when it drops. */
|
|
236
|
+
reconnect?: FoirReconnectPolicy;
|
|
216
237
|
}
|
|
217
238
|
export interface FoirEventsClient {
|
|
218
239
|
/** One page of the project's event history. */
|
|
@@ -227,6 +248,35 @@ export interface FoirEventsClient {
|
|
|
227
248
|
* including mid-page after a crash.
|
|
228
249
|
*/
|
|
229
250
|
stream(params?: FoirEventsStreamParams): AsyncGenerator<FoirEvent, void, void>;
|
|
251
|
+
/**
|
|
252
|
+
* The same cursor, pushed instead of pulled.
|
|
253
|
+
*
|
|
254
|
+
* The server backfills from `after` and then continues live on ONE stream, so
|
|
255
|
+
* there is no gap between catching up and keeping up and no second code path
|
|
256
|
+
* to write. `stream()` remains the right choice for a batch job that wants to
|
|
257
|
+
* drain and exit; this is for a process that stays open.
|
|
258
|
+
*
|
|
259
|
+
* The cursor advances as events arrive, so a dropped socket resumes from the
|
|
260
|
+
* last event delivered rather than replaying from where the subscription
|
|
261
|
+
* originally started.
|
|
262
|
+
*
|
|
263
|
+
* A cursor older than the retention window ends the subscription with a
|
|
264
|
+
* {@link FoirEventCursorExpiredError} carrying `resumeFrom`, exactly as the
|
|
265
|
+
* HTTP lane answers 410 — a gap is always something a consumer is told about,
|
|
266
|
+
* never something it has to notice.
|
|
267
|
+
*/
|
|
268
|
+
subscribe(params: FoirEventsSubscribeParams): FoirSubscription;
|
|
269
|
+
}
|
|
270
|
+
export interface FoirEventsSubscribeParams extends FoirSubscriptionHandlers<FoirEvent> {
|
|
271
|
+
/**
|
|
272
|
+
* Exclusive cursor: begin with events having `seq > after`. Omit to start
|
|
273
|
+
* from the live edge rather than the oldest retained event — a long-running
|
|
274
|
+
* consumer that has no checkpoint usually wants what happens next, not the
|
|
275
|
+
* whole window.
|
|
276
|
+
*/
|
|
277
|
+
after?: number;
|
|
278
|
+
/** Filter by event name. Omit for every event. */
|
|
279
|
+
events?: readonly string[];
|
|
230
280
|
}
|
|
231
281
|
/**
|
|
232
282
|
* The cursor named a position older than the retention window, so events the
|
package/dist/server/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type { OperationPayload, OperationTrigger, OperationRecordContext, Operat
|
|
|
7
7
|
export { createCallbackClient } from './callback-client';
|
|
8
8
|
export type { CallbackClient, CallbackResult } from './callback-client';
|
|
9
9
|
export { createEventsClient, FoirEventCursorExpiredError, isFoirEventCursorExpired, FOIR_EVENTS_PATH, FOIR_EVENTS_SCOPE, FOIR_EVENTS_RETENTION_DAYS, } from './events-client';
|
|
10
|
-
export type { FoirEvent, FoirEventActor, FoirEventPage, FoirEventRetention, FoirEventsClient, FoirEventsClientConfig, FoirEventsListParams, FoirEventsStreamParams, } from './events-client';
|
|
10
|
+
export type { FoirEvent, FoirEventActor, FoirEventPage, FoirEventRetention, FoirEventsClient, FoirEventsClientConfig, FoirEventsListParams, FoirEventsStreamParams, FoirEventsSubscribeParams, } from './events-client';
|
|
11
|
+
export type { FoirSubscription, FoirSubscriptionHandlers } from '../data/ws-transport';
|
|
11
12
|
export { FoirRequestError, isFoirForbidden, isFoirNotFound, isFoirRateLimited, isFoirUnauthenticated, } from '../transport/foir-http';
|
|
12
|
-
export type { FoirErrorCode, FoirErrorExtensions, FoirGraphQLErrorItem } from '../transport/foir-http';
|
|
13
|
+
export type { FoirErrorCode, FoirErrorExtensions, FoirGraphQLErrorItem, FoirAuthDowngrade, } from '../transport/foir-http';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* requireScopedToken — the one-line ingress guard for BFF routes the
|
|
3
|
-
* platform dispatches operations to
|
|
3
|
+
* platform dispatches operations to.
|
|
4
4
|
*
|
|
5
5
|
* Operation endpoints must be publicly reachable (the platform's dispatcher
|
|
6
6
|
* refuses private networks), so "the BFF is never public-facing" is achieved
|