@fougere/core 0.3.0-alpha.0 → 0.5.0-alpha.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 (243) hide show
  1. package/README.md +1 -1
  2. package/dist/boot/Emissions.js +1 -1
  3. package/dist/boot/Emissions.js.map +1 -1
  4. package/dist/boot/HandlerFacade.js +2 -2
  5. package/dist/boot/HandlerFacade.js.map +1 -1
  6. package/dist/boot/apply.d.ts.map +1 -1
  7. package/dist/boot/apply.js +2 -12
  8. package/dist/boot/apply.js.map +1 -1
  9. package/dist/boot/binding.d.ts +1 -1
  10. package/dist/boot/binding.d.ts.map +1 -1
  11. package/dist/boot/binding.js +4 -4
  12. package/dist/boot/binding.js.map +1 -1
  13. package/dist/boot/boot.d.ts +1 -1
  14. package/dist/boot/boot.d.ts.map +1 -1
  15. package/dist/boot/bootstrap.d.ts.map +1 -1
  16. package/dist/boot/bootstrap.js +26 -17
  17. package/dist/boot/bootstrap.js.map +1 -1
  18. package/dist/boot/frame.d.ts +1 -1
  19. package/dist/boot/frame.d.ts.map +1 -1
  20. package/dist/boot/frame.js +5 -14
  21. package/dist/boot/frame.js.map +1 -1
  22. package/dist/boot/hosted.d.ts +29 -0
  23. package/dist/boot/hosted.d.ts.map +1 -0
  24. package/dist/boot/hosted.js +38 -0
  25. package/dist/boot/hosted.js.map +1 -0
  26. package/dist/boot/ownership.d.ts +0 -3
  27. package/dist/boot/ownership.d.ts.map +1 -1
  28. package/dist/boot/ownership.js +5 -5
  29. package/dist/boot/ownership.js.map +1 -1
  30. package/dist/boot/remote.d.ts +1 -1
  31. package/dist/boot/remote.d.ts.map +1 -1
  32. package/dist/boot/remote.js +3 -3
  33. package/dist/boot/remote.js.map +1 -1
  34. package/dist/boot/statement-drift.d.ts +24 -0
  35. package/dist/boot/statement-drift.d.ts.map +1 -0
  36. package/dist/boot/statement-drift.js +38 -0
  37. package/dist/boot/statement-drift.js.map +1 -0
  38. package/dist/boot/together.d.ts.map +1 -1
  39. package/dist/boot/together.js +6 -6
  40. package/dist/boot/together.js.map +1 -1
  41. package/dist/boot/types.d.ts +17 -4
  42. package/dist/boot/types.d.ts.map +1 -1
  43. package/dist/contract/CallLog.d.ts +57 -0
  44. package/dist/contract/CallLog.d.ts.map +1 -0
  45. package/dist/contract/CallLog.js +2 -0
  46. package/dist/contract/CallLog.js.map +1 -0
  47. package/dist/contract.d.ts +9 -4
  48. package/dist/contract.d.ts.map +1 -1
  49. package/dist/contract.js +12 -5
  50. package/dist/contract.js.map +1 -1
  51. package/dist/dispatch/DispatchLifecycle.d.ts +9 -1
  52. package/dist/dispatch/DispatchLifecycle.d.ts.map +1 -1
  53. package/dist/dispatch/DispatchLifecycle.js +17 -2
  54. package/dist/dispatch/DispatchLifecycle.js.map +1 -1
  55. package/dist/dispatch/InFlight.d.ts +0 -1
  56. package/dist/dispatch/InFlight.d.ts.map +1 -1
  57. package/dist/dispatch/InFlight.js +0 -3
  58. package/dist/dispatch/InFlight.js.map +1 -1
  59. package/dist/dispatch/InputValidator.d.ts.map +1 -1
  60. package/dist/dispatch/InputValidator.js +3 -2
  61. package/dist/dispatch/InputValidator.js.map +1 -1
  62. package/dist/dispatch/OutputProjector.js +2 -2
  63. package/dist/dispatch/OutputProjector.js.map +1 -1
  64. package/dist/dispatch/StorageGuard.d.ts.map +1 -1
  65. package/dist/dispatch/StorageGuard.js +2 -2
  66. package/dist/dispatch/StorageGuard.js.map +1 -1
  67. package/dist/effective-operation.d.ts.map +1 -1
  68. package/dist/effective-operation.js +8 -5
  69. package/dist/effective-operation.js.map +1 -1
  70. package/dist/emit.d.ts +2 -2
  71. package/dist/emit.d.ts.map +1 -1
  72. package/dist/emit.js +2 -2
  73. package/dist/emit.js.map +1 -1
  74. package/dist/identity.js +1 -1
  75. package/dist/identity.js.map +1 -1
  76. package/dist/imports.d.ts +1 -1
  77. package/dist/imports.d.ts.map +1 -1
  78. package/dist/imports.js.map +1 -1
  79. package/dist/index.d.ts +8 -2
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +12 -1
  82. package/dist/index.js.map +1 -1
  83. package/dist/node.d.ts +2 -0
  84. package/dist/node.d.ts.map +1 -1
  85. package/dist/node.js +2 -0
  86. package/dist/node.js.map +1 -1
  87. package/dist/orm.d.ts.map +1 -1
  88. package/dist/orm.js +4 -4
  89. package/dist/orm.js.map +1 -1
  90. package/dist/prefab/collector.js +2 -2
  91. package/dist/prefab/collector.js.map +1 -1
  92. package/dist/prefab/mirror.d.ts.map +1 -1
  93. package/dist/prefab/mirror.js +2 -2
  94. package/dist/prefab/mirror.js.map +1 -1
  95. package/dist/prefab/presenter.d.ts.map +1 -1
  96. package/dist/prefab/presenter.js +2 -2
  97. package/dist/prefab/presenter.js.map +1 -1
  98. package/dist/prefab/repository.d.ts.map +1 -1
  99. package/dist/prefab/repository.js +2 -2
  100. package/dist/prefab/repository.js.map +1 -1
  101. package/dist/scan/bundling.d.ts +14 -0
  102. package/dist/scan/bundling.d.ts.map +1 -0
  103. package/dist/scan/bundling.js +26 -0
  104. package/dist/scan/bundling.js.map +1 -0
  105. package/dist/scan/declare.d.ts +71 -0
  106. package/dist/scan/declare.d.ts.map +1 -0
  107. package/dist/scan/declare.js +125 -0
  108. package/dist/scan/declare.js.map +1 -0
  109. package/dist/scan/emit.d.ts.map +1 -1
  110. package/dist/scan/emit.js +3 -2
  111. package/dist/scan/emit.js.map +1 -1
  112. package/dist/scan/handler-parser.d.ts +2 -0
  113. package/dist/scan/handler-parser.d.ts.map +1 -1
  114. package/dist/scan/handler-parser.js +52 -13
  115. package/dist/scan/handler-parser.js.map +1 -1
  116. package/dist/scan/scanner.d.ts.map +1 -1
  117. package/dist/scan/scanner.js +37 -11
  118. package/dist/scan/scanner.js.map +1 -1
  119. package/dist/scan/statement.d.ts +4 -0
  120. package/dist/scan/statement.d.ts.map +1 -0
  121. package/dist/scan/statement.js +112 -0
  122. package/dist/scan/statement.js.map +1 -0
  123. package/dist/verify.js +2 -2
  124. package/dist/verify.js.map +1 -1
  125. package/dist/wire/call.d.ts +19 -8
  126. package/dist/wire/call.d.ts.map +1 -1
  127. package/dist/wire/call.js +17 -6
  128. package/dist/wire/call.js.map +1 -1
  129. package/dist/wire/drift.d.ts +55 -0
  130. package/dist/wire/drift.d.ts.map +1 -0
  131. package/dist/wire/drift.js +121 -0
  132. package/dist/wire/drift.js.map +1 -0
  133. package/dist/wire/middleware.d.ts +1 -1
  134. package/dist/wire/middleware.d.ts.map +1 -1
  135. package/package.json +6 -4
  136. package/src/boot/AppLifecycle.ts +50 -0
  137. package/src/boot/Emissions.ts +263 -0
  138. package/src/boot/HandlerFacade.ts +242 -0
  139. package/src/boot/ambient-port.ts +41 -0
  140. package/src/boot/ambient.als.ts +43 -0
  141. package/src/boot/ambient.queue.ts +75 -0
  142. package/src/boot/apply.ts +47 -0
  143. package/src/boot/auth.ts +55 -0
  144. package/src/boot/binding.ts +182 -0
  145. package/src/boot/boot.ts +125 -0
  146. package/src/boot/bootstrap.ts +847 -0
  147. package/src/boot/frame.ts +210 -0
  148. package/src/boot/hosted.ts +77 -0
  149. package/src/boot/ownership.ts +137 -0
  150. package/src/boot/ports.ts +66 -0
  151. package/src/boot/remote.ts +179 -0
  152. package/src/boot/seed.ts +158 -0
  153. package/src/boot/statement-drift.ts +60 -0
  154. package/src/boot/together.ts +215 -0
  155. package/src/boot/types.ts +274 -0
  156. package/src/builtins/config.ts +19 -0
  157. package/src/builtins/logger.ts +175 -0
  158. package/src/config-loader.ts +146 -0
  159. package/src/contract/Call.ts +18 -0
  160. package/src/contract/CallLog.ts +55 -0
  161. package/src/contract/Invocation.ts +72 -0
  162. package/src/contract/RouteAddress.ts +49 -0
  163. package/src/contract.ts +83 -0
  164. package/src/crypto/encoding.ts +59 -0
  165. package/src/crypto/node.ts +30 -0
  166. package/src/crypto/port.ts +45 -0
  167. package/src/crypto/webcrypto.ts +55 -0
  168. package/src/define.ts +10 -0
  169. package/src/dispatch/ArgumentResolver.ts +17 -0
  170. package/src/dispatch/ArrayResult.ts +10 -0
  171. package/src/dispatch/DispatchEvent.ts +38 -0
  172. package/src/dispatch/DispatchLifecycle.ts +43 -0
  173. package/src/dispatch/DispatchPort.ts +6 -0
  174. package/src/dispatch/Dispatcher.ts +48 -0
  175. package/src/dispatch/InFlight.ts +45 -0
  176. package/src/dispatch/InputValidator.ts +28 -0
  177. package/src/dispatch/LocalRoutePolicy.ts +34 -0
  178. package/src/dispatch/OperationExecutor.ts +60 -0
  179. package/src/dispatch/OperationRoute.ts +18 -0
  180. package/src/dispatch/OutputProjector.ts +32 -0
  181. package/src/dispatch/OutputView.ts +9 -0
  182. package/src/dispatch/PresenterArgumentResolver.ts +25 -0
  183. package/src/dispatch/PresenterExecutor.ts +55 -0
  184. package/src/dispatch/RemoteRouteResolver.ts +21 -0
  185. package/src/dispatch/Route.ts +11 -0
  186. package/src/dispatch/RouteNotFoundError.ts +29 -0
  187. package/src/dispatch/RoutePolicy.ts +8 -0
  188. package/src/dispatch/RouteRegistry.ts +73 -0
  189. package/src/dispatch/RouteResolver.ts +7 -0
  190. package/src/dispatch/StorageGuard.ts +71 -0
  191. package/src/effective-operation.ts +601 -0
  192. package/src/emit.ts +90 -0
  193. package/src/entry/DynamicFacade.ts +28 -0
  194. package/src/entry/FacadeEntry.ts +32 -0
  195. package/src/entry/TransportEntry.ts +30 -0
  196. package/src/frond-config.ts +138 -0
  197. package/src/graph.ts +180 -0
  198. package/src/identity-keys.ts +43 -0
  199. package/src/identity.ts +235 -0
  200. package/src/imports.ts +123 -0
  201. package/src/index.ts +120 -0
  202. package/src/loader.ts +41 -0
  203. package/src/node.ts +30 -0
  204. package/src/orm.ts +252 -0
  205. package/src/prefab/collector.ts +36 -0
  206. package/src/prefab/crud.ts +188 -0
  207. package/src/prefab/mirror.ts +122 -0
  208. package/src/prefab/prefab.ts +46 -0
  209. package/src/prefab/presenter.ts +63 -0
  210. package/src/prefab/repository.ts +170 -0
  211. package/src/scan/Fronds.ts +73 -0
  212. package/src/scan/bundling.ts +25 -0
  213. package/src/scan/conventions.ts +122 -0
  214. package/src/scan/declare.ts +188 -0
  215. package/src/scan/emit.ts +231 -0
  216. package/src/scan/frond.ts +235 -0
  217. package/src/scan/handler-parser.ts +817 -0
  218. package/src/scan/scanner.ts +757 -0
  219. package/src/scan/statement.ts +126 -0
  220. package/src/verify.ts +193 -0
  221. package/src/wire/call.ts +381 -0
  222. package/src/wire/drift.ts +131 -0
  223. package/src/wire/errors.ts +124 -0
  224. package/src/wire/http-error.ts +77 -0
  225. package/src/wire/loggerMiddleware.ts +29 -0
  226. package/src/wire/middleware.ts +47 -0
  227. package/src/wire/operation.ts +258 -0
  228. package/dist/boot/Lifecycle.d.ts +0 -4
  229. package/dist/boot/Lifecycle.d.ts.map +0 -1
  230. package/dist/boot/Lifecycle.js +0 -3
  231. package/dist/boot/Lifecycle.js.map +0 -1
  232. package/dist/boot/egress.d.ts +0 -8
  233. package/dist/boot/egress.d.ts.map +0 -1
  234. package/dist/boot/egress.js +0 -11
  235. package/dist/boot/egress.js.map +0 -1
  236. package/dist/scan/scan-cache.d.ts +0 -16
  237. package/dist/scan/scan-cache.d.ts.map +0 -1
  238. package/dist/scan/scan-cache.js +0 -99
  239. package/dist/scan/scan-cache.js.map +0 -1
  240. package/dist/wire/invocation.d.ts +0 -4
  241. package/dist/wire/invocation.d.ts.map +0 -1
  242. package/dist/wire/invocation.js +0 -3
  243. package/dist/wire/invocation.js.map +0 -1
@@ -0,0 +1,55 @@
1
+ import type { DispatchEvent } from '../dispatch/DispatchEvent.js';
2
+
3
+ /**
4
+ * What crosses the door: the address, the route it took, and the verdict.
5
+ *
6
+ * It lives here rather than beside its producer for the reason `TopologyReport` does: it
7
+ * crosses a process boundary, so a reader that never installed `@fougere/calls` — the CLI
8
+ * — needs its shape, and putting it there is what produces a hand-copied duplicate.
9
+ *
10
+ * Never the body. Same rule the topology report states — a remote destination is named,
11
+ * not disclosed — and it holds here for the same reason: this answer leaves the process.
12
+ */
13
+ export interface CallRecord {
14
+ /** Monotonic, and the whole cursor protocol: a reader asks for what is above its own. */
15
+ seq: number;
16
+ frond?: string;
17
+ entity: string;
18
+ operation: string;
19
+ surface?: string;
20
+ /** Known at `resolved`, so absent on a call that never found a route. */
21
+ route?: NonNullable<DispatchEvent['routeKind']>;
22
+ /**
23
+ * The traceparent the invocation carried, when one did.
24
+ *
25
+ * One panel reads one process, so this is what lets two of them be sewn: the process
26
+ * that sent and the process that executed share it, and the gap between their two
27
+ * durations IS the cost of the wire rather than a deduction.
28
+ */
29
+ trace?: string;
30
+ /**
31
+ * The peer that established this call, when one did.
32
+ *
33
+ * A hosted frond serves several consumers, so its own ring mixes them: this is what
34
+ * separates them. Absent on a call nobody signed — a loopback dev boot has no identity.
35
+ */
36
+ caller?: string;
37
+ startedAt: number;
38
+ /** Known at `settled`. */
39
+ ms?: number;
40
+ verdict: 'running' | 'ok' | 'failed';
41
+ refusal?: { code?: string; message: string };
42
+ }
43
+
44
+ /**
45
+ * One page of the ring.
46
+ *
47
+ * `dropped` is what the ring could not keep — an absence is named rather than left to
48
+ * look like a quiet period.
49
+ */
50
+ export interface CallPage {
51
+ calls: CallRecord[];
52
+ cursor: number;
53
+ inFlight: number;
54
+ dropped: number;
55
+ }
@@ -0,0 +1,72 @@
1
+ export interface InvocationContext {
2
+ params: Record<string, unknown>;
3
+ query: Record<string, unknown>;
4
+ body: unknown;
5
+ state: Record<string, unknown>;
6
+ trace?: string;
7
+ identity?: string;
8
+ caller?: string;
9
+ }
10
+
11
+ export type InvocationInput = Partial<InvocationContext>;
12
+
13
+ function canonicalValue(value: unknown): unknown {
14
+ if (value === null || value === undefined || typeof value !== 'object') return value;
15
+ if (Array.isArray(value)) return Object.freeze(value.map(canonicalValue));
16
+
17
+ const prototype = Object.getPrototypeOf(value);
18
+ if (prototype !== Object.prototype && prototype !== null) return value;
19
+ return canonicalRecord(value);
20
+ }
21
+
22
+ function canonicalRecord(value: unknown): Record<string, unknown> {
23
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return Object.freeze({});
24
+
25
+ const result: Record<string, unknown> = {};
26
+ for (const [key, member] of Object.entries(value)) {
27
+ if (member !== undefined) result[key] = canonicalValue(member);
28
+ }
29
+ return Object.freeze(result);
30
+ }
31
+
32
+ /** Canonical invocation shared by every entry and transport. */
33
+ export class Invocation implements InvocationContext {
34
+ readonly params: Record<string, unknown>;
35
+ readonly query: Record<string, unknown>;
36
+ readonly body: unknown;
37
+ readonly state: Record<string, unknown>;
38
+ readonly trace?: string;
39
+ readonly identity?: string;
40
+ readonly caller?: string;
41
+
42
+ private constructor(input: InvocationInput) {
43
+ this.params = canonicalRecord(input.params);
44
+ this.query = canonicalRecord(input.query);
45
+ this.body = canonicalValue(input.body);
46
+ // State is host-owned and may still be enriched by existing middlewares. Moving it
47
+ // to immutable lifecycle context is a separate migration.
48
+ this.state = input.state ?? {};
49
+ if (input.trace !== undefined) this.trace = input.trace;
50
+ if (input.identity !== undefined) this.identity = input.identity;
51
+ if (input.caller !== undefined) this.caller = input.caller;
52
+ Object.freeze(this);
53
+ }
54
+
55
+ static from(input?: InvocationInput): Invocation {
56
+ return input instanceof Invocation ? input : new Invocation(input ?? {});
57
+ }
58
+
59
+ withBody(body: unknown): Invocation {
60
+ return new Invocation({ ...this, body });
61
+ }
62
+
63
+ withState(state: Record<string, unknown>): Invocation {
64
+ return new Invocation({ ...this, state });
65
+ }
66
+ }
67
+
68
+ export const EMPTY_INVOCATION = Invocation.from();
69
+
70
+ export function canonicalInvocation(input?: InvocationInput): Invocation {
71
+ return Invocation.from(input);
72
+ }
@@ -0,0 +1,49 @@
1
+ export interface RouteAddressInput {
2
+ entity: string;
3
+ operation: string;
4
+ surface?: string;
5
+ }
6
+
7
+ function required(value: string, name: string): string {
8
+ if (value.trim().length === 0) throw new TypeError(`Route ${name} cannot be empty`);
9
+ return value;
10
+ }
11
+
12
+ function optional(value: string | undefined, name: string): string | undefined {
13
+ return value === undefined ? undefined : required(value, name);
14
+ }
15
+
16
+ /** Canonical identity of one callable operation. */
17
+ export class RouteAddress {
18
+ readonly entity: string;
19
+ readonly operation: string;
20
+ readonly surface?: string;
21
+
22
+ constructor(input: RouteAddressInput) {
23
+ this.entity = required(input.entity, 'entity');
24
+ this.operation = required(input.operation, 'operation');
25
+ this.surface = optional(input.surface, 'surface');
26
+ Object.freeze(this);
27
+ }
28
+
29
+ key(): string {
30
+ return JSON.stringify([this.surface ?? null, this.entity, this.operation]);
31
+ }
32
+
33
+ equals(other: RouteAddress): boolean {
34
+ return this.key() === other.key();
35
+ }
36
+
37
+ toString(): string {
38
+ const audience = this.surface ? `${this.surface}/` : '';
39
+ return `${audience}${this.entity}.${this.operation}`;
40
+ }
41
+
42
+ toJSON(): RouteAddressInput {
43
+ return {
44
+ entity: this.entity,
45
+ operation: this.operation,
46
+ ...(this.surface !== undefined ? { surface: this.surface } : {}),
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Contract surface — what a sender needs to speak to a receiver, and
3
+ * nothing that assumes a runtime: the error vocabulary, the invocation
4
+ * shape, the call value, the naming convention.
5
+ *
6
+ * Browser-safe by design: no node builtin may enter this module's import
7
+ * graph. Published as the `@fougere/core/contract` subpath so client
8
+ * bundles never touch the full index (scanner, config loader). `@fougere/schema`
9
+ * is allowed in — measured, it imports no node builtin either.
10
+ */
11
+ import { lowerFirst } from '@fougere/schema';
12
+ import { canonicalInvocation, type InvocationContext } from './contract/Invocation.js';
13
+ import type { FrondCall } from './wire/call.js';
14
+
15
+ export { FougereError, ErrorCode, validationErrorsOf } from './wire/errors.js';
16
+ // A receiver turns a refusal into what may cross a process boundary, and it is not
17
+ // core's boot — reaching it through the main entry dragged the scanner into a bundle.
18
+ export { toPublicError } from './wire/http-error.js';
19
+ export type { FougereErrorOptions } from './wire/errors.js';
20
+ export { canonicalInvocation, EMPTY_INVOCATION } from './contract/Invocation.js';
21
+ export type { InvocationContext } from './contract/Invocation.js';
22
+ export { Invocation } from './contract/Invocation.js';
23
+ export type { InvocationInput } from './contract/Invocation.js';
24
+ export { Call } from './contract/Call.js';
25
+ export { RouteAddress } from './contract/RouteAddress.js';
26
+ export type { RouteAddressInput } from './contract/RouteAddress.js';
27
+ export type { FrondCall, Transport, SignedCall } from './wire/call.js';
28
+ export { MAX_BODY_BYTES } from './wire/call.js';
29
+ // The reserved entity, VALUE and not type: a consumer that wants to leave it alone — a
30
+ // call log ignoring its own reader — has to be able to name it.
31
+ export { RPC_ENTITY } from './wire/call.js';
32
+ export type { CallPage, CallRecord } from './contract/CallLog.js';
33
+ // The comparison of two cards, which a consumer runs about a producer — browser-safe on
34
+ // purpose: a panel showing the drift holds only the two cards, never the app.
35
+ export { driftOf, agrees, explain } from './wire/drift.js';
36
+ export type { CardDrift } from './wire/drift.js';
37
+
38
+ /**
39
+ * What `rpc.discover` answers. It belongs here and not to the runtime: the
40
+ * reserved op travels on the same wire as every other call, so a consumer that
41
+ * only sends — a browser bundle, a frond written elsewhere — needs its shape and
42
+ * nothing else. Type-only, so `call.js` never enters the runtime graph.
43
+ *
44
+ * Stated once, on purpose: two private copies of this interface have already gone
45
+ * stale (the CLI's, then the Rust demo's) the day an op stopped being a bare name.
46
+ */
47
+ export type { IdentityCard, CardOp, TopologyReport, FrondPlacement, Edge } from './wire/call.js';
48
+ export { assertIdentityCard } from './wire/call.js';
49
+
50
+ /**
51
+ * The key a class name is filed under — 'Post' → 'post'. Re-exported rather than
52
+ * respelled: a card writes it and a foreign key derives from it, so the convention
53
+ * belongs to the schema, and a second copy here is a second opinion. It travels
54
+ * through this subpath because a consumer of the wire (a browser bundle) may hold no
55
+ * schema dependency of its own — that audience only. Inside the package it is read from
56
+ * `@fougere/schema` directly: routing four modules through here bought nothing and put
57
+ * this file inside a cycle.
58
+ */
59
+ export { lowerFirst } from '@fougere/schema';
60
+
61
+ /** A call, fully fabricated: the designation and its completed invocation. */
62
+ export interface CallValue {
63
+ call: FrondCall;
64
+ invocation: InvocationContext;
65
+ }
66
+
67
+ /**
68
+ * Fabricate the call value — one designation, two spellings:
69
+ * `callValueOf(Post, 'list', { query })` (class + verb) or
70
+ * `callValueOf({ entity, op }, input)` (raw, for dynamic bridges).
71
+ * Missing invocation fields complete to the empty invocation.
72
+ */
73
+ export function callValueOf(
74
+ target: { name: string } | FrondCall,
75
+ opOrInput?: string | Partial<InvocationContext>,
76
+ input?: Partial<InvocationContext>,
77
+ ): CallValue {
78
+ const [call, given] =
79
+ typeof opOrInput === 'string'
80
+ ? [{ entity: lowerFirst((target as { name: string }).name), op: opOrInput }, input]
81
+ : [target as FrondCall, opOrInput];
82
+ return { call, invocation: canonicalInvocation(given) };
83
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * What both realizations spell identically — bytes in, bytes out, no platform.
3
+ *
4
+ * `Buffer` did all of this and does not exist on a Worker; `atob`/`btoa` exist on both.
5
+ * Nothing here is a policy decision, which is why it sits beside the port rather than
6
+ * inside either half: two copies of a base64url would be two chances to disagree on the
7
+ * padding, and the disagreement would show up as a bad signature.
8
+ */
9
+
10
+ const encoder = new TextEncoder();
11
+ const decoder = new TextDecoder();
12
+
13
+ export const bytesOf = (text: string): Uint8Array => encoder.encode(text);
14
+ export const textOf = (bytes: Uint8Array): string => decoder.decode(bytes);
15
+
16
+ /** Base64url, unpadded — what JWS puts between the dots. */
17
+ export function b64url(input: Uint8Array | string): string {
18
+ const bytes = typeof input === 'string' ? bytesOf(input) : input;
19
+ let binary = '';
20
+ // One character at a time: `String.fromCharCode(...bytes)` overflows the argument
21
+ // limit on a key, and a signature is small enough that the loop costs nothing.
22
+ for (const byte of bytes) binary += String.fromCharCode(byte);
23
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
24
+ }
25
+
26
+ export function unb64url(input: string): Uint8Array {
27
+ const binary = atob(input.replace(/-/g, '+').replace(/_/g, '/'));
28
+ const bytes = new Uint8Array(binary.length);
29
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
30
+ return bytes;
31
+ }
32
+
33
+ /** Plain base64, padded — what an environment variable carries a PEM as. */
34
+ export function b64(input: Uint8Array | string): string {
35
+ const bytes = typeof input === 'string' ? bytesOf(input) : input;
36
+ let binary = '';
37
+ for (const byte of bytes) binary += String.fromCharCode(byte);
38
+ return btoa(binary);
39
+ }
40
+
41
+ export const unb64 = (input: string): Uint8Array => {
42
+ const binary = atob(input);
43
+ const bytes = new Uint8Array(binary.length);
44
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
45
+ return bytes;
46
+ };
47
+
48
+ /**
49
+ * The DER a PEM wraps. WebCrypto imports DER and knows nothing about the armour.
50
+ *
51
+ * The label is not checked: a caller asking for a private key and handed a public one
52
+ * fails at `importKey` with a message about the key, which is the truer error than one
53
+ * about a header line.
54
+ */
55
+ export function derOf(pem: string): Uint8Array {
56
+ const body = pem.replace(/-----(BEGIN|END)[^-]+-----/g, '').replace(/\s+/g, '');
57
+ if (body.length === 0) throw new Error('Not a PEM: no base64 body between the armour');
58
+ return unb64(body);
59
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Ed25519 through `node:crypto` — the default realization, and the fast one.
3
+ *
4
+ * `sign`/`verify` are synchronous here and stay synchronous: the promise is put around
5
+ * the answer, never around the work. That is the whole reason this file exists rather
6
+ * than everyone sharing the WebCrypto half.
7
+ */
8
+ import { createHash, createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';
9
+ import type { CryptoPort, PublicJwk, Signer, Verifier } from './port.js';
10
+
11
+ export const crypto: CryptoPort = {
12
+ async sha256(data) {
13
+ return new Uint8Array(createHash('sha256').update(data).digest());
14
+ },
15
+
16
+ async signerOf(privateKeyPem): Promise<Signer> {
17
+ // Parsed here and captured, so a call signs against a KeyObject and never a string.
18
+ const key = createPrivateKey(privateKeyPem);
19
+ return { async sign(data) { return new Uint8Array(sign(null, data, key)); } };
20
+ },
21
+
22
+ async verifierOf(key): Promise<Verifier> {
23
+ const parsed = typeof key === 'string'
24
+ ? createPublicKey(key)
25
+ : createPublicKey({ key: key as never, format: 'jwk' });
26
+ return { async verify(data, signature) { return verify(null, data, parsed, signature); } };
27
+ },
28
+ };
29
+
30
+ export type { CryptoPort, PublicJwk, Signer, Verifier };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The two Ed25519 gestures a signed call needs, and the one hash that binds it.
3
+ *
4
+ * Node signs 59% faster than WebCrypto on the same curve (measured 2026-08-23: 14.30 µs
5
+ * against 22.72 µs per signature, 34.42 against 41.81 to verify), so this is a PORT with
6
+ * two realizations and not one implementation at the lowest common denominator. `#crypto`
7
+ * resolves to `crypto/node.ts` everywhere except under the `workerd` condition — the
8
+ * bundler picks at build time, and neither half ever appears in the other's output.
9
+ *
10
+ * The shape is async because WebCrypto is. On Node that costs one microtask, which the
11
+ * same measurement puts inside the noise of the signature itself (14.30 sync against
12
+ * 14.28 awaited) — awaiting is free, converting is not.
13
+ *
14
+ * A KEY IS PARSED ONCE. `crypto.subtle.importKey` is async and per-key, so an interface
15
+ * shaped `sign(key, data)` would import on every call; `signerOf(pem)` hands back
16
+ * something that holds the parsed key, and a boot builds it once.
17
+ */
18
+
19
+ /** A parsed private key. */
20
+ export interface Signer {
21
+ sign(data: Uint8Array): Promise<Uint8Array>;
22
+ }
23
+
24
+ /** A parsed public key. */
25
+ export interface Verifier {
26
+ /** False, never a throw: a bad signature is an answer, not a failure. */
27
+ verify(data: Uint8Array, signature: Uint8Array): Promise<boolean>;
28
+ }
29
+
30
+ /** A public key as a grant carries it — `{ kty: 'OKP', crv: 'Ed25519', x }`. */
31
+ export interface PublicJwk {
32
+ kty: string;
33
+ crv?: string;
34
+ x?: string;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ export interface CryptoPort {
39
+ /** SHA-256. What binds a body to an envelope without carrying the body. */
40
+ sha256(data: Uint8Array): Promise<Uint8Array>;
41
+ /** From a PKCS#8 PEM — what `fougere keys` writes. */
42
+ signerOf(privateKeyPem: string): Promise<Signer>;
43
+ /** From an SPKI PEM (the root's) or a JWK (the one a grant embeds). */
44
+ verifierOf(key: string | PublicJwk): Promise<Verifier>;
45
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Ed25519 through WebCrypto — what a runtime with no `node:` builtin has.
3
+ *
4
+ * Verified in real workerd with no compatibility flag (2026-08-23): sign, verify, a JWK
5
+ * import, an SPKI import and SHA-256 all answer. `Ed25519` is the algorithm name; the
6
+ * older `NODE-ED25519` spelling is not used, and a runtime that only knows that one will
7
+ * refuse at `importKey` rather than silently signing with something else.
8
+ *
9
+ * The four gestures are declared here rather than pulled from `lib.dom`: this package
10
+ * compiles with `lib: ["ESNext"]` and naming DOM would make every consumer carry a
11
+ * browser's globals to read a server type. What the file needs IS the declaration.
12
+ */
13
+ import { derOf } from './encoding.js';
14
+ import type { CryptoPort, PublicJwk, Signer, Verifier } from './port.js';
15
+
16
+ interface WebSubtle {
17
+ digest(algorithm: string, data: Uint8Array): Promise<ArrayBuffer>;
18
+ importKey(
19
+ format: 'pkcs8' | 'spki' | 'jwk',
20
+ key: Uint8Array | PublicJwk,
21
+ algorithm: { name: string },
22
+ extractable: boolean,
23
+ usages: string[],
24
+ ): Promise<unknown>;
25
+ sign(algorithm: { name: string }, key: unknown, data: Uint8Array): Promise<ArrayBuffer>;
26
+ verify(algorithm: { name: string }, key: unknown, signature: Uint8Array, data: Uint8Array): Promise<boolean>;
27
+ }
28
+
29
+ const ED25519 = { name: 'Ed25519' } as const;
30
+
31
+ /** Read at call time — a Worker has the global, and reading it at module load does not. */
32
+ const subtle = (): WebSubtle =>
33
+ (globalThis as unknown as { crypto: { subtle: WebSubtle } }).crypto.subtle;
34
+
35
+ export const crypto: CryptoPort = {
36
+ async sha256(data) {
37
+ return new Uint8Array(await subtle().digest('SHA-256', data));
38
+ },
39
+
40
+ async signerOf(privateKeyPem): Promise<Signer> {
41
+ // Imported once, at boot. Doing it per call would put an async key parse inside
42
+ // every outgoing request — the reason the port hands back a signer at all.
43
+ const key = await subtle().importKey('pkcs8', derOf(privateKeyPem), ED25519, false, ['sign']);
44
+ return { async sign(data) { return new Uint8Array(await subtle().sign(ED25519, key, data)); } };
45
+ },
46
+
47
+ async verifierOf(key): Promise<Verifier> {
48
+ const imported = typeof key === 'string'
49
+ ? await subtle().importKey('spki', derOf(key), ED25519, false, ['verify'])
50
+ : await subtle().importKey('jwk', key, ED25519, false, ['verify']);
51
+ return { verify: (data, signature) => subtle().verify(ED25519, imported, signature, data) };
52
+ },
53
+ };
54
+
55
+ export type { CryptoPort, PublicJwk, Signer, Verifier };
package/src/define.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { FougereConfig } from './config-loader.js';
2
+
3
+ /**
4
+ * Identity helper for fougere.config.ts — preserves literal types for autocomplete
5
+ * while validating against FougereConfig. Equivalent to `satisfies FougereConfig`
6
+ * but more explicit and aligned with defineFrond().
7
+ */
8
+ export function defineFougere<T extends FougereConfig>(config: T): T {
9
+ return config;
10
+ }
@@ -0,0 +1,17 @@
1
+ import {
2
+ resolveArgs,
3
+ type BindingPlan,
4
+ type CollectorResolver,
5
+ } from '../boot/binding.js';
6
+ import type { InvocationContext } from '../contract/Invocation.js';
7
+
8
+ export type CollectorLookup = (typeName: string) => CollectorResolver | undefined;
9
+
10
+ /** Resolves an operation's declared binding plan against one invocation. */
11
+ export class ArgumentResolver {
12
+ constructor(private readonly collectors?: CollectorLookup) {}
13
+
14
+ resolve(plan: BindingPlan, invocation: InvocationContext): Promise<unknown[]> {
15
+ return resolveArgs(plan, invocation, this.collectors);
16
+ }
17
+ }
@@ -0,0 +1,10 @@
1
+ /** Preserve the named properties carried by a list result when its rows are replaced. */
2
+ export function preserveArrayProperties<T extends unknown[]>(source: unknown[], target: T): T {
3
+ for (const key of Object.keys(source)) {
4
+ if (!/^\d+$/.test(key)) {
5
+ (target as unknown as Record<string, unknown>)[key] =
6
+ (source as unknown as Record<string, unknown>)[key];
7
+ }
8
+ }
9
+ return target;
10
+ }
@@ -0,0 +1,38 @@
1
+ import type { Call } from '../contract/Call.js';
2
+ import type { RouteKind } from './Route.js';
3
+
4
+ export type DispatchStage = 'received' | 'resolved' | 'completed' | 'failed' | 'settled';
5
+
6
+ /** Immutable transition observed across one dispatch lifecycle. */
7
+ export class DispatchEvent {
8
+ private constructor(
9
+ readonly stage: DispatchStage,
10
+ readonly call: Call,
11
+ readonly routeKind?: RouteKind,
12
+ readonly error?: unknown,
13
+ ) {
14
+ Object.freeze(this);
15
+ }
16
+
17
+ static received(call: Call): DispatchEvent {
18
+ return new DispatchEvent('received', call);
19
+ }
20
+
21
+ static resolved(call: Call, routeKind: RouteKind): DispatchEvent {
22
+ return new DispatchEvent('resolved', call, routeKind);
23
+ }
24
+
25
+ static completed(call: Call, routeKind: RouteKind): DispatchEvent {
26
+ return new DispatchEvent('completed', call, routeKind);
27
+ }
28
+
29
+ static failed(call: Call, error: unknown, routeKind?: RouteKind): DispatchEvent {
30
+ return new DispatchEvent('failed', call, routeKind, error);
31
+ }
32
+
33
+ static settled(call: Call, routeKind?: RouteKind): DispatchEvent {
34
+ return new DispatchEvent('settled', call, routeKind);
35
+ }
36
+ }
37
+
38
+ export type DispatchObserver = (event: DispatchEvent) => void;
@@ -0,0 +1,43 @@
1
+ import type { DispatchEvent, DispatchObserver } from './DispatchEvent.js';
2
+
3
+ /** Publishes dispatch transitions without participating in dispatch decisions. */
4
+ export class DispatchLifecycle {
5
+ private readonly observers: DispatchObserver[];
6
+
7
+ constructor(
8
+ observers: readonly DispatchObserver[] = [],
9
+ private readonly diagnose: (error: unknown, event: DispatchEvent) => void = () => {},
10
+ ) {
11
+ this.observers = [...observers];
12
+ }
13
+
14
+ /**
15
+ * Subscribe after the dispatcher was built, and get the unsubscription back.
16
+ *
17
+ * The list used to be settled at construction, so only the caller of `createApp` could
18
+ * observe — an extension, which runs in `up(app)`, could not. `app.use` already adds a
19
+ * middleware this late; this is its dual, and the pair is *participate* / *watch*.
20
+ */
21
+ add(observer: DispatchObserver): () => void {
22
+ this.observers.push(observer);
23
+
24
+ return () => {
25
+ const at = this.observers.indexOf(observer);
26
+ if (at !== -1) this.observers.splice(at, 1);
27
+ };
28
+ }
29
+
30
+ publish(event: DispatchEvent): void {
31
+ for (const observer of this.observers) {
32
+ try {
33
+ observer(event);
34
+ } catch (error) {
35
+ try {
36
+ this.diagnose(error, event);
37
+ } catch {
38
+ // Diagnostics remain observational too.
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,6 @@
1
+ import type { Call } from '../contract/Call.js';
2
+
3
+ /** The only capability an entry needs. */
4
+ export interface DispatchPort {
5
+ dispatch(call: Call): Promise<unknown>;
6
+ }
@@ -0,0 +1,48 @@
1
+ import type { Call } from '../contract/Call.js';
2
+ import { DispatchEvent } from './DispatchEvent.js';
3
+ import { DispatchLifecycle } from './DispatchLifecycle.js';
4
+ import type { DispatchPort } from './DispatchPort.js';
5
+ import type { Route } from './Route.js';
6
+ import type { RoutePolicy } from './RoutePolicy.js';
7
+ import { RouteRegistry } from './RouteRegistry.js';
8
+ import { RouteNotFoundError } from './RouteNotFoundError.js';
9
+ import type { InFlight } from './InFlight.js';
10
+
11
+ /** Resolves and executes every call through the same transverse lifecycle. */
12
+ export class Dispatcher implements DispatchPort {
13
+ constructor(
14
+ private readonly routes: RouteRegistry,
15
+ private readonly inFlight: InFlight,
16
+ private readonly lifecycle = new DispatchLifecycle(),
17
+ private readonly policy?: RoutePolicy,
18
+ ) {}
19
+
20
+ async dispatch(call: Call): Promise<unknown> {
21
+ let route: Route | undefined;
22
+ let release: (() => void) | undefined;
23
+ this.lifecycle.publish(DispatchEvent.received(call));
24
+
25
+ try {
26
+ release = this.inFlight.enter(call.address.entity, call.address.operation);
27
+ const known = this.routes.find(call.address);
28
+ const resolved = known ?? await this.routes.resolve(call.address);
29
+ route = resolved && (!this.policy || this.policy.accepts(resolved)) ? resolved : undefined;
30
+ if (!route) {
31
+ throw this.policy?.notFound?.(call, this.routes.routes())
32
+ ?? new RouteNotFoundError(call, this.routes.routes(), this.policy);
33
+ }
34
+
35
+ this.lifecycle.publish(DispatchEvent.resolved(call, route.kind));
36
+ const result = await route.execute(call);
37
+ this.lifecycle.publish(DispatchEvent.completed(call, route.kind));
38
+ return result;
39
+ } catch (error) {
40
+ this.lifecycle.publish(DispatchEvent.failed(call, error, route?.kind));
41
+ throw error;
42
+ } finally {
43
+ this.lifecycle.publish(DispatchEvent.settled(call, route?.kind));
44
+ release?.();
45
+ }
46
+ }
47
+
48
+ }