@substrat-run/adapter-cloudflare 0.113.0 → 0.116.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.
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Live reads (#938) — the wire between the coordinator and the scope's Durable Object.
3
+ *
4
+ * The two halves of the live-read path live in different files for different reasons:
5
+ * `host.ts` owns the door (who is asking, and whether this connection can carry a push
6
+ * at all) and `scope-do.ts` owns the subscription and the fan-out (which committed
7
+ * events this subscriber may be told about). Everything they must agree on is here, so
8
+ * neither end can drift from the other by editing its own file.
9
+ *
10
+ * Nothing in this module reaches the network or the database. It is names and shapes.
11
+ */
12
+ import type { PrincipalId, ScopeId, TenantId } from '@substrat-run/contracts';
13
+ /**
14
+ * The path the coordinator fetches on the scope stub to open a subscription.
15
+ *
16
+ * A path rather than an RPC method because a WebSocket cannot cross Durable Object RPC:
17
+ * a socket is not serializable, so the only way to hand one back is a `Response` with a
18
+ * `webSocket` on it, and the only thing that returns a `Response` from a DO is `fetch`.
19
+ * This is the one place in the adapter where the DO is addressed as a fetch target
20
+ * rather than as an object — every other call is `stub.<method>(…)`.
21
+ */
22
+ export declare const LIVE_SUBSCRIBE_PATH = "/_substrat/live";
23
+ /**
24
+ * WHO is subscribing, asserted by the coordinator on the request to the stub.
25
+ *
26
+ * The same trust shape as `invoke`'s `principal` argument: the coordinator resolved it
27
+ * from the vertical's own session, and the DO takes it as given. What the DO does NOT
28
+ * take as given is what that principal may see — every frame is checked individually,
29
+ * against live tuple state, at the moment it is about to be sent.
30
+ */
31
+ export declare const LIVE_PRINCIPAL_HEADER = "x-substrat-live-principal";
32
+ export declare const LIVE_TENANT_HEADER = "x-substrat-live-tenant";
33
+ export declare const LIVE_SCOPE_HEADER = "x-substrat-live-scope";
34
+ /**
35
+ * Set on every refusal this surface returns, so a client can tell "no push here, poll"
36
+ * from "your request was wrong" without parsing a body or guessing from a status.
37
+ *
38
+ * This exists because of the O2O case below. A downgrade that a client cannot observe
39
+ * is how somebody reports "live updates are broken" two months later and nobody can
40
+ * establish whether they ever worked on that hostname. One header makes the fallback a
41
+ * fact the client can log, display, and report.
42
+ */
43
+ export declare const LIVE_MODE_HEADER = "x-substrat-live";
44
+ /**
45
+ * Cloudflare's own per-request marker for orange-to-orange routing.
46
+ *
47
+ * Set by the edge on requests entering a SaaS provider's zone when the custom hostname's
48
+ * own zone is ALSO a proxied Cloudflare zone, in a different account. WebSockets are not
49
+ * supported across that path — Cloudflare's product-compatibility table lists them `No`
50
+ * for both the customer zone and the provider zone — so an upgrade offered on such a
51
+ * connection would complete for nobody, or worse, appear to.
52
+ *
53
+ * **Read per request, never cached, never configured.** There is no API field and no
54
+ * zone setting that says whether a tenant is O2O: it is decided by the tenant's own DNS,
55
+ * which is theirs to change without telling us, so anything we stored would be a fact
56
+ * with an expiry date we could not see. Cloudflare's documentation names checking this
57
+ * header in a Worker as the way to detect it, and one header read per upgrade is both
58
+ * cheaper than a matrix and correct on the request after the tenant changes their DNS.
59
+ */
60
+ export declare const O2O_HEADER = "cf-connecting-o2o";
61
+ /** Why a subscription was refused — the value of `LIVE_MODE_HEADER` on a refusal. */
62
+ export type LiveRefusal =
63
+ /** This connection cannot carry a WebSocket; the client should keep polling. */
64
+ 'poll'
65
+ /** The request was not an upgrade at all — a programming error at the caller. */
66
+ | 'not-an-upgrade';
67
+ /**
68
+ * What a subscribed socket remembers about itself across a hibernation.
69
+ *
70
+ * Carried on the socket's own attachment rather than in a field on the Durable Object,
71
+ * deliberately: a DO is evicted and revived constantly, and an in-memory roster would
72
+ * leave a subscriber holding a socket that still looks open and has silently stopped
73
+ * receiving. The attachment survives the eviction with the socket it belongs to, so a
74
+ * revived object rebuilds the roster from `ctx.getWebSockets()` and nothing is lost.
75
+ */
76
+ export interface LiveSubscription {
77
+ readonly principal: PrincipalId;
78
+ readonly tenantId: TenantId;
79
+ readonly scopeId: ScopeId;
80
+ /** When the subscription was accepted (ISO 8601) — for the roster read, and for logs. */
81
+ readonly since: string;
82
+ }
83
+ /**
84
+ * Read a socket's subscription back, or `null` if it does not have a usable one.
85
+ *
86
+ * Fail-closed on every unusable shape, including ones that "cannot happen": a socket
87
+ * whose attachment is missing, malformed, or from an older field set is a socket whose
88
+ * subscriber we cannot name — and a frame is only ever sent to a named principal whose
89
+ * permission was checked. Dropping it costs a client its live updates, which it is
90
+ * built to survive (it polls); guessing costs somebody else's row.
91
+ */
92
+ export declare function readSubscription(attachment: unknown): LiveSubscription | null;
93
+ /**
94
+ * How many newly-committed events one post-commit fan-out will consider.
95
+ *
96
+ * A bound rather than a page, deliberately: this is not a read the client walks, it is
97
+ * work the writing operation pays for after its own commit, and an unbounded loop there
98
+ * would let one bulk import hold the scope while it announced ten thousand rows to
99
+ * everybody watching. Past the bound, a subscriber simply does not hear about the tail —
100
+ * which its poll picks up, because the poll is the floor and the push is the hint.
101
+ *
102
+ * Generous relative to what one operation plus its consumers realistically emits, so
103
+ * reaching it means something unusual happened rather than something normal being cut off.
104
+ */
105
+ export declare const LIVE_FANOUT_LIMIT = 200;
106
+ /** Is this request asking to be upgraded to a WebSocket? */
107
+ export declare function isUpgradeRequest(request: Request): boolean;
108
+ /**
109
+ * Is this request arriving over an orange-to-orange hop?
110
+ *
111
+ * Cloudflare sets the header to `1`. Anything else — absent, empty, `0` — is an
112
+ * ordinary request. Compared exactly rather than for truthiness, so a future value
113
+ * this code has not seen is not silently read as "yes" (or as "no").
114
+ */
115
+ export declare function isOrangeToOrange(request: Request): boolean;
116
+ //# sourceMappingURL=live-reads.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-reads.d.ts","sourceRoot":"","sources":["../src/live-reads.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAE9E;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,oBAAoB,CAAC;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,8BAA8B,CAAC;AACjE,eAAO,MAAM,kBAAkB,2BAA2B,CAAC;AAC3D,eAAO,MAAM,iBAAiB,0BAA0B,CAAC;AAEzD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,oBAAoB,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,UAAU,sBAAsB,CAAC;AAE9C,qFAAqF;AACrF,MAAM,MAAM,WAAW;AACrB,gFAAgF;AAC9E,MAAM;AACR,iFAAiF;GAC/E,gBAAgB,CAAC;AAErB;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,OAAO,GAAG,gBAAgB,GAAG,IAAI,CAa7E;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAErC,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAI1D;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAE1D"}
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The path the coordinator fetches on the scope stub to open a subscription.
3
+ *
4
+ * A path rather than an RPC method because a WebSocket cannot cross Durable Object RPC:
5
+ * a socket is not serializable, so the only way to hand one back is a `Response` with a
6
+ * `webSocket` on it, and the only thing that returns a `Response` from a DO is `fetch`.
7
+ * This is the one place in the adapter where the DO is addressed as a fetch target
8
+ * rather than as an object — every other call is `stub.<method>(…)`.
9
+ */
10
+ export const LIVE_SUBSCRIBE_PATH = '/_substrat/live';
11
+ /**
12
+ * WHO is subscribing, asserted by the coordinator on the request to the stub.
13
+ *
14
+ * The same trust shape as `invoke`'s `principal` argument: the coordinator resolved it
15
+ * from the vertical's own session, and the DO takes it as given. What the DO does NOT
16
+ * take as given is what that principal may see — every frame is checked individually,
17
+ * against live tuple state, at the moment it is about to be sent.
18
+ */
19
+ export const LIVE_PRINCIPAL_HEADER = 'x-substrat-live-principal';
20
+ export const LIVE_TENANT_HEADER = 'x-substrat-live-tenant';
21
+ export const LIVE_SCOPE_HEADER = 'x-substrat-live-scope';
22
+ /**
23
+ * Set on every refusal this surface returns, so a client can tell "no push here, poll"
24
+ * from "your request was wrong" without parsing a body or guessing from a status.
25
+ *
26
+ * This exists because of the O2O case below. A downgrade that a client cannot observe
27
+ * is how somebody reports "live updates are broken" two months later and nobody can
28
+ * establish whether they ever worked on that hostname. One header makes the fallback a
29
+ * fact the client can log, display, and report.
30
+ */
31
+ export const LIVE_MODE_HEADER = 'x-substrat-live';
32
+ /**
33
+ * Cloudflare's own per-request marker for orange-to-orange routing.
34
+ *
35
+ * Set by the edge on requests entering a SaaS provider's zone when the custom hostname's
36
+ * own zone is ALSO a proxied Cloudflare zone, in a different account. WebSockets are not
37
+ * supported across that path — Cloudflare's product-compatibility table lists them `No`
38
+ * for both the customer zone and the provider zone — so an upgrade offered on such a
39
+ * connection would complete for nobody, or worse, appear to.
40
+ *
41
+ * **Read per request, never cached, never configured.** There is no API field and no
42
+ * zone setting that says whether a tenant is O2O: it is decided by the tenant's own DNS,
43
+ * which is theirs to change without telling us, so anything we stored would be a fact
44
+ * with an expiry date we could not see. Cloudflare's documentation names checking this
45
+ * header in a Worker as the way to detect it, and one header read per upgrade is both
46
+ * cheaper than a matrix and correct on the request after the tenant changes their DNS.
47
+ */
48
+ export const O2O_HEADER = 'cf-connecting-o2o';
49
+ /**
50
+ * Read a socket's subscription back, or `null` if it does not have a usable one.
51
+ *
52
+ * Fail-closed on every unusable shape, including ones that "cannot happen": a socket
53
+ * whose attachment is missing, malformed, or from an older field set is a socket whose
54
+ * subscriber we cannot name — and a frame is only ever sent to a named principal whose
55
+ * permission was checked. Dropping it costs a client its live updates, which it is
56
+ * built to survive (it polls); guessing costs somebody else's row.
57
+ */
58
+ export function readSubscription(attachment) {
59
+ if (typeof attachment !== 'object' || attachment === null)
60
+ return null;
61
+ const a = attachment;
62
+ if (typeof a.principal !== 'string' || a.principal === '')
63
+ return null;
64
+ if (typeof a.tenantId !== 'string' || a.tenantId === '')
65
+ return null;
66
+ if (typeof a.scopeId !== 'string' || a.scopeId === '')
67
+ return null;
68
+ if (typeof a.since !== 'string')
69
+ return null;
70
+ return {
71
+ principal: a.principal,
72
+ tenantId: a.tenantId,
73
+ scopeId: a.scopeId,
74
+ since: a.since,
75
+ };
76
+ }
77
+ /**
78
+ * How many newly-committed events one post-commit fan-out will consider.
79
+ *
80
+ * A bound rather than a page, deliberately: this is not a read the client walks, it is
81
+ * work the writing operation pays for after its own commit, and an unbounded loop there
82
+ * would let one bulk import hold the scope while it announced ten thousand rows to
83
+ * everybody watching. Past the bound, a subscriber simply does not hear about the tail —
84
+ * which its poll picks up, because the poll is the floor and the push is the hint.
85
+ *
86
+ * Generous relative to what one operation plus its consumers realistically emits, so
87
+ * reaching it means something unusual happened rather than something normal being cut off.
88
+ */
89
+ export const LIVE_FANOUT_LIMIT = 200;
90
+ /** Is this request asking to be upgraded to a WebSocket? */
91
+ export function isUpgradeRequest(request) {
92
+ // Case-insensitive: the header is `Upgrade: websocket` by the RFC, but the token is
93
+ // compared case-insensitively there too, and browsers are not the only clients.
94
+ return (request.headers.get('Upgrade') ?? '').toLowerCase() === 'websocket';
95
+ }
96
+ /**
97
+ * Is this request arriving over an orange-to-orange hop?
98
+ *
99
+ * Cloudflare sets the header to `1`. Anything else — absent, empty, `0` — is an
100
+ * ordinary request. Compared exactly rather than for truthiness, so a future value
101
+ * this code has not seen is not silently read as "yes" (or as "no").
102
+ */
103
+ export function isOrangeToOrange(request) {
104
+ return request.headers.get(O2O_HEADER) === '1';
105
+ }
106
+ //# sourceMappingURL=live-reads.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live-reads.js","sourceRoot":"","sources":["../src/live-reads.ts"],"names":[],"mappings":"AAaA;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,iBAAiB,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,2BAA2B,CAAC;AACjE,MAAM,CAAC,MAAM,kBAAkB,GAAG,wBAAwB,CAAC;AAC3D,MAAM,CAAC,MAAM,iBAAiB,GAAG,uBAAuB,CAAC;AAEzD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,iBAAiB,CAAC;AAElD;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,mBAAmB,CAAC;AA0B9C;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAAmB;IAClD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACvE,MAAM,CAAC,GAAG,UAA8D,CAAC;IACzE,IAAI,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC,SAAS,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACvE,IAAI,OAAO,CAAC,CAAC,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC,QAAQ,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACrE,IAAI,OAAO,CAAC,CAAC,OAAO,KAAK,QAAQ,IAAI,CAAC,CAAC,OAAO,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IACnE,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7C,OAAO;QACL,SAAS,EAAE,CAAC,CAAC,SAAwB;QACrC,QAAQ,EAAE,CAAC,CAAC,QAAoB;QAChC,OAAO,EAAE,CAAC,CAAC,OAAkB;QAC7B,KAAK,EAAE,CAAC,CAAC,KAAK;KACf,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC,4DAA4D;AAC5D,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,oFAAoF;IACpF,gFAAgF;IAChF,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,KAAK,WAAW,CAAC;AAC9E,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB;IAC/C,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,GAAG,CAAC;AACjD,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAiDnD,OAAO,EA0BL,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAsCtB,MAAM,sBAAsB,CAAC;AAkB9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAiaD;uEACuE;AACvE,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AA6DD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwDxD;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CAy8FjE"}
1
+ {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAmDnD,OAAO,EA0BL,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAiDtB,MAAM,sBAAsB,CAAC;AA+B9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAyaD;uEACuE;AACvE,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AA6DD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwDxD;AAED,wBAAgB,aAAa,CAC3B,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CAirHjE"}