stitchkit 0.75.1 → 0.76.1
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/dist/application/keyspace.d.ts +37 -0
- package/dist/application/keyspace.d.ts.map +1 -1
- package/dist/application/watch-hub.d.ts +11 -2
- package/dist/application/watch-hub.d.ts.map +1 -1
- package/dist/application.d.ts +1 -1
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +29 -5
- package/dist/live/watch-client.d.ts +78 -5
- package/dist/live/watch-client.d.ts.map +1 -1
- package/dist/live.d.ts +1 -1
- package/dist/live.d.ts.map +1 -1
- package/dist/live.js +63 -26
- package/dist/server/index.js +11 -1
- package/dist/server/middleware/trust-fence.d.ts +21 -0
- package/dist/server/middleware/trust-fence.d.ts.map +1 -1
- package/llms-full.txt +107 -7
- package/package.json +1 -1
|
@@ -29,6 +29,18 @@
|
|
|
29
29
|
* and the owner's to configure. Saying "durable" without saying whose durability
|
|
30
30
|
* would be the more comfortable lie.
|
|
31
31
|
*
|
|
32
|
+
* ## Two ways in, because there are two lifecycles
|
|
33
|
+
*
|
|
34
|
+
* {@link keyspaceResource} declares it to the application kernel, which is right
|
|
35
|
+
* whenever there is a kernel: it closes after everything that writes to it,
|
|
36
|
+
* because a writer that depends on it is stopped first.
|
|
37
|
+
*
|
|
38
|
+
* {@link openKeyspace} opens one directly, for an application that owns its own
|
|
39
|
+
* lifecycle — a server that binds its signals and closes what it holds in an
|
|
40
|
+
* order it wrote itself. The resource is a thin wrapper over exactly this, not a
|
|
41
|
+
* second implementation: without it the only way to open a keyspace would be to
|
|
42
|
+
* have a kernel, which is a requirement nothing about a keyspace justifies.
|
|
43
|
+
*
|
|
32
44
|
* ## It is a resource, not a global
|
|
33
45
|
*
|
|
34
46
|
* A keyspace is opened as a {@link ManagedResource} and its handle is read with
|
|
@@ -127,6 +139,31 @@ export declare function defineKeyspace<TValue>(name: string, declaration: {
|
|
|
127
139
|
schema: z.ZodType<TValue>;
|
|
128
140
|
key: (value: TValue) => string;
|
|
129
141
|
}): KeyspaceDeclaration<TValue>;
|
|
142
|
+
/**
|
|
143
|
+
* An opened keyspace, plus the lifecycle its owner drives.
|
|
144
|
+
*
|
|
145
|
+
* The same four phases a {@link ManagedResource} has, because they are the same
|
|
146
|
+
* four phases — an application without a kernel calls them in the order it
|
|
147
|
+
* chose, and one with a kernel gets them called for it.
|
|
148
|
+
*/
|
|
149
|
+
export interface OpenedKeyspace<TValue> {
|
|
150
|
+
readonly keyspace: OpenKeyspace<TValue>;
|
|
151
|
+
/** Refuse new writes. The queued ones still finish. */
|
|
152
|
+
stopAdmission(): void;
|
|
153
|
+
/** Wait for every accepted write to reach the backend. */
|
|
154
|
+
drain(): Promise<void>;
|
|
155
|
+
/** Report anything still unwritten, then close the backend. */
|
|
156
|
+
close(): Promise<void>;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Open a keyspace directly, for an application that owns its own lifecycle.
|
|
160
|
+
*
|
|
161
|
+
* Loads every stored record before it returns, so `get` and `list` are
|
|
162
|
+
* answerable from the first call. Where there is a kernel, prefer
|
|
163
|
+
* {@link keyspaceResource} — it gets the ordering right without anyone
|
|
164
|
+
* remembering to.
|
|
165
|
+
*/
|
|
166
|
+
export declare function openKeyspace<TValue>(declaration: KeyspaceDeclaration<TValue>, config: KeyspaceResourceConfig<TValue>): Promise<OpenedKeyspace<TValue>>;
|
|
130
167
|
/**
|
|
131
168
|
* A keyspace as a managed resource. Its `start` publishes the {@link OpenKeyspace}.
|
|
132
169
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyspace.d.ts","sourceRoot":"","sources":["../../src/application/keyspace.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"keyspace.d.ts","sourceRoot":"","sources":["../../src/application/keyspace.ts"],"names":[],"mappings":"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"}
|
|
@@ -80,8 +80,17 @@ export interface WatchHubConfig {
|
|
|
80
80
|
read(operation: WatchOperation, args: unknown): Promise<unknown>;
|
|
81
81
|
/** Whether an operation may be watched at all. Refusal is answered in words. */
|
|
82
82
|
watchable(operation: WatchOperation): boolean;
|
|
83
|
-
/**
|
|
84
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The topics whose announcement means this answer may have changed.
|
|
85
|
+
*
|
|
86
|
+
* Given the arguments as well as the operation, so a topic can name what the
|
|
87
|
+
* answer actually depends on: `chat.transcript:<address>` rather than
|
|
88
|
+
* `chat.transcript`. Without that narrowing, one address changing wakes every
|
|
89
|
+
* watcher of the operation — twenty conversations open means twenty reads for
|
|
90
|
+
* one change, and nineteen of them publish nothing because nothing changed.
|
|
91
|
+
* The read is still paid.
|
|
92
|
+
*/
|
|
93
|
+
invalidatedBy(operation: WatchOperation, args: unknown): readonly string[];
|
|
85
94
|
/** Subscribe to a topic; returns the unsubscribe. Normally an event bus's `on`. */
|
|
86
95
|
subscribe(topic: string, listener: () => void): () => void;
|
|
87
96
|
/** The most keys one subscriber may watch at once. Default 64. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch-hub.d.ts","sourceRoot":"","sources":["../../src/application/watch-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,sBAAsB,CAAC;AAEzE,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,oDAAoD;IACpD,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3E,KAAK,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;;;;;OAeG;IACH,IAAI,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,gFAAgF;IAChF,SAAS,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC;IAC9C
|
|
1
|
+
{"version":3,"file":"watch-hub.d.ts","sourceRoot":"","sources":["../../src/application/watch-hub.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,sBAAsB,CAAC;AAEzE,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,mFAAmF;AACnF,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;IACpC,oDAAoD;IACpD,KAAK,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,GAAG;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3E,KAAK,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3B,yEAAyE;IACzE,MAAM,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B;;;;;;;;;;;;;;;OAeG;IACH,IAAI,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,gFAAgF;IAChF,SAAS,CAAC,SAAS,EAAE,cAAc,GAAG,OAAO,CAAC;IAC9C;;;;;;;;;OASG;IACH,aAAa,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,SAAS,MAAM,EAAE,CAAC;IAC3E,mFAAmF;IACnF,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC;IAC3D,kEAAkE;IAClE,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,yFAAyF;IACzF,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjD,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,CAAC,UAAU,EAAE,eAAe,GAAG,eAAe,CAAC;IACrD;;;;;;OAMG;IACH,SAAS,IAAI,MAAM,CAAC;IACpB,qEAAqE;IACrE,IAAI,IAAI,MAAM,CAAC;IACf,KAAK,IAAI,IAAI,CAAC;CACf;AAqBD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,SAAS,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAU1F;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,QAAQ,CA4M/D"}
|
package/dist/application.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export { type DiagnosticJournal, type DiagnosticJournalCloseResult, DiagnosticJo
|
|
|
6
6
|
export { type ApplicationEventSink, type ApplicationEventSinkConfig, type ApplicationLifecycleEvent, ApplicationLifecycleEventSchema, applicationLifecycleEvent, createApplicationEventSink, } from './application/events.js';
|
|
7
7
|
export { type ApplicationHealthHandlerOptions, ApplicationHealthHandlerOptionsSchema, type ApplicationOperationalHandlers, type ApplicationOperationalHandlersOptions, ApplicationOperationalHandlersOptionsSchema, createApplicationHealthHandler, createApplicationOperationalHandlers, } from './application/health.js';
|
|
8
8
|
export { type ApplicationAdmission, ApplicationAdmissionError, type ApplicationConfig, type ApplicationHandle, type ApplicationOperationLease, type ApplicationResourceFailure, type ApplicationResourcePhase, type ApplicationShutdownBudget, ApplicationShutdownBudgetSchema, type ApplicationShutdownOptions, ApplicationShutdownOptionsSchema, createApplication, } from './application/kernel.js';
|
|
9
|
-
export { defineKeyspace, type KeyspaceBackend, type KeyspaceChange, type KeyspaceDeclaration, type KeyspaceResourceConfig, keyspaceResource, memoryKeyspaceBackend, type OpenKeyspace, } from './application/keyspace.js';
|
|
9
|
+
export { defineKeyspace, type KeyspaceBackend, type KeyspaceChange, type KeyspaceDeclaration, type KeyspaceResourceConfig, keyspaceResource, memoryKeyspaceBackend, type OpenedKeyspace, type OpenKeyspace, openKeyspace, } from './application/keyspace.js';
|
|
10
10
|
export { type SqliteKeyspaceBackendConfig, sqliteKeyspaceBackend, } from './application/keyspace-sqlite.js';
|
|
11
11
|
export { type ApplicationSnapshotSink, type ApplicationSnapshotSinkConfig, type ApplicationSnapshotSinkError, type ApplicationSnapshotSinkStatus, ApplicationSnapshotSinkStatusSchema, createApplicationSnapshotSink, type RevisionedApplicationSnapshot, } from './application/latest-sink.js';
|
|
12
12
|
export { defineManagedResource, type ManagedResource, type ManagedResourceContext, type ManagedResourceDependency, type ManagedResourcePublished, type ManagedResourcePublishesNoValue, type ManagedResourceStartResult, managedResourceDependencyId, } from './application/resource.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../src/application.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"application.d.ts","sourceRoot":"","sources":["../src/application.ts"],"names":[],"mappings":"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"}
|
package/dist/application.js
CHANGED
|
@@ -1223,7 +1223,32 @@ function defineKeyspace(name, declaration) {
|
|
|
1223
1223
|
}
|
|
1224
1224
|
return { name, schema: declaration.schema, key: declaration.key };
|
|
1225
1225
|
}
|
|
1226
|
+
async function openKeyspace(declaration, config) {
|
|
1227
|
+
const machine = keyspaceMachine(declaration, config);
|
|
1228
|
+
await machine.load();
|
|
1229
|
+
return {
|
|
1230
|
+
keyspace: machine.keyspace,
|
|
1231
|
+
stopAdmission: machine.stopAdmission,
|
|
1232
|
+
drain: machine.drain,
|
|
1233
|
+
close: machine.close
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1226
1236
|
function keyspaceResource(declaration, config) {
|
|
1237
|
+
const machine = keyspaceMachine(declaration, config);
|
|
1238
|
+
return {
|
|
1239
|
+
id: config.id ?? `keyspace:${declaration.name}`,
|
|
1240
|
+
...config.dependsOn && { dependsOn: config.dependsOn },
|
|
1241
|
+
async start(_context) {
|
|
1242
|
+
await machine.load();
|
|
1243
|
+
return { value: machine.keyspace };
|
|
1244
|
+
},
|
|
1245
|
+
stopAdmission: machine.stopAdmission,
|
|
1246
|
+
drain: machine.drain,
|
|
1247
|
+
close: machine.close,
|
|
1248
|
+
force: machine.force
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function keyspaceMachine(declaration, config) {
|
|
1227
1252
|
const maxPending = config.maxPendingWrites ?? 1024;
|
|
1228
1253
|
const records = new Map;
|
|
1229
1254
|
let admitting = true;
|
|
@@ -1281,14 +1306,12 @@ function keyspaceResource(declaration, config) {
|
|
|
1281
1306
|
config.onUnwritten?.(pending);
|
|
1282
1307
|
}
|
|
1283
1308
|
return {
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
async start(_context) {
|
|
1309
|
+
keyspace,
|
|
1310
|
+
async load() {
|
|
1287
1311
|
for (const stored of await config.backend.load()) {
|
|
1288
1312
|
const value = declaration.schema.parse(stored);
|
|
1289
1313
|
records.set(declaration.key(value), value);
|
|
1290
1314
|
}
|
|
1291
|
-
return { value: keyspace };
|
|
1292
1315
|
},
|
|
1293
1316
|
stopAdmission() {
|
|
1294
1317
|
admitting = false;
|
|
@@ -1610,7 +1633,7 @@ function createWatchHub(config) {
|
|
|
1610
1633
|
state: { key, phase: "opening" },
|
|
1611
1634
|
backoff: createBackoff(config.backoff ?? DEFAULT_BACKOFF)
|
|
1612
1635
|
};
|
|
1613
|
-
for (const topic of config.invalidatedBy(operation)) {
|
|
1636
|
+
for (const topic of config.invalidatedBy(operation, args)) {
|
|
1614
1637
|
source.unsubscribes.push(config.subscribe(topic, () => {
|
|
1615
1638
|
source.dirty = true;
|
|
1616
1639
|
pump(source);
|
|
@@ -1724,6 +1747,7 @@ export {
|
|
|
1724
1747
|
sqliteKeyspaceBackend,
|
|
1725
1748
|
readDiagnosticJournalLockDiagnosis,
|
|
1726
1749
|
projectApplicationStatus,
|
|
1750
|
+
openKeyspace,
|
|
1727
1751
|
memoryKeyspaceBackend,
|
|
1728
1752
|
managedServerResource,
|
|
1729
1753
|
managedResourceDependencyId,
|
|
@@ -17,6 +17,23 @@
|
|
|
17
17
|
* `createUrlBuilder`, and the identity is the contract's own
|
|
18
18
|
* `(prefix, endpoint key)` — the same pair the server labels every request with.
|
|
19
19
|
*
|
|
20
|
+
* ## A connection is not forever, and recovering is this client's job
|
|
21
|
+
*
|
|
22
|
+
* A socket drops and comes back, and the hub forgets everything the old
|
|
23
|
+
* connection held — it releases a subscriber's keys the moment that subscriber
|
|
24
|
+
* detaches. So a client that opened each question once and never again is a
|
|
25
|
+
* client whose face freezes the first time a server restarts, *silently*,
|
|
26
|
+
* because nothing tells the subscriber that what it is looking at stopped being
|
|
27
|
+
* live.
|
|
28
|
+
*
|
|
29
|
+
* This client holds every key and every listener, so nobody else can do it: on a
|
|
30
|
+
* drop it tells subscribers the source is gone and forgets that anything was
|
|
31
|
+
* opened; on a fresh connection it re-opens every key that still has a listener.
|
|
32
|
+
* That is why {@link WatchTransport} **requires** `onConnectionChange` rather
|
|
33
|
+
* than using it when offered — a transport that cannot say when it reconnected
|
|
34
|
+
* cannot host a recovering client, and an optional hook would turn recovery into
|
|
35
|
+
* something that silently did not happen.
|
|
36
|
+
*
|
|
20
37
|
* ## Retention
|
|
21
38
|
*
|
|
22
39
|
* The last value of a key is kept while anyone holds it, and for `holdMs` after
|
|
@@ -27,7 +44,7 @@
|
|
|
27
44
|
* sense a rendering component can use.
|
|
28
45
|
*/
|
|
29
46
|
import type { ContractDef, EndpointDef } from '../contract/define.js';
|
|
30
|
-
import { type WatchKey, type WatchStateFrame } from './watch-contract.js';
|
|
47
|
+
import { WATCH_CLOSE, WATCH_OPEN, WATCH_STATE, WATCH_VALUE, type WatchKey, type WatchStateFrame, type WatchValueFrame } from './watch-contract.js';
|
|
31
48
|
export interface WatchListeners<TValue> {
|
|
32
49
|
value(value: TValue): void;
|
|
33
50
|
/** Phase and, when unhealthy, the read's own code and message. */
|
|
@@ -39,16 +56,72 @@ export interface WatchHandle<TValue> {
|
|
|
39
56
|
/** Drops every listener this handle registered and releases the key. */
|
|
40
57
|
close(): void;
|
|
41
58
|
}
|
|
42
|
-
/**
|
|
59
|
+
/** What the server sends a watcher, by event name. */
|
|
60
|
+
export interface WatchInboundEvents {
|
|
61
|
+
[WATCH_VALUE]: WatchValueFrame;
|
|
62
|
+
[WATCH_STATE]: WatchStateFrame;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A realtime client, as much of one as this adapter needs to see.
|
|
66
|
+
*
|
|
67
|
+
* Four members and no types on them, because the point of this shape is that a
|
|
68
|
+
* *typed* client satisfies it: a bound realtime client's `on` is generic over
|
|
69
|
+
* the events of the contract it was bound to, and TypeScript will not relate
|
|
70
|
+
* that signature to any concrete one written here — measured, both with a payload
|
|
71
|
+
* parameter and with the tuple form the realtime handler actually has.
|
|
72
|
+
*/
|
|
73
|
+
export interface RealtimeClientLike {
|
|
74
|
+
on: (...args: never[]) => unknown;
|
|
75
|
+
emit: (...args: never[]) => unknown;
|
|
76
|
+
request: (...args: never[]) => unknown;
|
|
77
|
+
onConnectionChange: (...args: never[]) => unknown;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Hand a bound realtime client to {@link createWatchClient}.
|
|
81
|
+
*
|
|
82
|
+
* The guide used to say "pass a bound realtime client" and the types refused it,
|
|
83
|
+
* which is the worst combination: an instruction that reads as supported and
|
|
84
|
+
* fails at the call site. This is the conversion, once, in the framework —
|
|
85
|
+
* rather than the same cast copied into every application that follows the
|
|
86
|
+
* guide.
|
|
87
|
+
*/
|
|
88
|
+
export declare function watchTransport(client: RealtimeClientLike): WatchTransport;
|
|
89
|
+
/**
|
|
90
|
+
* The transport half a watch client needs — a realtime client bound to a
|
|
91
|
+
* contract that carries `watchContract`.
|
|
92
|
+
*
|
|
93
|
+
* Written in the protocol's own four event names rather than in `string`, so an
|
|
94
|
+
* application bringing its own transport knows exactly what to implement.
|
|
95
|
+
*
|
|
96
|
+
* A **bound realtime client does not satisfy this shape**, and no phrasing of it
|
|
97
|
+
* would fix that: its `on` is generic over the events of the contract it was
|
|
98
|
+
* bound to, and TypeScript will not relate two generic signatures like these.
|
|
99
|
+
* Pass it through {@link watchTransport}, which is that conversion done once
|
|
100
|
+
* here instead of once per application.
|
|
101
|
+
*/
|
|
43
102
|
export interface WatchTransport {
|
|
44
|
-
on(event:
|
|
45
|
-
emit(event:
|
|
46
|
-
|
|
103
|
+
on<TEvent extends keyof WatchInboundEvents>(event: TEvent, handler: (payload: WatchInboundEvents[TEvent]) => void): () => void;
|
|
104
|
+
emit(event: typeof WATCH_CLOSE, payload: {
|
|
105
|
+
key: WatchKey;
|
|
106
|
+
}): unknown;
|
|
107
|
+
request(event: typeof WATCH_OPEN, payload: {
|
|
108
|
+
key: WatchKey;
|
|
109
|
+
args: unknown;
|
|
110
|
+
}, options: {
|
|
47
111
|
timeoutMs: number;
|
|
48
112
|
}): Promise<{
|
|
49
113
|
accepted: boolean;
|
|
50
114
|
reason?: string;
|
|
51
115
|
}>;
|
|
116
|
+
/**
|
|
117
|
+
* Observe connection changes. Required, because recovery depends on it.
|
|
118
|
+
*
|
|
119
|
+
* The hub releases a subscriber's keys when its connection detaches, so every
|
|
120
|
+
* question opened over the old socket is gone the moment it drops. Without
|
|
121
|
+
* this the client would go on believing it was subscribed and show a face that
|
|
122
|
+
* had quietly stopped updating.
|
|
123
|
+
*/
|
|
124
|
+
onConnectionChange(listener: (connected: boolean, reason?: string) => void): () => void;
|
|
52
125
|
}
|
|
53
126
|
export interface WatchClientConfig {
|
|
54
127
|
readonly transport: WatchTransport;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch-client.d.ts","sourceRoot":"","sources":["../../src/live/watch-client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"watch-client.d.ts","sourceRoot":"","sources":["../../src/live/watch-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEnE,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EACX,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,eAAe,EAErB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,cAAc,CAAC,MAAM;IACpC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,kEAAkE;IAClE,KAAK,CAAC,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAAC;CACtC;AAED,MAAM,WAAW,WAAW,CAAC,MAAM;IACjC,sFAAsF;IACtF,SAAS,CAAC,SAAS,EAAE,cAAc,CAAC,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC;IACzD,wEAAwE;IACxE,KAAK,IAAI,IAAI,CAAC;CACf;AAED,sDAAsD;AACtD,MAAM,WAAW,kBAAkB;IACjC,CAAC,WAAW,CAAC,EAAE,eAAe,CAAC;IAC/B,CAAC,WAAW,CAAC,EAAE,eAAe,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;IAClC,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;IACpC,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;IACvC,kBAAkB,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;CACnD;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,kBAAkB,GAAG,cAAc,CAKzE;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,MAAM,SAAS,MAAM,kBAAkB,EACxC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,CAAC,OAAO,EAAE,kBAAkB,CAAC,MAAM,CAAC,KAAK,IAAI,GACrD,MAAM,IAAI,CAAC;IACd,IAAI,CAAC,KAAK,EAAE,OAAO,WAAW,EAAE,OAAO,EAAE;QAAE,GAAG,EAAE,QAAQ,CAAA;KAAE,GAAG,OAAO,CAAC;IACrE,OAAO,CACL,KAAK,EAAE,OAAO,UAAU,EACxB,OAAO,EAAE;QAAE,GAAG,EAAE,QAAQ,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,EACzC,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAC7B,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD;;;;;;;OAOG;IACH,kBAAkB,CAAC,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACzF;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC;IACnC,+EAA+E;IAC/E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,wEAAwE;IACxE,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9D;AAuBD,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,IAAI;KACnE,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC;CACzE,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACrE,QAAQ,EAAE,WAAW,CAAC,CAAC,EAAE,MAAM,CAAC,EAChC,MAAM,EAAE,iBAAiB,GACxB,gBAAgB,CAAC,CAAC,CAAC,CAwNrB"}
|
package/dist/live.d.ts
CHANGED
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
* a watched read live in `stitchkit/application`. → ADR 0150.
|
|
14
14
|
*/
|
|
15
15
|
export { defineEvents, type EventDecision, type EventDeliveryMode, type EventPayloads, type EventsConfig, type EventsDeclaration, type EventTopicDeclaration, type EventTopicRegistry, type EventTopicsOfMode, type EventUndecided, toRealtimeContract, type WireTopic, } from './live/events.js';
|
|
16
|
-
export { createWatchClient, type TypedWatchClient, type WatchClientConfig, type WatchHandle, type WatchListeners, type WatchTransport, } from './live/watch-client.js';
|
|
16
|
+
export { createWatchClient, type RealtimeClientLike, type TypedWatchClient, type WatchClientConfig, type WatchHandle, type WatchInboundEvents, type WatchListeners, type WatchTransport, watchTransport, } from './live/watch-client.js';
|
|
17
17
|
export { WATCH_CLOSE, WATCH_OPEN, WATCH_STATE, WATCH_VALUE, type WatchKey, WatchKeySchema, type WatchStateFrame, WatchStateSchema, type WatchValueFrame, WatchValueSchema, watchContract, watchKeyString, } from './live/watch-contract.js';
|
|
18
18
|
//# sourceMappingURL=live.d.ts.map
|
package/dist/live.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,kBAAkB,EAClB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,KAAK,cAAc,
|
|
1
|
+
{"version":3,"file":"live.d.ts","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EACL,YAAY,EACZ,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,kBAAkB,EAClB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,iBAAiB,EACjB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,cAAc,EACnB,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,WAAW,EACX,UAAU,EACV,WAAW,EACX,WAAW,EACX,KAAK,QAAQ,EACb,cAAc,EACd,KAAK,eAAe,EACpB,gBAAgB,EAChB,KAAK,eAAe,EACpB,gBAAgB,EAChB,aAAa,EACb,cAAc,GACf,MAAM,uBAAuB,CAAC"}
|
package/dist/live.js
CHANGED
|
@@ -116,12 +116,20 @@ function watchKeyString(key) {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// src/live/watch-client.ts
|
|
119
|
+
function watchTransport(client) {
|
|
120
|
+
return client;
|
|
121
|
+
}
|
|
119
122
|
function createWatchClient(contract, config) {
|
|
120
123
|
const entries = new Map;
|
|
121
124
|
const digests = new Map;
|
|
122
125
|
const holdMs = config.holdMs ?? 0;
|
|
123
126
|
const openTimeoutMs = config.openTimeoutMs ?? 1e4;
|
|
124
127
|
const service = contract.meta.prefix;
|
|
128
|
+
function publishState(entry, state) {
|
|
129
|
+
entry.state = state;
|
|
130
|
+
for (const listener of [...entry.listeners])
|
|
131
|
+
listener.state?.(state);
|
|
132
|
+
}
|
|
125
133
|
config.transport.on(WATCH_VALUE, (frame) => {
|
|
126
134
|
const entry = entries.get(watchKeyString(frame.key));
|
|
127
135
|
if (!entry)
|
|
@@ -138,9 +146,27 @@ function createWatchClient(contract, config) {
|
|
|
138
146
|
const entry = entries.get(watchKeyString(frame.key));
|
|
139
147
|
if (!entry)
|
|
140
148
|
return;
|
|
141
|
-
entry
|
|
142
|
-
|
|
143
|
-
|
|
149
|
+
publishState(entry, frame);
|
|
150
|
+
});
|
|
151
|
+
config.transport.onConnectionChange((connected, reason) => {
|
|
152
|
+
if (!connected) {
|
|
153
|
+
for (const entry of entries.values()) {
|
|
154
|
+
entry.opened = false;
|
|
155
|
+
publishState(entry, {
|
|
156
|
+
key: entry.key,
|
|
157
|
+
phase: "unavailable",
|
|
158
|
+
reason: "source-unavailable",
|
|
159
|
+
...reason !== undefined && { message: reason }
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
for (const entry of entries.values()) {
|
|
165
|
+
if (entry.listeners.size === 0)
|
|
166
|
+
continue;
|
|
167
|
+
publishState(entry, { key: entry.key, phase: "opening" });
|
|
168
|
+
open(entry);
|
|
169
|
+
}
|
|
144
170
|
});
|
|
145
171
|
function cachedDigest(action, args) {
|
|
146
172
|
return digests.get(`${service}/${action}/${JSON.stringify(stableValue(args))}`);
|
|
@@ -154,7 +180,7 @@ function createWatchClient(contract, config) {
|
|
|
154
180
|
digests.set(cacheKey, digest);
|
|
155
181
|
return digest;
|
|
156
182
|
}
|
|
157
|
-
function entryFor(key) {
|
|
183
|
+
function entryFor(key, args) {
|
|
158
184
|
const id = watchKeyString(key);
|
|
159
185
|
const existing = entries.get(id);
|
|
160
186
|
if (existing) {
|
|
@@ -164,14 +190,44 @@ function createWatchClient(contract, config) {
|
|
|
164
190
|
}
|
|
165
191
|
const entry = {
|
|
166
192
|
key,
|
|
193
|
+
args,
|
|
167
194
|
listeners: new Set,
|
|
168
195
|
revision: 0,
|
|
169
196
|
hasValue: false,
|
|
197
|
+
opened: false,
|
|
170
198
|
state: { key, phase: "opening" }
|
|
171
199
|
};
|
|
172
200
|
entries.set(id, entry);
|
|
173
201
|
return entry;
|
|
174
202
|
}
|
|
203
|
+
async function open(entry) {
|
|
204
|
+
if (entry.opened)
|
|
205
|
+
return;
|
|
206
|
+
entry.opened = true;
|
|
207
|
+
try {
|
|
208
|
+
const acknowledgement = await config.transport.request(WATCH_OPEN, { key: entry.key, args: entry.args }, { timeoutMs: openTimeoutMs });
|
|
209
|
+
if (!acknowledgement.accepted) {
|
|
210
|
+
const reason = acknowledgement.reason ?? "the server refused this watch";
|
|
211
|
+
config.onRefused?.(entry.key, reason);
|
|
212
|
+
publishState(entry, {
|
|
213
|
+
key: entry.key,
|
|
214
|
+
phase: "unavailable",
|
|
215
|
+
reason: "source-unavailable",
|
|
216
|
+
message: reason
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
entry.opened = false;
|
|
221
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(Reflect.get(error, "code")) : undefined;
|
|
222
|
+
publishState(entry, {
|
|
223
|
+
key: entry.key,
|
|
224
|
+
phase: "unavailable",
|
|
225
|
+
reason: "source-unavailable",
|
|
226
|
+
...code !== undefined && { code },
|
|
227
|
+
message: error instanceof Error ? error.message : String(error)
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
175
231
|
function releaseEntry(entry) {
|
|
176
232
|
if (entry.listeners.size > 0)
|
|
177
233
|
return;
|
|
@@ -191,36 +247,16 @@ function createWatchClient(contract, config) {
|
|
|
191
247
|
function handleFor(action, args) {
|
|
192
248
|
const mine = new Set;
|
|
193
249
|
let entry;
|
|
194
|
-
let opened = false;
|
|
195
250
|
const known = cachedDigest(action, args);
|
|
196
251
|
if (known !== undefined)
|
|
197
|
-
entry = entryFor({ service, action, digest: known });
|
|
252
|
+
entry = entryFor({ service, action, digest: known }, args);
|
|
198
253
|
const ready = (async () => {
|
|
199
254
|
if (entry)
|
|
200
255
|
return entry;
|
|
201
256
|
const digest = await resolveDigest(action, args);
|
|
202
|
-
entry = entryFor({ service, action, digest });
|
|
257
|
+
entry = entryFor({ service, action, digest }, args);
|
|
203
258
|
return entry;
|
|
204
259
|
})();
|
|
205
|
-
async function open(target) {
|
|
206
|
-
if (opened)
|
|
207
|
-
return;
|
|
208
|
-
opened = true;
|
|
209
|
-
const acknowledgement = await config.transport.request(WATCH_OPEN, { key: target.key, args }, { timeoutMs: openTimeoutMs });
|
|
210
|
-
if (!acknowledgement.accepted) {
|
|
211
|
-
const reason = acknowledgement.reason ?? "the server refused this watch";
|
|
212
|
-
config.onRefused?.(target.key, reason);
|
|
213
|
-
const refusal = {
|
|
214
|
-
key: target.key,
|
|
215
|
-
phase: "unavailable",
|
|
216
|
-
reason: "source-unavailable",
|
|
217
|
-
message: reason
|
|
218
|
-
};
|
|
219
|
-
target.state = refusal;
|
|
220
|
-
for (const listener of [...target.listeners])
|
|
221
|
-
listener.state?.(refusal);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
260
|
return {
|
|
225
261
|
subscribe(listeners) {
|
|
226
262
|
const registered = listeners;
|
|
@@ -269,6 +305,7 @@ function createWatchClient(contract, config) {
|
|
|
269
305
|
return client;
|
|
270
306
|
}
|
|
271
307
|
export {
|
|
308
|
+
watchTransport,
|
|
272
309
|
watchKeyString,
|
|
273
310
|
watchContract,
|
|
274
311
|
toRealtimeContract,
|
package/dist/server/index.js
CHANGED
|
@@ -875,6 +875,14 @@ function createTrustFence(config) {
|
|
|
875
875
|
}
|
|
876
876
|
trusted.push(authority);
|
|
877
877
|
}
|
|
878
|
+
const trustedOrigins = [];
|
|
879
|
+
for (const entry of config.trustedOrigins ?? []) {
|
|
880
|
+
const authority = readAuthority(entry);
|
|
881
|
+
if (!authority) {
|
|
882
|
+
throw new Error(`[stitchkit] trust fence: trustedOrigins entry "${entry}" is not an authority. An entry is \`host\` or \`host:port\` — no scheme, no path, no wildcard, the same form as trustedHosts.`);
|
|
883
|
+
}
|
|
884
|
+
trustedOrigins.push(authority);
|
|
885
|
+
}
|
|
878
886
|
function refuse(refusal) {
|
|
879
887
|
config.onRefused?.(refusal);
|
|
880
888
|
config.logger?.warn?.("[stitchkit] trust fence refused a request", {
|
|
@@ -911,7 +919,9 @@ function createTrustFence(config) {
|
|
|
911
919
|
} catch {
|
|
912
920
|
originAuthority = undefined;
|
|
913
921
|
}
|
|
914
|
-
const
|
|
922
|
+
const sameAuthority = originAuthority !== undefined && originAuthority.hostname === requested.hostname && comparablePort(originAuthority.port) === comparablePort(requested.port);
|
|
923
|
+
const declared = originAuthority !== undefined && trustedOrigins.some((entry) => entry.hostname === originAuthority.hostname && (entry.port === null || entry.port === originAuthority.port));
|
|
924
|
+
const agrees = sameAuthority || declared;
|
|
915
925
|
if (!agrees) {
|
|
916
926
|
return refuse({ reason: "origin-mismatch", lane, host, origin, site });
|
|
917
927
|
}
|
|
@@ -84,6 +84,27 @@ export interface TrustFenceConfig {
|
|
|
84
84
|
* list it when you want it.
|
|
85
85
|
*/
|
|
86
86
|
readonly trustedHosts: readonly string[];
|
|
87
|
+
/**
|
|
88
|
+
* Origins allowed to address this server from a different authority.
|
|
89
|
+
*
|
|
90
|
+
* Same entry format as {@link trustedHosts} — `host` or `host:port`, compared
|
|
91
|
+
* against the `Origin`'s authority. Absent, only an `Origin` that matches the
|
|
92
|
+
* `Host` is accepted, which is the safe default and also refuses the most
|
|
93
|
+
* ordinary arrangement there is: a UI dev server on one port talking to an API
|
|
94
|
+
* on the next. That is the case this exists for.
|
|
95
|
+
*
|
|
96
|
+
* Worth knowing what the `Origin` check is and is not for, because the
|
|
97
|
+
* question comes up the moment it refuses something. It is **not** what stops
|
|
98
|
+
* DNS rebinding: that attack is same-origin by construction — the page believes
|
|
99
|
+
* it *is* the target authority — so it sends a matching `Origin` or none at
|
|
100
|
+
* all, and {@link trustedHosts} is what refuses it. What the `Origin` check
|
|
101
|
+
* stops is a plain cross-origin request from a page that never needs to read
|
|
102
|
+
* the reply: CORS governs whether a response can be *read*, never whether the
|
|
103
|
+
* request is *sent*, so a state-changing endpoint is reachable without it.
|
|
104
|
+
* That is why the answer here is a declared list rather than dropping the
|
|
105
|
+
* check.
|
|
106
|
+
*/
|
|
107
|
+
readonly trustedOrigins?: readonly string[];
|
|
87
108
|
/** Called on every refusal, before the 403 is written. */
|
|
88
109
|
readonly onRefused?: (refusal: TrustRefusal) => void;
|
|
89
110
|
/** Refusals are logged at `warn` when a logger is supplied. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trust-fence.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/trust-fence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,qCAAqC;AACrC,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE1C,MAAM,MAAM,kBAAkB;AAC5B,yEAAyE;AACvE,cAAc;AAChB,wEAAwE;GACtE,gBAAgB;AAClB,iEAAiE;GAC/D,iBAAiB;AACnB,4DAA4D;GAC1D,YAAY,CAAC;AAEjB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IACrD,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;IAClD,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;IAC7C,oFAAoF;IACpF,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;CACpE;AAyDD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAM1D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"trust-fence.d.ts","sourceRoot":"","sources":["../../../src/server/middleware/trust-fence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE/C,qCAAqC;AACrC,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE1C,MAAM,MAAM,kBAAkB;AAC5B,yEAAyE;AACvE,cAAc;AAChB,wEAAwE;GACtE,gBAAgB;AAClB,iEAAiE;GAC/D,iBAAiB;AACnB,4DAA4D;GAC1D,YAAY,CAAC;AAEjB,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,0DAA0D;IAC1D,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IACrD,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;CAChC;AAED,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,CAAC;IAClD,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,qBAAqB,CAAC;IAC7C,oFAAoF;IACpF,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;CACpE;AAyDD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAM1D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,UAAU,CA6GrE"}
|
package/llms-full.txt
CHANGED
|
@@ -62,7 +62,7 @@ own, recorded as an ADR.
|
|
|
62
62
|
| `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
|
|
63
63
|
| `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
|
|
64
64
|
| `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
|
|
65
|
-
| `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the
|
|
65
|
+
| `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 21 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
|
|
66
66
|
| `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
|
|
67
67
|
| `stitchkit/agent-runtime/coding-tools` | server (Bun or Node) | evolving | bounded host-authorized direct file and shell tools; a root boundary, not an OS sandbox |
|
|
68
68
|
| `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
|
|
@@ -70,7 +70,7 @@ own, recorded as an ADR.
|
|
|
70
70
|
| `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
|
|
71
71
|
| `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
|
|
72
72
|
| `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
|
|
73
|
-
| `stitchkit/application` | server | evolving<br>_redefined in 4 of the
|
|
73
|
+
| `stitchkit/application` | server | evolving<br>_redefined in 4 of the 21 minors since 0.56.2, most recently 0.72.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
|
|
74
74
|
| `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
|
|
75
75
|
| `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
|
|
76
76
|
|
|
@@ -7617,9 +7617,17 @@ const hub = createWatchHub({
|
|
|
7617
7617
|
|
|
7618
7618
|
```ts
|
|
7619
7619
|
// browser
|
|
7620
|
-
import { createWatchClient } from 'stitchkit/live';
|
|
7621
|
-
|
|
7622
|
-
|
|
7620
|
+
import { createWatchClient, watchTransport } from 'stitchkit/live';
|
|
7621
|
+
|
|
7622
|
+
// `live` is the bound realtime client. It goes through `watchTransport`, and
|
|
7623
|
+
// that is not ceremony: its `on` is generic over the contract it was bound to,
|
|
7624
|
+
// and TypeScript will not relate that signature to the one a watch client
|
|
7625
|
+
// declares. The conversion lives in the framework so it is written once rather
|
|
7626
|
+
// than in every application.
|
|
7627
|
+
const watch = createWatchClient(notesContract, {
|
|
7628
|
+
transport: watchTransport(live),
|
|
7629
|
+
holdMs: 30_000,
|
|
7630
|
+
});
|
|
7623
7631
|
|
|
7624
7632
|
const handle = watch.list({ folder: 'inbox' });
|
|
7625
7633
|
const stop = handle.subscribe({
|
|
@@ -7636,6 +7644,19 @@ order-independent — `{a, b}` and `{b, a}` are the same question, which a plain
|
|
|
7636
7644
|
`JSON.stringify` key would get wrong exactly when two components happen to build
|
|
7637
7645
|
their argument object in a different order.
|
|
7638
7646
|
|
|
7647
|
+
### Narrow the topic to the arguments
|
|
7648
|
+
|
|
7649
|
+
`invalidatedBy` is given the arguments as well as the operation, so a topic can
|
|
7650
|
+
name what the answer actually depends on:
|
|
7651
|
+
|
|
7652
|
+
```ts
|
|
7653
|
+
invalidatedBy: (operation, args) => [`chat.transcript:${args.address}`],
|
|
7654
|
+
```
|
|
7655
|
+
|
|
7656
|
+
Without that, one address changing wakes every watcher of the operation. Twenty
|
|
7657
|
+
conversations open means twenty reads for one change — nineteen of them publish
|
|
7658
|
+
nothing, because nothing changed, and the read is paid anyway.
|
|
7659
|
+
|
|
7639
7660
|
### Who shares with whom
|
|
7640
7661
|
|
|
7641
7662
|
A key is `(service, action, digest of arguments)`, and everyone on that key gets
|
|
@@ -7661,6 +7682,20 @@ A failed read arrives as `unavailable` with the read's own `code` and `message`.
|
|
|
7661
7682
|
The hub retries on its own backoff, and a success clears the state without anyone
|
|
7662
7683
|
asking.
|
|
7663
7684
|
|
|
7685
|
+
### Across a reconnect
|
|
7686
|
+
|
|
7687
|
+
The hub releases a connection's keys when it detaches, so everything opened over
|
|
7688
|
+
the old socket is gone the moment it drops. The client recovers on its own: on a
|
|
7689
|
+
drop your `state` listener gets `unavailable` / `source-unavailable`, and on the
|
|
7690
|
+
next connection every key that still has a listener is re-opened and the values
|
|
7691
|
+
resume. That is what `onConnectionChange` on the transport is for, and why it is
|
|
7692
|
+
required rather than optional — a client that cannot be told has no way to
|
|
7693
|
+
notice, and the face stops updating without a word.
|
|
7694
|
+
|
|
7695
|
+
Nothing an open can fail with escapes as a rejected promise: a disconnected
|
|
7696
|
+
socket, a timeout, a refusal all arrive as `unavailable` carrying the error's own
|
|
7697
|
+
code and message, and the next connection retries.
|
|
7698
|
+
|
|
7664
7699
|
### `watch` or `createLiveStateController`?
|
|
7665
7700
|
|
|
7666
7701
|
> **If you would have written `applyEvent` as `(_, next) => next`, you want
|
|
@@ -7710,8 +7745,23 @@ point:
|
|
|
7710
7745
|
immediately. An event emitted before the memory update is a wake-up to the old
|
|
7711
7746
|
value.
|
|
7712
7747
|
|
|
7713
|
-
|
|
7714
|
-
|
|
7748
|
+
Where there is no kernel — a server that binds its own signals and closes what
|
|
7749
|
+
it holds in an order it wrote — open it directly instead:
|
|
7750
|
+
|
|
7751
|
+
```ts
|
|
7752
|
+
const opened = await openKeyspace(sessions, { backend });
|
|
7753
|
+
opened.keyspace.get(id); // synchronous, from memory
|
|
7754
|
+
// on shutdown, in the order you chose:
|
|
7755
|
+
opened.stopAdmission();
|
|
7756
|
+
await opened.drain();
|
|
7757
|
+
await opened.close();
|
|
7758
|
+
```
|
|
7759
|
+
|
|
7760
|
+
The resource is a thin wrapper over exactly that, so there is one implementation
|
|
7761
|
+
and two lifecycles, not two keyspaces.
|
|
7762
|
+
|
|
7763
|
+
Where there **is** a kernel it is a **resource**, read with `context.use(...)`,
|
|
7764
|
+
rather than a function you call wherever you need it. The kernel resolves its resource graph in the constructor
|
|
7715
7765
|
and cannot register one afterwards, so a keyspace opened by a bare call inside
|
|
7716
7766
|
another resource's `start` is never drained, never closed, and never ordered
|
|
7717
7767
|
against the things that write to it.
|
|
@@ -7745,6 +7795,32 @@ createServer({
|
|
|
7745
7795
|
});
|
|
7746
7796
|
```
|
|
7747
7797
|
|
|
7798
|
+
### A UI on one port, an API on the next
|
|
7799
|
+
|
|
7800
|
+
The most ordinary arrangement in development is also the one the fence refuses by
|
|
7801
|
+
default: a UI dev server on `:5180` calling an API on `:5181`. The browser sends
|
|
7802
|
+
`Origin: http://localhost:5180` to `Host: localhost:5181`, they differ, and both
|
|
7803
|
+
lanes answer 403. Declare the second origin:
|
|
7804
|
+
|
|
7805
|
+
```ts
|
|
7806
|
+
createTrustFence({
|
|
7807
|
+
trustedHosts: ['localhost:5181'],
|
|
7808
|
+
trustedOrigins: ['localhost:5180'],
|
|
7809
|
+
});
|
|
7810
|
+
```
|
|
7811
|
+
|
|
7812
|
+
Same entry format as `trustedHosts`, compared against the `Origin`'s authority.
|
|
7813
|
+
Declaring one does not widen anything else: an undeclared origin is still
|
|
7814
|
+
refused, and `trustedHosts` still decides which authority the server answers on.
|
|
7815
|
+
|
|
7816
|
+
It is worth knowing what the `Origin` check is for, because it is not what most
|
|
7817
|
+
people assume. It is **not** the DNS-rebinding defence — that attack is
|
|
7818
|
+
same-origin by construction, so it sends a matching `Origin` or none at all, and
|
|
7819
|
+
`trustedHosts` is what refuses it. What the `Origin` check stops is a plain
|
|
7820
|
+
cross-origin request from a page that never needs to read the reply: CORS governs
|
|
7821
|
+
whether a response can be *read*, never whether the request is *sent*, so a
|
|
7822
|
+
state-changing endpoint is reachable without it.
|
|
7823
|
+
|
|
7748
7824
|
### Install **both** halves
|
|
7749
7825
|
|
|
7750
7826
|
`fence.hooks` fences HTTP. `fence.allowRequest` fences the Socket.IO lane — and
|
|
@@ -9918,6 +9994,28 @@ makes one thing your job rather than the resolver's:
|
|
|
9918
9994
|
The mechanical part is identical either way. Only the *noticing* differs, and an
|
|
9919
9995
|
exact pin moves it onto you.
|
|
9920
9996
|
|
|
9997
|
+
## Released migration: 0.76.0
|
|
9998
|
+
|
|
9999
|
+
One change, and only if you hand `createWatchClient` a transport you wrote yourself.
|
|
10000
|
+
|
|
10001
|
+
```bash
|
|
10002
|
+
rg -n "createWatchClient" --glob '*.ts'
|
|
10003
|
+
```
|
|
10004
|
+
|
|
10005
|
+
If the `transport:` you pass is a bound realtime client — `bindRealtimeClient(...)` or
|
|
10006
|
+
`createRealtimeClient(...)` — there is nothing to do: it already carries
|
|
10007
|
+
`onConnectionChange`. If it is an object you assembled, add the fourth member:
|
|
10008
|
+
|
|
10009
|
+
```ts
|
|
10010
|
+
onConnectionChange(listener: (connected: boolean, reason?: string) => void): () => void
|
|
10011
|
+
```
|
|
10012
|
+
|
|
10013
|
+
It is required because the client recovers through it: on a drop it publishes
|
|
10014
|
+
`unavailable` to subscribers and forgets what was opened, and on a fresh
|
|
10015
|
+
connection it re-opens every key that still has a listener. Without it a watch
|
|
10016
|
+
stays "open" on the client after a server restart while the hub remembers
|
|
10017
|
+
nothing, and the face stops updating without saying so.
|
|
10018
|
+
|
|
9921
10019
|
## Released migration: 0.75.0
|
|
9922
10020
|
|
|
9923
10021
|
Two mechanical renames and one option that has to move. Nothing about a passing
|
|
@@ -13050,6 +13148,7 @@ realtime contract from `stitchkit`, and the server halves live in `stitchkit/app
|
|
|
13050
13148
|
| `EventPayloads` / `EventTopicsOfMode` / `WireTopic` | _types_ | payload map keyed by wire topic, the topics of one mode, and the `prefix.name` a topic is addressed by |
|
|
13051
13149
|
| `createWatchClient` | function | contract-shaped watch client: `watch.action(args)` returns a ref-counted handle sharing one subscription |
|
|
13052
13150
|
| `WatchHandle` / `WatchListeners` / `WatchClientConfig` / `WatchTransport` / `TypedWatchClient` | _types_ | subscribe/close, the value and state listeners, hold window and open deadline, and the bound realtime client it rides |
|
|
13151
|
+
| `watchTransport` / `RealtimeClientLike` / `WatchInboundEvents` | function / _types_ | hands a bound realtime client to a watch client — two generic signatures TypeScript will not relate, converted once here |
|
|
13053
13152
|
| `watchContract` | const | the four-event realtime contract a watched read travels on |
|
|
13054
13153
|
| `WATCH_OPEN` / `WATCH_CLOSE` / `WATCH_VALUE` / `WATCH_STATE` | const | the event names of that contract |
|
|
13055
13154
|
| `WatchKey` / `WatchKeySchema` / `watchKeyString` | type / schema / function | `(service, action, arguments digest)` — the identity both ends compute the same way |
|
|
@@ -13480,6 +13579,7 @@ the function that derives it from a snapshot,
|
|
|
13480
13579
|
| Export | Kind | Summary |
|
|
13481
13580
|
|--------|------|---------|
|
|
13482
13581
|
| `defineKeyspace` / `keyspaceResource` | function | a named record set read synchronously from memory and written through one serialised chain; backend first, then memory, then the change event |
|
|
13582
|
+
| `openKeyspace` / `OpenedKeyspace` | function / type | the same keyspace opened directly, for an application that owns its own lifecycle rather than declaring resources to a kernel |
|
|
13483
13583
|
| `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle |
|
|
13484
13584
|
| `memoryKeyspaceBackend` / `sqliteKeyspaceBackend` / `SqliteKeyspaceBackendConfig` | function / type | a disposable in-process backend, and one table with a key and a JSON payload over a caller-owned database |
|
|
13485
13585
|
| `SqliteDatabase` / `SqliteStatement` / `SqliteValue` | _types_ | the minimal synchronous SQLite boundary the framework types against |
|
package/package.json
CHANGED