@volter/world-core 2.0.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.
Files changed (180) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +29 -0
  3. package/app-route.cjs +154 -0
  4. package/app-route.d.cts +7 -0
  5. package/attach.cjs +80 -0
  6. package/dist/app-route.cjs +154 -0
  7. package/dist/app-route.d.cts +7 -0
  8. package/dist/attach.cjs +80 -0
  9. package/dist/generated/pack-facts.json +4306 -0
  10. package/dist/inject.cjs +1097 -0
  11. package/dist/network-policy.cjs +92 -0
  12. package/dist/network-policy.d.cts +10 -0
  13. package/dist/src/actions.d.ts +276 -0
  14. package/dist/src/actions.js +436 -0
  15. package/dist/src/ancestry.d.ts +22 -0
  16. package/dist/src/ancestry.js +238 -0
  17. package/dist/src/args.d.ts +3 -0
  18. package/dist/src/args.js +12 -0
  19. package/dist/src/blob-store.d.ts +55 -0
  20. package/dist/src/blob-store.js +186 -0
  21. package/dist/src/brand-tokens.d.ts +2 -0
  22. package/dist/src/brand-tokens.js +17 -0
  23. package/dist/src/changeset.d.ts +431 -0
  24. package/dist/src/changeset.js +0 -0
  25. package/dist/src/client-bundle.d.ts +1 -0
  26. package/dist/src/client-bundle.js +28 -0
  27. package/dist/src/credential.d.ts +38 -0
  28. package/dist/src/credential.js +114 -0
  29. package/dist/src/derived-core.d.ts +452 -0
  30. package/dist/src/derived-core.js +782 -0
  31. package/dist/src/derived.d.ts +84 -0
  32. package/dist/src/derived.js +122 -0
  33. package/dist/src/emit.d.ts +106 -0
  34. package/dist/src/emit.js +157 -0
  35. package/dist/src/executor.d.ts +120 -0
  36. package/dist/src/executor.js +387 -0
  37. package/dist/src/file-response.d.ts +3 -0
  38. package/dist/src/file-response.js +22 -0
  39. package/dist/src/fork.d.ts +26 -0
  40. package/dist/src/fork.js +68 -0
  41. package/dist/src/git/history.d.ts +36 -0
  42. package/dist/src/git/history.js +298 -0
  43. package/dist/src/git/index.d.ts +6 -0
  44. package/dist/src/git/index.js +6 -0
  45. package/dist/src/git/inflate.d.ts +11 -0
  46. package/dist/src/git/inflate.js +194 -0
  47. package/dist/src/git/objects.d.ts +64 -0
  48. package/dist/src/git/objects.js +161 -0
  49. package/dist/src/git/pack.d.ts +14 -0
  50. package/dist/src/git/pack.js +199 -0
  51. package/dist/src/git/refs.d.ts +19 -0
  52. package/dist/src/git/refs.js +35 -0
  53. package/dist/src/git/smart-http.d.ts +45 -0
  54. package/dist/src/git/smart-http.js +223 -0
  55. package/dist/src/hash.d.ts +38 -0
  56. package/dist/src/hash.js +48 -0
  57. package/dist/src/head.d.ts +140 -0
  58. package/dist/src/head.js +313 -0
  59. package/dist/src/history.d.ts +76 -0
  60. package/dist/src/history.js +322 -0
  61. package/dist/src/index.d.ts +73 -0
  62. package/dist/src/index.js +98 -0
  63. package/dist/src/lifecycle.d.ts +1 -0
  64. package/dist/src/lifecycle.js +8 -0
  65. package/dist/src/log.d.ts +254 -0
  66. package/dist/src/log.js +801 -0
  67. package/dist/src/mirror-shell.d.ts +2 -0
  68. package/dist/src/mirror-shell.js +13 -0
  69. package/dist/src/observe.d.ts +49 -0
  70. package/dist/src/observe.js +148 -0
  71. package/dist/src/pack-assets.d.ts +30 -0
  72. package/dist/src/pack-assets.js +88 -0
  73. package/dist/src/packRegistry.d.ts +374 -0
  74. package/dist/src/packRegistry.js +142 -0
  75. package/dist/src/placeholder-remote.d.ts +22 -0
  76. package/dist/src/placeholder-remote.js +86 -0
  77. package/dist/src/proxy.d.ts +25 -0
  78. package/dist/src/proxy.js +155 -0
  79. package/dist/src/rateBudget.d.ts +367 -0
  80. package/dist/src/rateBudget.js +925 -0
  81. package/dist/src/references.d.ts +18 -0
  82. package/dist/src/references.js +27 -0
  83. package/dist/src/remote-execute.d.ts +22 -0
  84. package/dist/src/remote-execute.js +1 -0
  85. package/dist/src/resource-blob.d.ts +10 -0
  86. package/dist/src/resource-blob.js +56 -0
  87. package/dist/src/scenario.d.ts +197 -0
  88. package/dist/src/scenario.js +425 -0
  89. package/dist/src/schemas.d.ts +78 -0
  90. package/dist/src/schemas.js +50 -0
  91. package/dist/src/serve-http.d.ts +48 -0
  92. package/dist/src/serve-http.js +340 -0
  93. package/dist/src/serve.d.ts +147 -0
  94. package/dist/src/serve.js +507 -0
  95. package/dist/src/shared-blob-index.d.ts +4 -0
  96. package/dist/src/shared-blob-index.js +126 -0
  97. package/dist/src/state-system.d.ts +70 -0
  98. package/dist/src/state-system.js +90 -0
  99. package/dist/src/storage.d.ts +101 -0
  100. package/dist/src/storage.js +337 -0
  101. package/dist/src/twin-fetch.d.ts +64 -0
  102. package/dist/src/twin-fetch.js +91 -0
  103. package/dist/src/types.d.ts +40 -0
  104. package/dist/src/types.js +1 -0
  105. package/dist/src/v1-removed.d.ts +159 -0
  106. package/dist/src/v1-removed.js +124 -0
  107. package/dist/src/volter-home.d.ts +5 -0
  108. package/dist/src/volter-home.js +10 -0
  109. package/dist/src/world-clock.d.ts +4 -0
  110. package/dist/src/world-clock.js +32 -0
  111. package/dist/src/world-env.d.ts +3 -0
  112. package/dist/src/world-env.js +22 -0
  113. package/dist/src/world-store-sql.d.ts +27 -0
  114. package/dist/src/world-store-sql.js +86 -0
  115. package/dist/src/world-store.d.ts +168 -0
  116. package/dist/src/world-store.js +475 -0
  117. package/dist/src/worldConfig.d.ts +9 -0
  118. package/dist/src/worldConfig.js +17 -0
  119. package/dist/stream-bridge.cjs +80 -0
  120. package/dist/vendor-hosts.cjs +200 -0
  121. package/generated/pack-facts.json +4306 -0
  122. package/inject.cjs +1097 -0
  123. package/network-policy.cjs +92 -0
  124. package/network-policy.d.cts +10 -0
  125. package/package.json +103 -0
  126. package/src/actions.ts +564 -0
  127. package/src/ancestry.ts +213 -0
  128. package/src/args.ts +14 -0
  129. package/src/blob-store.ts +185 -0
  130. package/src/brand-tokens.ts +17 -0
  131. package/src/changeset.ts +1032 -0
  132. package/src/client-bundle.ts +29 -0
  133. package/src/credential.ts +140 -0
  134. package/src/derived-core.ts +1004 -0
  135. package/src/derived.ts +176 -0
  136. package/src/emit.ts +242 -0
  137. package/src/executor.ts +431 -0
  138. package/src/file-response.ts +22 -0
  139. package/src/fork.ts +89 -0
  140. package/src/git/history.ts +177 -0
  141. package/src/git/index.ts +6 -0
  142. package/src/git/inflate.ts +125 -0
  143. package/src/git/objects.ts +110 -0
  144. package/src/git/pack.ts +105 -0
  145. package/src/git/refs.ts +25 -0
  146. package/src/git/smart-http.ts +149 -0
  147. package/src/hash.ts +66 -0
  148. package/src/head.ts +318 -0
  149. package/src/history.ts +246 -0
  150. package/src/index.ts +323 -0
  151. package/src/lifecycle.ts +8 -0
  152. package/src/log.ts +793 -0
  153. package/src/mirror-shell.ts +15 -0
  154. package/src/observe.ts +130 -0
  155. package/src/pack-assets.ts +81 -0
  156. package/src/packRegistry.ts +408 -0
  157. package/src/placeholder-remote.ts +81 -0
  158. package/src/proxy.ts +183 -0
  159. package/src/rateBudget.ts +1115 -0
  160. package/src/references.ts +46 -0
  161. package/src/remote-execute.ts +26 -0
  162. package/src/resource-blob.ts +57 -0
  163. package/src/scenario.ts +479 -0
  164. package/src/schemas.ts +56 -0
  165. package/src/serve-http.ts +299 -0
  166. package/src/serve.ts +618 -0
  167. package/src/shared-blob-index.ts +108 -0
  168. package/src/state-system.ts +115 -0
  169. package/src/storage.ts +407 -0
  170. package/src/twin-fetch.ts +147 -0
  171. package/src/types.ts +50 -0
  172. package/src/v1-removed.ts +172 -0
  173. package/src/volter-home.ts +11 -0
  174. package/src/world-clock.ts +33 -0
  175. package/src/world-env.ts +18 -0
  176. package/src/world-store-sql.ts +118 -0
  177. package/src/world-store.ts +572 -0
  178. package/src/worldConfig.ts +27 -0
  179. package/stream-bridge.cjs +80 -0
  180. package/vendor-hosts.cjs +200 -0
@@ -0,0 +1,84 @@
1
+ /** The slice of a generated surface the dispatch reads. */
2
+ export type DerivedOperation = {
3
+ id: string;
4
+ method: string;
5
+ path: string;
6
+ class: string;
7
+ resource?: string;
8
+ /** query values that tell this operation from another at the same method and path */
9
+ discriminator?: Record<string, string | null>;
10
+ /** request headers that name the operation, for a wire whose operations share a path (AWS JSON's X-Amz-Target) */
11
+ headers?: Record<string, string | null>;
12
+ /** the status the vendor answers success with (201 for a create, 204 for an empty answer) */
13
+ successStatus?: number;
14
+ /** a form-encoded operation's number and boolean fields, by bracket path (`items.[].quantity`, `metadata.*`) */
15
+ scalars?: Readonly<Record<string, string | undefined>>;
16
+ /** what the success answer is: a resource, a list of them, and the envelope key holding it */
17
+ answers?: {
18
+ resource: string;
19
+ list: boolean;
20
+ key?: string;
21
+ };
22
+ /** the path its own server puts before it, where the operation names a server other than the surface's */
23
+ basePath?: string;
24
+ };
25
+ /** `basePath` is what the vendor's server URL puts before every operation path; `spanning` names the
26
+ * path parameters whose values may hold slashes (a git ref, a file path), which no spec says. */
27
+ export type DerivedSurface = {
28
+ version: string;
29
+ basePath?: string;
30
+ spanning?: ReadonlyArray<string>;
31
+ operations: ReadonlyArray<DerivedOperation>;
32
+ };
33
+ /** What a semantics handler receives: the request, the operation it matched, and its path parameters. */
34
+ export type DerivedCall = {
35
+ request: Request;
36
+ operation: DerivedOperation;
37
+ params: Record<string, string>;
38
+ };
39
+ export type DerivedHandler = (call: DerivedCall) => Promise<Response>;
40
+ /** What the derived core made of an operation: an answer, or why it cannot model it. */
41
+ export type DerivedCoreOutcome = {
42
+ served: Response;
43
+ } | {
44
+ unmodeled: string;
45
+ };
46
+ export type DerivedFetchOptions = {
47
+ surface: DerivedSurface;
48
+ /** Semantics handlers by operationId. A handler naming an operation the surface lacks is refused. */
49
+ handlers?: Record<string, DerivedHandler>;
50
+ /** The derived core, and which operations it owns (those of the resources the manifest declares). */
51
+ core?: {
52
+ owns: (operation: DerivedOperation) => boolean;
53
+ serve: (call: DerivedCall) => Promise<DerivedCoreOutcome>;
54
+ };
55
+ /** An RPC wire: every method is reached by any HTTP method (Slack's clients POST reads too). */
56
+ anyMethod?: boolean;
57
+ /** The pack's existing fetch while it moves: everything no handler or core owns, and what the core cannot model. */
58
+ legacy?: (request: Request) => Promise<Response>;
59
+ /** What every operation a handler or the core serves passes through (read-only refusal, version
60
+ * checks, idempotent replay); the pack's existing fetch keeps its own while it moves. */
61
+ around?: (call: DerivedCall, next: () => Promise<Response>) => Promise<Response>;
62
+ /** The vendor's answer for an operation nothing models: the pack's gap. */
63
+ gap: (request: Request, operation: DerivedOperation | undefined, reason: string) => Response;
64
+ };
65
+ type Route = {
66
+ operation: DerivedOperation;
67
+ pattern: RegExp;
68
+ names: string[];
69
+ literals: number;
70
+ };
71
+ /** Routes per method, most literal segments first, so `/v1/customers/search` wins over `/v1/customers/{customer}`. */
72
+ export declare function compileSurface(surface: DerivedSurface): Map<string, Route[]>;
73
+ export declare function matchOperation(routes: Map<string, Route[]>, method: string, pathname: string, query?: URLSearchParams, headers?: Headers): {
74
+ operation: DerivedOperation;
75
+ params: Record<string, string>;
76
+ } | undefined;
77
+ export type DerivedOwner = 'handler' | 'core' | 'legacy' | 'gap';
78
+ export type DerivedFetch = ((request: Request) => Promise<Response>) & {
79
+ /** Per operationId, who serves it: a semantics handler, the derived core, the pack's existing
80
+ * fetch while it moves, or nothing (the vendor's gap). */
81
+ owners(): Record<string, DerivedOwner>;
82
+ };
83
+ export declare function createDerivedFetch(options: DerivedFetchOptions): DerivedFetch;
84
+ export {};
@@ -0,0 +1,122 @@
1
+ // THE DERIVED DISPATCH — the generated wire's router (docs/contributing/architecture.md, "The derived
2
+ // pack"). A derived pack's surface (`src/generated/surface.gen.json`, written by scripts/derive-pack.ts)
3
+ // names every operation the vendor publishes. This matches a request to one of them and hands it to its
4
+ // owner: the pack's semantics handler for that operationId when there is one, otherwise the pack's
5
+ // existing fetch, which keeps serving everything not yet moved (and every route outside the spec, such
6
+ // as the `/twin` door). `owners` says, per operation, which of the two serves it: the count a pack's
7
+ // move is measured by. Workerd-clean: no fs, no clock, nothing at import.
8
+ class Unmodeled extends Error {
9
+ }
10
+ function compile(operation, basePath, spanning) {
11
+ const names = [];
12
+ let literals = 0;
13
+ const body = ((operation.basePath ?? basePath) + operation.path)
14
+ .split('/')
15
+ .map((seg) => {
16
+ const m = /^\{([^}]+)\}$/.exec(seg);
17
+ if (m) {
18
+ names.push(m[1]);
19
+ // a parameter the vendor lets hold slashes (a ref, a file path) takes the rest of its segments
20
+ return spanning.has(m[1]) ? '(.+?)' : '([^/]+)';
21
+ }
22
+ literals++;
23
+ // a segment templated in part (OpenAPI's `/report.{format}`, Tinybird's `/v0/pipes/{pipe}.json`) holds its
24
+ // labels between its literal text, and counts as literal: it is more specific than a bare label
25
+ return seg.split(/(\{[^}]+\})/).map((part) => {
26
+ const label = /^\{([^}]+)\}$/.exec(part);
27
+ if (!label)
28
+ return part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
29
+ names.push(label[1]);
30
+ return '([^/]+?)';
31
+ }).join('');
32
+ })
33
+ .join('/');
34
+ return { operation, pattern: new RegExp(`^${body}/?$`), names, literals };
35
+ }
36
+ const compiled = new WeakMap(); // cache: compiled routes per surface; a surface is immutable generated data
37
+ /** Routes per method, most literal segments first, so `/v1/customers/search` wins over `/v1/customers/{customer}`. */
38
+ export function compileSurface(surface) {
39
+ const hit = compiled.get(surface);
40
+ if (hit)
41
+ return hit;
42
+ const byMethod = new Map();
43
+ for (const operation of surface.operations) {
44
+ const method = operation.method.toUpperCase();
45
+ const list = byMethod.get(method) ?? [];
46
+ list.push(compile(operation, surface.basePath ?? '', new Set(surface.spanning ?? [])));
47
+ byMethod.set(method, list);
48
+ }
49
+ // most literal segments first; among equals, an operation with a query discriminator before the one without
50
+ const disc = (r) => Object.keys(r.operation.discriminator ?? {}).length + Object.keys(r.operation.headers ?? {}).length;
51
+ for (const list of byMethod.values())
52
+ list.sort((a, b) => b.literals - a.literals || disc(b) - disc(a) || a.operation.path.localeCompare(b.operation.path));
53
+ compiled.set(surface, byMethod);
54
+ return byMethod;
55
+ }
56
+ export function matchOperation(routes, method, pathname, query, headers) {
57
+ for (const route of routes.get(method.toUpperCase()) ?? []) {
58
+ const m = route.pattern.exec(pathname);
59
+ if (!m)
60
+ continue;
61
+ // a null value asks only that the key be present (S3's UploadPart, `uploadId`)
62
+ if (Object.entries(route.operation.discriminator ?? {}).some(([k, v]) => (v === null ? !query?.has(k) : query?.get(k) !== v)))
63
+ continue;
64
+ if (Object.entries(route.operation.headers ?? {}).some(([k, v]) => (v === null ? !headers?.has(k) : headers?.get(k) !== v)))
65
+ continue;
66
+ const params = {};
67
+ try {
68
+ route.names.forEach((name, i) => {
69
+ params[name] = decodeURIComponent(m[i + 1]);
70
+ });
71
+ }
72
+ catch {
73
+ // a malformed escape matches no operation: whoever serves unmatched requests answers it
74
+ return undefined;
75
+ }
76
+ return { operation: route.operation, params };
77
+ }
78
+ return undefined;
79
+ }
80
+ export function createDerivedFetch(options) {
81
+ const handlers = options.handlers ?? {};
82
+ const known = new Set(options.surface.operations.map((o) => o.id));
83
+ const unknown = Object.keys(handlers).filter((id) => !known.has(id));
84
+ if (unknown.length)
85
+ throw new Error(`derived dispatch: handlers name operations the surface does not have: ${unknown.sort().join(', ')}`);
86
+ const routes = compileSurface(options.surface);
87
+ const fallback = (request, operation, reason) => options.legacy ? options.legacy(request) : Promise.resolve(options.gap(request, operation, reason));
88
+ const fetch = (async (request) => {
89
+ const url = new URL(request.url);
90
+ const matched = options.anyMethod
91
+ ? [...routes.keys()].map((m) => matchOperation(routes, m, url.pathname, url.searchParams, request.headers)).find(Boolean)
92
+ : matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers);
93
+ if (!matched)
94
+ return fallback(request, undefined, 'no operation at this path');
95
+ const call = { request, operation: matched.operation, params: matched.params };
96
+ const around = options.around ?? ((_c, next) => next());
97
+ const handler = handlers[matched.operation.id];
98
+ if (handler)
99
+ return around(call, () => handler(call));
100
+ if (options.core?.owns(matched.operation)) {
101
+ // the core reads the body; a clone keeps it for whatever serves an operation the core cannot
102
+ // model, and that case leaves `around` by a throw, so nothing it records (an idempotent
103
+ // answer) is recorded for a request the core did not serve
104
+ try {
105
+ return await around(call, async () => {
106
+ const outcome = await options.core.serve({ ...call, request: request.clone() });
107
+ if ('served' in outcome)
108
+ return outcome.served;
109
+ throw new Unmodeled(outcome.unmodeled);
110
+ });
111
+ }
112
+ catch (error) {
113
+ if (error instanceof Unmodeled)
114
+ return fallback(request, matched.operation, error.message);
115
+ throw error;
116
+ }
117
+ }
118
+ return fallback(request, matched.operation, 'no handler');
119
+ });
120
+ fetch.owners = () => Object.fromEntries(options.surface.operations.map((o) => [o.id, handlers[o.id] ? 'handler' : options.core?.owns(o) ? 'core' : options.legacy ? 'legacy' : 'gap']));
121
+ return fetch;
122
+ }
@@ -0,0 +1,106 @@
1
+ /** One event type a pack can synthesize, and the twin resource type that supplies its payload. */
2
+ export type EmittableEvent = {
3
+ type: string;
4
+ /** twin resource type whose current state becomes the event payload (`data.object` for Stripe). */
5
+ subjectType: string;
6
+ description?: string;
7
+ };
8
+ /** A delivery destination registered IN TWIN STATE (at rest — the emit CLI runs in its own
9
+ * process, so in-memory registries don't count; endpoints must be resolvable from the root). */
10
+ export type EmitEndpoint = {
11
+ /** the vendor resource id of the registration (e.g. Stripe `we_…`), when it has one. */
12
+ id?: string;
13
+ url: string;
14
+ /** event-type subscriptions; `*` wildcards supported. Absent ⇒ subscribed to everything. */
15
+ enabledEvents?: string[];
16
+ };
17
+ /** A fully built, signed delivery for one endpoint: exact body bytes + headers to POST. */
18
+ export type SynthesizedDelivery = {
19
+ /** the exact payload string — the signature covers THESE bytes, do not re-serialize. */
20
+ payload: string;
21
+ headers: Record<string, string>;
22
+ /** the synthesized event envelope (for reporting/assertions). */
23
+ event: Record<string, unknown>;
24
+ };
25
+ /** What a pack declares to support `emit` — see `TwinPack.emitter`. All state reads take the
26
+ * same `root` every pack verb takes, so the CLI composes with worlds unchanged. */
27
+ export type TwinEmitter = {
28
+ vendor: string;
29
+ /** the emittable catalog (drives `--list` and the unknown-event error). */
30
+ events(): EmittableEvent[];
31
+ /** endpoints registered in twin state (e.g. Stripe webhook_endpoint rows). */
32
+ endpoints(root?: string): EmitEndpoint[];
33
+ /** subject ids present in state for a subject type (drives `--list` + unknown-subject errors). */
34
+ subjects(subjectType: string, root?: string): string[];
35
+ /** synthesize the signed delivery for one endpoint. MUST throw loudly on an unknown subject. */
36
+ synthesize(opts: {
37
+ type: string;
38
+ subjectId: string;
39
+ endpoint: EmitEndpoint;
40
+ root?: string;
41
+ occurredAt?: string;
42
+ }): SynthesizedDelivery;
43
+ };
44
+ export type EmitDeliveryResult = {
45
+ endpoint: EmitEndpoint;
46
+ ok: boolean;
47
+ /** HTTP status when the POST completed; absent when the request itself failed. */
48
+ status?: number;
49
+ error?: string;
50
+ event: Record<string, unknown>;
51
+ };
52
+ export type EmitReport = {
53
+ vendor: string;
54
+ type: string;
55
+ subjectId: string;
56
+ deliveries: EmitDeliveryResult[];
57
+ /** true only when EVERY delivery reached its endpoint and got a 2xx back. */
58
+ ok: boolean;
59
+ };
60
+ /** Vendor wildcard subscription match (Stripe semantics: `*` matches all;
61
+ * `invoice.*` matches every invoice event; otherwise exact). */
62
+ export declare function eventSubscriptionMatches(pattern: string, type: string): boolean;
63
+ /** The `--list` view: everything emittable right now — event types, the subject ids present in
64
+ * state for each subject type, and the registered endpoints. Pure state read. */
65
+ export declare function listEmittable(emitter: TwinEmitter, root?: string): {
66
+ vendor: string;
67
+ events: Array<EmittableEvent & {
68
+ subjects: string[];
69
+ }>;
70
+ endpoints: EmitEndpoint[];
71
+ };
72
+ /**
73
+ * DELIVER: synthesize the event for (`type`, `subjectId`) from current twin state and POST it,
74
+ * signed, to every registered endpoint subscribed to that event type (or the one endpoint
75
+ * `endpoint` names by url or id). Loud failures:
76
+ * • event type the pack can't synthesize → throws, listing the emittable types;
77
+ * • unknown subject id → the pack's synthesize throws (listing available ids);
78
+ * • no registered/subscribed endpoint → throws (register one via the vendor API first);
79
+ * • a delivery that errors or comes back non-2xx → reported per-endpoint, report.ok=false.
80
+ * `fetchFn` is the delivery seam (tests inject; default real fetch).
81
+ */
82
+ export declare function emitTwinEvent(emitter: TwinEmitter, opts: {
83
+ type: string;
84
+ subjectId: string;
85
+ root?: string;
86
+ /** restrict delivery to the endpoint with this url or vendor id. */
87
+ endpoint?: string;
88
+ occurredAt?: string;
89
+ fetchFn?: (url: string, init: {
90
+ method: string;
91
+ headers: Record<string, string>;
92
+ body: string;
93
+ }) => Promise<{
94
+ status: number;
95
+ }>;
96
+ }): Promise<EmitReport>;
97
+ /**
98
+ * Shared CLI glue so every pack's `emit` verb behaves identically:
99
+ * world-<vendor> emit --list [--root DIR] [--json]
100
+ * world-<vendor> emit <event.type> <subject-id> [--root DIR] [--endpoint URL|ID] [--at ISO] [--json]
101
+ * Returns the process exit code (0 delivered; 1 loud failure / any failed delivery; 2 usage).
102
+ */
103
+ export declare function runEmitCli(emitter: TwinEmitter, argv: string[], io?: {
104
+ out: (s: string) => void;
105
+ err: (s: string) => void;
106
+ }): Promise<number>;
@@ -0,0 +1,157 @@
1
+ // The DELIVER verb (`emit`) — fire a vendor-shaped, SIGNED webhook/event at the app under
2
+ // test, synthesized from CURRENT TWIN STATE, without hand-constructing the envelope or the
3
+ // signature. The engine here is vendor-agnostic; everything vendor-shaped (which event types
4
+ // exist, which twin resource supplies `data.object`, where registered endpoints + signing
5
+ // secrets live in state, how a delivery is signed) is a pack's knowledge, declared as a
6
+ // `TwinEmitter` and registered on its `TwinPack.emitter` (same argument as `browserRouting`:
7
+ // vendor knowledge in the descriptor, kernel stays vendor-agnostic).
8
+ //
9
+ // The operator surface is the PACK's bin (`world-stripe emit …`), exactly like the mirror
10
+ // UIs — the kernel never imports packs, so `volter-twin emit <service>` only works when a
11
+ // consumer registered the pack in-process, and otherwise fails loudly pointing at the
12
+ // vendor bin. Unlike the twin's own background emission on state changes (fire-and-forget,
13
+ // vendor-faithful), an explicit DELIVER is an operator asking for exactly this delivery, so
14
+ // every failure here is LOUD: unknown event type, unknown subject, no registered endpoint,
15
+ // and non-2xx delivery responses all throw / report instead of being swallowed.
16
+ /** Vendor wildcard subscription match (Stripe semantics: `*` matches all;
17
+ * `invoice.*` matches every invoice event; otherwise exact). */
18
+ export function eventSubscriptionMatches(pattern, type) {
19
+ if (pattern === '*')
20
+ return true;
21
+ if (pattern.endsWith('.*'))
22
+ return type.startsWith(pattern.slice(0, -1));
23
+ return pattern === type;
24
+ }
25
+ function endpointSubscribed(endpoint, type) {
26
+ if (!endpoint.enabledEvents || endpoint.enabledEvents.length === 0)
27
+ return true;
28
+ return endpoint.enabledEvents.some((p) => eventSubscriptionMatches(p, type));
29
+ }
30
+ /** The `--list` view: everything emittable right now — event types, the subject ids present in
31
+ * state for each subject type, and the registered endpoints. Pure state read. */
32
+ export function listEmittable(emitter, root) {
33
+ const subjectsByType = new Map();
34
+ const events = emitter.events().map((e) => {
35
+ let subjects = subjectsByType.get(e.subjectType);
36
+ if (!subjects) {
37
+ subjects = emitter.subjects(e.subjectType, root);
38
+ subjectsByType.set(e.subjectType, subjects);
39
+ }
40
+ return { ...e, subjects };
41
+ });
42
+ return { vendor: emitter.vendor, events, endpoints: emitter.endpoints(root) };
43
+ }
44
+ /**
45
+ * DELIVER: synthesize the event for (`type`, `subjectId`) from current twin state and POST it,
46
+ * signed, to every registered endpoint subscribed to that event type (or the one endpoint
47
+ * `endpoint` names by url or id). Loud failures:
48
+ * • event type the pack can't synthesize → throws, listing the emittable types;
49
+ * • unknown subject id → the pack's synthesize throws (listing available ids);
50
+ * • no registered/subscribed endpoint → throws (register one via the vendor API first);
51
+ * • a delivery that errors or comes back non-2xx → reported per-endpoint, report.ok=false.
52
+ * `fetchFn` is the delivery seam (tests inject; default real fetch).
53
+ */
54
+ export async function emitTwinEvent(emitter, opts) {
55
+ const catalog = emitter.events();
56
+ const known = catalog.find((e) => e.type === opts.type);
57
+ if (!known) {
58
+ throw new Error(`emit: the ${emitter.vendor} pack cannot synthesize "${opts.type}". Emittable event types:\n ${catalog.map((e) => `${e.type} (subject: ${e.subjectType})`).join('\n ')}`);
59
+ }
60
+ const all = emitter.endpoints(opts.root);
61
+ if (all.length === 0) {
62
+ throw new Error(`emit: no ${emitter.vendor} webhook endpoint is registered in twin state — nothing to deliver to. ` +
63
+ `Register one through the vendor API first (for Stripe: POST /v1/webhook_endpoints with url + enabled_events).`);
64
+ }
65
+ let targets = all.filter((e) => endpointSubscribed(e, opts.type));
66
+ if (opts.endpoint) {
67
+ targets = targets.filter((e) => e.url === opts.endpoint || e.id === opts.endpoint);
68
+ if (targets.length === 0) {
69
+ throw new Error(`emit: no registered endpoint matches "${opts.endpoint}" for event "${opts.type}". Registered endpoints:\n ${all.map((e) => `${e.id ?? '-'} ${e.url} [${(e.enabledEvents ?? ['*']).join(', ')}]`).join('\n ')}`);
70
+ }
71
+ }
72
+ if (targets.length === 0) {
73
+ throw new Error(`emit: ${all.length} endpoint(s) registered but none subscribes to "${opts.type}". Registered endpoints:\n ${all.map((e) => `${e.id ?? '-'} ${e.url} [${(e.enabledEvents ?? ['*']).join(', ')}]`).join('\n ')}`);
74
+ }
75
+ const fetchFn = opts.fetchFn ?? (async (url, init) => {
76
+ const res = await fetch(url, init);
77
+ return { status: res.status };
78
+ });
79
+ const deliveries = [];
80
+ for (const endpoint of targets) {
81
+ // synthesize PER ENDPOINT: signatures are per-endpoint secrets (real Stripe signs each
82
+ // delivery with the destination endpoint's own whsec). Unknown subject throws here — loudly.
83
+ const built = emitter.synthesize({ type: opts.type, subjectId: opts.subjectId, endpoint, ...(opts.root !== undefined ? { root: opts.root } : {}), ...(opts.occurredAt !== undefined ? { occurredAt: opts.occurredAt } : {}) });
84
+ try {
85
+ const res = await fetchFn(endpoint.url, { method: 'POST', headers: built.headers, body: built.payload });
86
+ deliveries.push({ endpoint, ok: res.status >= 200 && res.status < 300, status: res.status, event: built.event, ...(res.status >= 200 && res.status < 300 ? {} : { error: `endpoint responded ${res.status}` }) });
87
+ }
88
+ catch (error) {
89
+ deliveries.push({ endpoint, ok: false, error: error instanceof Error ? error.message : String(error), event: built.event });
90
+ }
91
+ }
92
+ return { vendor: emitter.vendor, type: opts.type, subjectId: opts.subjectId, deliveries, ok: deliveries.every((d) => d.ok) };
93
+ }
94
+ /**
95
+ * Shared CLI glue so every pack's `emit` verb behaves identically:
96
+ * world-<vendor> emit --list [--root DIR] [--json]
97
+ * world-<vendor> emit <event.type> <subject-id> [--root DIR] [--endpoint URL|ID] [--at ISO] [--json]
98
+ * Returns the process exit code (0 delivered; 1 loud failure / any failed delivery; 2 usage).
99
+ */
100
+ export async function runEmitCli(emitter, argv, io = { out: (s) => process.stdout.write(s), err: (s) => process.stderr.write(s) }) {
101
+ const flags = new Map();
102
+ const positional = [];
103
+ let list = false;
104
+ let json = false;
105
+ for (let i = 0; i < argv.length; i += 1) {
106
+ const a = argv[i];
107
+ if (a === '--list')
108
+ list = true;
109
+ else if (a === '--json')
110
+ json = true;
111
+ else if (a.startsWith('--')) {
112
+ flags.set(a, argv[i + 1] ?? '');
113
+ i += 1;
114
+ }
115
+ else
116
+ positional.push(a);
117
+ }
118
+ const root = flags.get('--root');
119
+ if (list) {
120
+ const view = listEmittable(emitter, root);
121
+ if (json) {
122
+ io.out(`${JSON.stringify(view, null, 2)}\n`);
123
+ }
124
+ else {
125
+ io.out(`emittable ${view.vendor} events (synthesized from current twin state):\n`);
126
+ for (const e of view.events) {
127
+ io.out(` ${e.type} (subject: ${e.subjectType}${e.subjects.length ? ` — ${e.subjects.join(', ')}` : ' — none in state'})\n`);
128
+ }
129
+ io.out(view.endpoints.length === 0
130
+ ? 'no webhook endpoints registered — deliveries have nowhere to go.\n'
131
+ : `endpoints:\n${view.endpoints.map((e) => ` ${e.id ?? '-'} ${e.url} [${(e.enabledEvents ?? ['*']).join(', ')}]`).join('\n')}\n`);
132
+ }
133
+ return 0;
134
+ }
135
+ const [type, subjectId] = positional;
136
+ if (!type || !subjectId) {
137
+ io.err(`Usage: world-${emitter.vendor} emit --list [--root DIR] [--json]\n world-${emitter.vendor} emit <event.type> <subject-id> [--root DIR] [--endpoint URL|ID] [--at ISO] [--json]\n`);
138
+ return 2;
139
+ }
140
+ try {
141
+ const endpointFlag = flags.get('--endpoint');
142
+ const at = flags.get('--at');
143
+ const report = await emitTwinEvent(emitter, { type, subjectId, ...(root !== undefined ? { root } : {}), ...(endpointFlag !== undefined ? { endpoint: endpointFlag } : {}), ...(at !== undefined ? { occurredAt: at } : {}) });
144
+ if (json)
145
+ io.out(`${JSON.stringify(report, null, 2)}\n`);
146
+ else {
147
+ for (const d of report.deliveries) {
148
+ io.out(`${d.ok ? 'delivered' : 'FAILED'} ${report.type} (${String(d.event.id ?? '')}) → ${d.endpoint.url}${d.status !== undefined ? ` [${d.status}]` : ''}${d.error ? ` — ${d.error}` : ''}\n`);
149
+ }
150
+ }
151
+ return report.ok ? 0 : 1;
152
+ }
153
+ catch (error) {
154
+ io.err(`${error instanceof Error ? error.message : String(error)}\n`);
155
+ return 1;
156
+ }
157
+ }
@@ -0,0 +1,120 @@
1
+ import type { CredentialPayload } from './credential.js';
2
+ import type { RemoteExecute } from './remote-execute.js';
3
+ import type { HostRule } from './packRegistry.js';
4
+ /**
5
+ * HOW A VENDOR AUTHENTICATES — a fact about the vendor, declared by the pack (`TwinPack.auth`) and
6
+ * applied here (docs/concepts/the-model.md#the-rules, rule 5: the executor "applies the credential by strategy").
7
+ *
8
+ * The executor dispatches on the STRATEGY NAME and never on vendor identity, and imports no pack:
9
+ * architecture invariant A2's own instruction is that vendor-specific knowledge lives in the
10
+ * descriptor rather than a kernel switch table. Absent means header replacement, which is what
11
+ * `credential.headers` has always done — so a pack that declares nothing keeps today's behaviour
12
+ * and no sealed credential migrates.
13
+ */
14
+ export type TwinAuthStrategy =
15
+ /** the key rides in the query string (`?key=…`, `?appid=…`) — no header will do */
16
+ {
17
+ in: 'query';
18
+ name: string;
19
+ }
20
+ /** AWS Signature Version 4 — a signature computed per request over the whole canonical request.
21
+ * `scope` answers which region and service THIS request is for, because a pack may route several
22
+ * services over one executor (aws routes six); it is pure and is handed no secret. */
23
+ | {
24
+ in: 'signature';
25
+ algorithm: 'aws-sigv4';
26
+ scope: (req: {
27
+ method: string;
28
+ path: string;
29
+ host: string;
30
+ headers: Record<string, string>;
31
+ body?: string | Uint8Array;
32
+ }) => {
33
+ region: string;
34
+ service: string;
35
+ };
36
+ }
37
+ /** a signature computed PER REQUEST over bytes only the pack can canonicalize */
38
+ | {
39
+ in: 'signature';
40
+ algorithm: 'hmac-sha256';
41
+ encoding: 'hex' | 'base64';
42
+ header: string;
43
+ /** The exact bytes to sign, from the request as the kernel will send it. Pure: it is handed no
44
+ * secret, which is what keeps the secret out of pack code while the pack still decides what
45
+ * the vendor signs.
46
+ *
47
+ * `null` means THIS REQUEST IS NOT SIGNED — a real thing vendors do (Veriff exempts
48
+ * `POST /sessions`, the call that has no session to sign for yet). It is the pack stating the
49
+ * vendor's own rule, not a failure: an unsigned request the vendor expects to be unsigned is
50
+ * correct, and the alternative would be signing bytes the vendor will reject. */
51
+ canonical: (req: {
52
+ method: string;
53
+ path: string;
54
+ body?: string;
55
+ }) => string | null;
56
+ }
57
+ /** A TOKEN EXCHANGE: the sealed secret is traded for a short-lived token the calls then carry (Bluesky's
58
+ * createSession from an app password, an OAuth refresh-token grant). The kernel sends `request` itself —
59
+ * its body fields are literals or `{ credential: '<field>' }` read from the sealed credential — keeps
60
+ * the token it answers to itself, and puts it on every later call; a `retryOn` answer (401 by default)
61
+ * exchanges once more and retries the call once. The pack never holds the secret or the token: what it
62
+ * needs to know about the session it asks the vendor for with an ordinary authenticated call. */
63
+ | {
64
+ in: 'exchange';
65
+ request: {
66
+ method: 'POST';
67
+ path: string;
68
+ format: 'json' | 'form';
69
+ body: Record<string, string | {
70
+ credential: string;
71
+ }>;
72
+ /** HTTP Basic client authentication on the token request (an OAuth confidential client), from sealed fields */
73
+ basicAuth?: {
74
+ user: {
75
+ credential: string;
76
+ };
77
+ password: {
78
+ credential: string;
79
+ };
80
+ };
81
+ };
82
+ /** the token's field in the JSON answer (a dotted path) */
83
+ token: string;
84
+ /** the header it rides in (default `authorization`) and its scheme (default `Bearer`; '' for none) */
85
+ header?: string;
86
+ scheme?: string;
87
+ retryOn?: number[];
88
+ /** the vendor's code for an expired token in the answer's `error` field (Bluesky's `ExpiredToken`): with it,
89
+ * a retryOn status exchanges again only when the answer names that code, so a bad request never spends
90
+ * the shared token or the vendor's session allowance */
91
+ retryCode?: string;
92
+ /** A vendor that ROTATES the secret each exchange spends (X issues a new refresh token with every refresh grant,
93
+ * and the old one no longer works): the answer's field holding the new one (a dotted path) and the sealed field
94
+ * it replaces. The kernel keeps the new value for its next exchange and hands the updated credential to the
95
+ * root's custody to seal (`onRotate`), so the root outlives the first refresh token. */
96
+ rotate?: {
97
+ answer: string;
98
+ credential: string;
99
+ };
100
+ };
101
+ /** Throws with the precise refusal; returns the parsed URL when the origin is usable. */
102
+ export declare function validateRemoteOrigin(origin: string, opts?: {
103
+ allowInsecureLoopback?: boolean;
104
+ }): URL;
105
+ /** `vendorHosts`: the pack's declared vendor host rules (its descriptor's `hosts`). An absolute https URL
106
+ * on one of them — an exact `host` rule whose `pathPattern` the path matches — is a second host of the
107
+ * same vendor that takes the credential (LinkedIn's image uploads on www.linkedin.com/dms-uploads,
108
+ * Instagram's rupload host); it is sent credentialed like an anchored call. Any other absolute URL is
109
+ * refused, as before. */
110
+ /** Where a credential is kept (the head's and refresh's root: head.ts rootCustody): `key` names the credential stably
111
+ * across rotations and anew when another is sealed in its place (the token cache is keyed by it, not by the secret's value), `open` reads it as sealed now, `seal` keeps a rotated one. */
112
+ export type CredentialCustody = {
113
+ key: string;
114
+ open: () => Promise<CredentialPayload>;
115
+ seal: (credential: CredentialPayload) => Promise<void> | void;
116
+ };
117
+ export type RemoteExecuteOptions = {
118
+ custody?: CredentialCustody;
119
+ };
120
+ export declare function buildRemoteExecute(origin: URL, credential: CredentialPayload, auth?: TwinAuthStrategy, vendorHosts?: readonly HostRule[], options?: RemoteExecuteOptions): RemoteExecute;