theorum 1.0.0 → 1.1.3

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 (204) hide show
  1. package/README.md +81 -25
  2. package/esm/mod.d.ts +48 -29
  3. package/esm/mod.js +37 -24
  4. package/esm/src/cli/commands/bench.js +13 -16
  5. package/esm/src/cli/commands/fuzz-canary.js +25 -29
  6. package/esm/src/cli/commands/guardrails-eval.d.ts +14 -0
  7. package/esm/src/cli/commands/guardrails-eval.js +15 -0
  8. package/esm/src/cli/commands/profile.js +12 -3
  9. package/esm/src/cli/commands/run.js +8 -9
  10. package/esm/src/cli/commands/test.js +8 -8
  11. package/esm/src/cli/event-log.d.ts +1 -1
  12. package/esm/src/cli/index.js +12 -0
  13. package/esm/src/cli/matrix/synthesizer.d.ts +6 -6
  14. package/esm/src/cli/matrix/synthesizer.js +23 -22
  15. package/esm/src/guardrails/canary-gate.d.ts +2 -1
  16. package/esm/src/guardrails/canary-gate.js +2 -1
  17. package/esm/src/guardrails/canary.d.ts +9 -1
  18. package/esm/src/guardrails/canary.js +21 -10
  19. package/esm/src/guardrails/corpus/canary-egress-attacks.d.ts +1 -0
  20. package/esm/src/guardrails/corpus/fuzz-inbound.js +12 -15
  21. package/esm/src/guardrails/corpus/inbound-payloads.js +1 -0
  22. package/esm/src/guardrails/corpus/live-attacks.js +0 -1
  23. package/esm/src/guardrails/corpus/mod.d.ts +1 -0
  24. package/esm/src/guardrails/corpus/secrets.d.ts +1 -0
  25. package/esm/src/guardrails/corpus/secrets.js +1 -0
  26. package/esm/src/guardrails/corpus/strings.d.ts +1 -0
  27. package/esm/src/guardrails/corpus/strings.js +1 -0
  28. package/esm/src/guardrails/corpus/types.d.ts +1 -0
  29. package/esm/src/guardrails/egress.d.ts +26 -3
  30. package/esm/src/guardrails/egress.js +69 -18
  31. package/esm/src/guardrails/error.d.ts +14 -23
  32. package/esm/src/guardrails/error.js +86 -77
  33. package/esm/src/guardrails/eval/corpus.d.ts +108 -0
  34. package/esm/src/guardrails/eval/corpus.js +978 -0
  35. package/esm/src/guardrails/eval/mod.d.ts +51 -0
  36. package/esm/src/guardrails/eval/mod.js +133 -0
  37. package/esm/src/guardrails/eval/score.d.ts +66 -0
  38. package/esm/src/guardrails/eval/score.js +114 -0
  39. package/esm/src/guardrails/events.d.ts +25 -0
  40. package/esm/src/guardrails/events.js +56 -0
  41. package/esm/src/guardrails/hits.d.ts +24 -0
  42. package/esm/src/guardrails/hits.js +45 -0
  43. package/esm/src/guardrails/lexicon.d.ts +39 -0
  44. package/esm/src/guardrails/lexicon.js +200 -0
  45. package/esm/src/guardrails/live-outbound-gate.d.ts +15 -13
  46. package/esm/src/guardrails/live-outbound-gate.js +131 -89
  47. package/esm/src/guardrails/mod.d.ts +19 -4
  48. package/esm/src/guardrails/mod.js +12 -3
  49. package/esm/src/guardrails/network.d.ts +19 -0
  50. package/esm/src/guardrails/network.js +234 -0
  51. package/esm/src/guardrails/policy.d.ts +35 -0
  52. package/esm/src/guardrails/policy.js +50 -0
  53. package/esm/src/guardrails/progressive-yield.d.ts +51 -0
  54. package/esm/src/guardrails/progressive-yield.js +98 -0
  55. package/esm/src/guardrails/quota.d.ts +17 -3
  56. package/esm/src/guardrails/quota.js +18 -4
  57. package/esm/src/guardrails/sanitize.d.ts +36 -6
  58. package/esm/src/guardrails/sanitize.js +152 -60
  59. package/esm/src/guardrails/serialize.d.ts +35 -0
  60. package/esm/src/guardrails/serialize.js +58 -0
  61. package/esm/src/guardrails/testing.d.ts +7 -0
  62. package/esm/src/guardrails/testing.js +4 -0
  63. package/esm/src/guardrails/theorum-error.d.ts +12 -0
  64. package/esm/src/guardrails/theorum-error.js +15 -0
  65. package/esm/src/guardrails/tool-directives.d.ts +48 -0
  66. package/esm/src/guardrails/tool-directives.js +124 -0
  67. package/esm/src/guardrails/tool-result.d.ts +93 -0
  68. package/esm/src/guardrails/tool-result.js +276 -0
  69. package/esm/src/guardrails/types.d.ts +291 -0
  70. package/esm/src/guardrails/types.js +72 -0
  71. package/esm/src/host/client-turn.js +4 -0
  72. package/esm/src/host/mint-trace.d.ts +1 -1
  73. package/esm/src/kernel/auth/crypto.d.ts +42 -0
  74. package/esm/src/kernel/auth/crypto.js +106 -0
  75. package/esm/src/kernel/auth/mod.d.ts +11 -0
  76. package/esm/src/kernel/auth/mod.js +11 -0
  77. package/esm/src/kernel/auth/oauth.d.ts +47 -0
  78. package/esm/src/kernel/auth/oauth.js +278 -0
  79. package/esm/src/kernel/auth/types.d.ts +133 -0
  80. package/esm/src/kernel/auth/types.js +13 -0
  81. package/esm/src/kernel/engine/delta.js +43 -2
  82. package/esm/src/kernel/engine/live-inbound.d.ts +15 -3
  83. package/esm/src/kernel/engine/live-inbound.js +22 -8
  84. package/esm/src/kernel/engine/live-ingress.d.ts +19 -0
  85. package/esm/src/kernel/engine/live-ingress.js +47 -0
  86. package/esm/src/kernel/engine/repair.js +13 -12
  87. package/esm/src/kernel/engine/runner/gates.js +127 -40
  88. package/esm/src/kernel/engine/runner/mod.d.ts +6 -4
  89. package/esm/src/kernel/engine/runner/mod.js +182 -43
  90. package/esm/src/kernel/engine/runner/schema-validation.js +3 -3
  91. package/esm/src/kernel/engine/runner/stages.d.ts +39 -0
  92. package/esm/src/kernel/engine/runner/stages.js +89 -0
  93. package/esm/src/kernel/engine/runner/state.d.ts +24 -0
  94. package/esm/src/kernel/engine/runner/steps.js +128 -61
  95. package/esm/src/kernel/engine/runner/stream.d.ts +9 -2
  96. package/esm/src/kernel/engine/runner/stream.js +114 -55
  97. package/esm/src/kernel/engine/session/mod.d.ts +3 -1
  98. package/esm/src/kernel/engine/session/mod.js +386 -34
  99. package/esm/src/kernel/interaction-parts.d.ts +14 -0
  100. package/esm/src/kernel/interaction-parts.js +23 -0
  101. package/esm/src/kernel/mod.d.ts +19 -7
  102. package/esm/src/kernel/mod.js +10 -5
  103. package/esm/src/kernel/profile-graph.d.ts +159 -0
  104. package/esm/src/kernel/profile-graph.js +156 -0
  105. package/esm/src/kernel/registry/attachments.d.ts +10 -10
  106. package/esm/src/kernel/registry/attachments.js +29 -26
  107. package/esm/src/kernel/registry/catalog.d.ts +25 -11
  108. package/esm/src/kernel/registry/catalog.js +59 -24
  109. package/esm/src/kernel/registry/ingress.js +34 -22
  110. package/esm/src/kernel/registry/profile-outputs.d.ts +4 -0
  111. package/esm/src/kernel/registry/profile-outputs.js +8 -0
  112. package/esm/src/kernel/registry/profiles.d.ts +34 -14
  113. package/esm/src/kernel/registry/profiles.js +306 -59
  114. package/esm/src/kernel/registry/provider-request.js +2 -0
  115. package/esm/src/kernel/registry/resolve.d.ts +7 -6
  116. package/esm/src/kernel/registry/resolve.js +102 -117
  117. package/esm/src/kernel/registry/schemas.js +1 -1
  118. package/esm/src/kernel/registry/sole-model.d.ts +8 -0
  119. package/esm/src/kernel/registry/sole-model.js +10 -0
  120. package/esm/src/kernel/registry/system-prompt.d.ts +10 -0
  121. package/esm/src/kernel/registry/system-prompt.js +40 -0
  122. package/esm/src/kernel/registry/system-role.d.ts +8 -0
  123. package/esm/src/kernel/registry/system-role.js +14 -0
  124. package/esm/src/kernel/registry/vault.d.ts +2 -2
  125. package/esm/src/kernel/registry/vault.js +4 -4
  126. package/esm/src/kernel/schema.d.ts +97 -11
  127. package/esm/src/kernel/schema.js +256 -111
  128. package/esm/src/kernel/stages.d.ts +175 -0
  129. package/esm/src/kernel/stages.js +476 -0
  130. package/esm/src/kernel/stop.d.ts +65 -11
  131. package/esm/src/kernel/stop.js +46 -11
  132. package/esm/src/kernel/tools/events.d.ts +41 -0
  133. package/esm/src/kernel/tools/events.js +71 -0
  134. package/esm/src/kernel/tools/execute.d.ts +63 -24
  135. package/esm/src/kernel/tools/execute.js +475 -224
  136. package/esm/src/kernel/tools/harness.js +16 -14
  137. package/esm/src/kernel/tools/invoke.js +26 -8
  138. package/esm/src/kernel/tools/mod.d.ts +3 -1
  139. package/esm/src/kernel/tools/mod.js +2 -1
  140. package/esm/src/kernel/tools/permission.d.ts +15 -0
  141. package/esm/src/kernel/tools/permission.js +47 -0
  142. package/esm/src/kernel/tools/project.js +5 -3
  143. package/esm/src/kernel/tools/registry.js +20 -10
  144. package/esm/src/kernel/tools/remote.d.ts +94 -0
  145. package/esm/src/kernel/tools/remote.js +577 -0
  146. package/esm/src/kernel/tools/resolve.d.ts +17 -7
  147. package/esm/src/kernel/tools/resolve.js +38 -17
  148. package/esm/src/kernel/tools/schema.d.ts +2 -0
  149. package/esm/src/kernel/tools/schema.js +23 -6
  150. package/esm/src/kernel/tools/stage-run.d.ts +105 -0
  151. package/esm/src/kernel/tools/stage-run.js +155 -0
  152. package/esm/src/kernel/tools/types.d.ts +198 -20
  153. package/esm/src/kernel/types.d.ts +253 -114
  154. package/esm/src/kernel/util/find-last.d.ts +2 -0
  155. package/esm/src/kernel/util/find-last.js +10 -0
  156. package/esm/src/observability/destinations.d.ts +31 -0
  157. package/esm/src/observability/destinations.js +67 -0
  158. package/esm/src/observability/mod.d.ts +10 -3
  159. package/esm/src/observability/mod.js +6 -2
  160. package/esm/src/observability/policy.d.ts +27 -0
  161. package/esm/src/observability/policy.js +80 -0
  162. package/esm/src/observability/resolve-policy.d.ts +16 -0
  163. package/esm/src/observability/resolve-policy.js +64 -0
  164. package/esm/src/observability/trace-attach.d.ts +3 -1
  165. package/esm/src/observability/trace-attach.js +17 -10
  166. package/esm/src/observability/trace-record.d.ts +12 -4
  167. package/esm/src/observability/trace-record.js +63 -19
  168. package/esm/src/observability/trace-sink.d.ts +19 -0
  169. package/esm/src/observability/trace-sink.js +10 -0
  170. package/esm/src/observability/trace.d.ts +14 -11
  171. package/esm/src/observability/trace.js +23 -14
  172. package/esm/src/observability/types.d.ts +113 -0
  173. package/esm/src/observability/types.js +11 -0
  174. package/esm/src/providers/create-provider.d.ts +7 -4
  175. package/esm/src/providers/create-provider.js +21 -7
  176. package/esm/src/providers/google/interactions/framing.d.ts +8 -0
  177. package/esm/src/providers/google/interactions/framing.js +74 -13
  178. package/esm/src/providers/google/live/framing.d.ts +5 -2
  179. package/esm/src/providers/google/live/framing.js +49 -12
  180. package/esm/src/providers/google/live/openapi-schema.js +3 -2
  181. package/esm/src/providers/google/live/stream.d.ts +9 -0
  182. package/esm/src/providers/google/live/stream.js +16 -3
  183. package/esm/src/providers/openrouter/cache-control.d.ts +24 -0
  184. package/esm/src/providers/openrouter/cache-control.js +23 -0
  185. package/esm/src/providers/openrouter/chat.d.ts +21 -1
  186. package/esm/src/providers/openrouter/chat.js +51 -6
  187. package/esm/src/providers/openrouter/image.js +8 -19
  188. package/esm/src/providers/openrouter/openai/chat-payload.js +23 -1
  189. package/esm/src/providers/openrouter/openai/compat.d.ts +7 -1
  190. package/esm/src/providers/openrouter/openai/compat.js +23 -4
  191. package/esm/src/providers/openrouter/openai/image-payload.js +8 -2
  192. package/esm/src/providers/openrouter/openai/sdk-messages.d.ts +1 -1
  193. package/esm/src/providers/openrouter/openai/sdk-messages.js +32 -5
  194. package/package.json +1 -1
  195. package/esm/src/interface/blocks.d.ts +0 -20
  196. package/esm/src/interface/blocks.js +0 -180
  197. package/esm/src/interface/from-profile.d.ts +0 -14
  198. package/esm/src/interface/from-profile.js +0 -104
  199. package/esm/src/interface/inputs.d.ts +0 -8
  200. package/esm/src/interface/inputs.js +0 -127
  201. package/esm/src/interface/mod.d.ts +0 -10
  202. package/esm/src/interface/mod.js +0 -9
  203. package/esm/src/interface/types.d.ts +0 -153
  204. package/esm/src/interface/types.js +0 -13
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Guardrail vocabulary — trust levels, stages, verdicts, and profile policy shape.
3
+ *
4
+ * This module is the single source of truth for guardrail types. It must not import
5
+ * from `src/kernel/`: the kernel type-imports `ProfileGuardrailsSpec` for
6
+ * `ProfileCommon.guardrails`, and that edge stays one-directional. Implementation
7
+ * modules under `src/guardrails/` may import kernel types freely.
8
+ *
9
+ * @module
10
+ */
11
+ /**
12
+ * Origin trust for text entering the model's context.
13
+ *
14
+ * - `trusted` — author-time profile text (`identity.system`). Sensitive redaction
15
+ * only; injection redaction would mangle the host's own instructions.
16
+ * - `assembled` — host-built per turn (`req.system`). Interpolates retrieval and
17
+ * user data, so it is permeable and takes full detection.
18
+ * - `untrusted` — user input, tool results, attachments, delegated agents.
19
+ */
20
+ export const TRUST_LEVELS = ['trusted', 'assembled', 'untrusted'];
21
+ /** Boundary a guardrail check runs at. */
22
+ export const GUARDRAIL_STAGES = [
23
+ 'input',
24
+ 'history',
25
+ 'system',
26
+ 'attachment',
27
+ 'tool_call',
28
+ 'tool_result',
29
+ 'output_delta',
30
+ 'output_final',
31
+ 'network',
32
+ 'live_inbound',
33
+ 'live_outbound',
34
+ 'trace',
35
+ ];
36
+ /** How serious a hit is. Does not decide what happens next — that is `onBlock`. */
37
+ export const SEVERITIES = ['info', 'low', 'medium', 'high'];
38
+ /** Egress block handling. */
39
+ export const EGRESS_ON_BLOCK = ['reject_to_agent', 'refuse_to_user'];
40
+ /**
41
+ * Where a tool result came from.
42
+ *
43
+ * `local` is host TypeScript the profile registered; `http` and `mcp` are remote
44
+ * services whose bytes the host does not control. `delegated` is another agent
45
+ * answering through the tool boundary — its output is model-generated prose that
46
+ * reads as authoritative, which is why depth is tracked separately.
47
+ */
48
+ export const TOOL_ORIGINS = ['local', 'builtin', 'http', 'mcp', 'delegated'];
49
+ /**
50
+ * What a turn may still do after it has ingested untrusted remote content.
51
+ *
52
+ * Enforcement is opt-in. Tracking and reporting are on by default — every
53
+ * remote read is observable — but refusing tool calls changes what working agents
54
+ * are allowed to do, so a host declares which access levels to gate rather than
55
+ * having the kernel guess.
56
+ */
57
+ /**
58
+ * How strongly the tool-ingress signals fired, derived from the hits themselves.
59
+ *
60
+ * Not a probability: there is no calibrated model behind it. `elevated` means one
61
+ * directive signal alongside an external destination; `high` means the content
62
+ * named a tool the model can call, or several signals agreed.
63
+ */
64
+ export const ADVISORY_LEVELS = ['none', 'elevated', 'high'];
65
+ export const TAINT_GATES = ['off', 'destructive', 'write'];
66
+ /** The guardrail field names a `host` profile may set. */
67
+ export const HOST_GUARDRAIL_FIELDS = [
68
+ 'sanitizeInput',
69
+ 'redactSensitive',
70
+ 'network',
71
+ 'taint',
72
+ ];
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @module
5
5
  */
6
+ import { projectGuardrailTurnEvent } from '../guardrails/events.js';
6
7
  function stripErrorInternal(event) {
7
8
  if (event.type !== 'error' || !event.errorInternal) {
8
9
  return event;
@@ -23,6 +24,9 @@ function forClient(event, options) {
23
24
  if (!options?.includeEvidenceRaw) {
24
25
  out = stripEvidenceRaw(out);
25
26
  }
27
+ // Clients never receive matched substrings — even if the host opted into
28
+ // guardrailMatchPreview for server logs / JSONL.
29
+ out = projectGuardrailTurnEvent(out, false);
26
30
  return out;
27
31
  }
28
32
  /** Map {@link forClient} over a batch (e.g. Live relay or HTTP stream flush). */
@@ -6,8 +6,8 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import { type TraceSink } from '../observability/trace.js';
10
9
  import type { TraceRecord } from '../observability/trace-record.js';
10
+ import type { TraceSink } from '../observability/trace-sink.js';
11
11
  interface CutoutTape {
12
12
  ok: boolean;
13
13
  ms: number;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Web Crypto utilities for OAuth 2.1 PKCE and stateless sealed state envelopes.
3
+ *
4
+ * All operations use standard `crypto.subtle` and `crypto.getRandomValues`,
5
+ * ensuring 100% portability across Node, Deno, Bun, Cloudflare Workers, and browsers.
6
+ *
7
+ * @module
8
+ */
9
+ /** Encode Uint8Array to RFC 4648 base64url string without padding. */
10
+ export declare function toBase64Url(bytes: Uint8Array): string;
11
+ /** Decode RFC 4648 base64url string to Uint8Array. */
12
+ export declare function fromBase64Url(base64url: string): Uint8Array;
13
+ /**
14
+ * Generate a cryptographically secure PKCE code verifier (RFC 7636 Section 4.1).
15
+ * Length must be between 43 and 128 characters without modulo bias.
16
+ */
17
+ export declare function generateCodeVerifier(length?: number): string;
18
+ /**
19
+ * Compute the PKCE code challenge using S256 (RFC 7636 Section 4.2):
20
+ * `BASE64URL(SHA256(ASCII(code_verifier)))`
21
+ */
22
+ export declare function computeCodeChallenge(verifier: string): Promise<string>;
23
+ export interface SealedStatePayload {
24
+ codeVerifier: string;
25
+ expectedIssuer: string;
26
+ resource?: string;
27
+ redirectUri: string;
28
+ expiresAt: number;
29
+ clientId: string;
30
+ extra?: Record<string, unknown>;
31
+ }
32
+ /**
33
+ * Create a stateless HMAC-SHA256 signed envelope for OAuth `state`.
34
+ * This allows a stateless backend to recover the code_verifier and expected issuer
35
+ * upon receiving the OAuth callback, without any database or session cache.
36
+ */
37
+ export declare function sealStatePayload(payload: SealedStatePayload, secret: string): Promise<string>;
38
+ /**
39
+ * Unpack and verify an HMAC-SHA256 signed `state` envelope.
40
+ * Validates cryptographic signature and expiration timestamp.
41
+ */
42
+ export declare function unsealStatePayload(sealed: string, secret: string): Promise<SealedStatePayload>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Web Crypto utilities for OAuth 2.1 PKCE and stateless sealed state envelopes.
3
+ *
4
+ * All operations use standard `crypto.subtle` and `crypto.getRandomValues`,
5
+ * ensuring 100% portability across Node, Deno, Bun, Cloudflare Workers, and browsers.
6
+ *
7
+ * @module
8
+ */
9
+ /** Encode Uint8Array to RFC 4648 base64url string without padding. */
10
+ export function toBase64Url(bytes) {
11
+ let binary = '';
12
+ for (let i = 0; i < bytes.byteLength; i++) {
13
+ binary += String.fromCharCode(bytes[i]);
14
+ }
15
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
16
+ }
17
+ /** Decode RFC 4648 base64url string to Uint8Array. */
18
+ export function fromBase64Url(base64url) {
19
+ let base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
20
+ while (base64.length % 4 !== 0) {
21
+ base64 += '=';
22
+ }
23
+ try {
24
+ const binary = atob(base64);
25
+ const bytes = new Uint8Array(binary.length);
26
+ for (let i = 0; i < binary.length; i++) {
27
+ bytes[i] = binary.charCodeAt(i);
28
+ }
29
+ return bytes;
30
+ }
31
+ catch (err) {
32
+ throw new Error(`Invalid base64url encoding: ${err instanceof Error ? err.message : String(err)}`);
33
+ }
34
+ }
35
+ /**
36
+ * Generate a cryptographically secure PKCE code verifier (RFC 7636 Section 4.1).
37
+ * Length must be between 43 and 128 characters without modulo bias.
38
+ */
39
+ export function generateCodeVerifier(length = 64) {
40
+ if (length < 43 || length > 128) {
41
+ throw new RangeError(`Invalid PKCE code_verifier length: ${length}. RFC 7636 Section 4.1 requires length between 43 and 128 characters.`);
42
+ }
43
+ const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
44
+ const maxValid = 256 - (256 % validChars.length); // 198 (66 * 3) eliminates modulo bias
45
+ let verifier = '';
46
+ const buffer = new Uint8Array(length * 2);
47
+ while (verifier.length < length) {
48
+ crypto.getRandomValues(buffer);
49
+ for (let i = 0; i < buffer.length && verifier.length < length; i++) {
50
+ const val = buffer[i];
51
+ if (val !== undefined && val < maxValid) {
52
+ verifier += validChars[val % validChars.length];
53
+ }
54
+ }
55
+ }
56
+ return verifier;
57
+ }
58
+ /**
59
+ * Compute the PKCE code challenge using S256 (RFC 7636 Section 4.2):
60
+ * `BASE64URL(SHA256(ASCII(code_verifier)))`
61
+ */
62
+ export async function computeCodeChallenge(verifier) {
63
+ const encoder = new TextEncoder();
64
+ const data = encoder.encode(verifier);
65
+ const digest = await crypto.subtle.digest('SHA-256', data);
66
+ return toBase64Url(new Uint8Array(digest));
67
+ }
68
+ /**
69
+ * Create a stateless HMAC-SHA256 signed envelope for OAuth `state`.
70
+ * This allows a stateless backend to recover the code_verifier and expected issuer
71
+ * upon receiving the OAuth callback, without any database or session cache.
72
+ */
73
+ export async function sealStatePayload(payload, secret) {
74
+ const encoder = new TextEncoder();
75
+ const jsonStr = JSON.stringify(payload);
76
+ const payloadBytes = encoder.encode(jsonStr);
77
+ const payloadB64 = toBase64Url(payloadBytes);
78
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
79
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(payloadB64));
80
+ const signatureB64 = toBase64Url(new Uint8Array(signature));
81
+ return `${payloadB64}.${signatureB64}`;
82
+ }
83
+ /**
84
+ * Unpack and verify an HMAC-SHA256 signed `state` envelope.
85
+ * Validates cryptographic signature and expiration timestamp.
86
+ */
87
+ export async function unsealStatePayload(sealed, secret) {
88
+ const parts = sealed.split('.');
89
+ if (parts.length !== 2) {
90
+ throw new Error('Invalid sealed state format'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
91
+ }
92
+ const [payloadB64, signatureB64] = parts;
93
+ const encoder = new TextEncoder();
94
+ const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
95
+ const signatureBytes = fromBase64Url(signatureB64);
96
+ const isValid = await crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(payloadB64));
97
+ if (!isValid) {
98
+ throw new Error('OAuth state HMAC signature verification failed: state has been tampered with or corrupted');
99
+ }
100
+ const payloadJson = new TextDecoder().decode(fromBase64Url(payloadB64));
101
+ const payload = JSON.parse(payloadJson);
102
+ if (Date.now() > payload.expiresAt) {
103
+ throw new Error('OAuth state has expired'); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
104
+ }
105
+ return payload;
106
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Authentication and authorization primitives for Theorum.
3
+ *
4
+ * Implements stateless OAuth 2.1 PKCE, RFC 9728 discovery, RFC 8414 AS metadata,
5
+ * RFC 9207 issuer validation, and RFC 8707 resource indicators.
6
+ *
7
+ * @module
8
+ */
9
+ export * from './crypto.js';
10
+ export * from './oauth.js';
11
+ export * from './types.js';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Authentication and authorization primitives for Theorum.
3
+ *
4
+ * Implements stateless OAuth 2.1 PKCE, RFC 9728 discovery, RFC 8414 AS metadata,
5
+ * RFC 9207 issuer validation, and RFC 8707 resource indicators.
6
+ *
7
+ * @module
8
+ */
9
+ export * from './crypto.js';
10
+ export * from './oauth.js';
11
+ export * from './types.js';
@@ -0,0 +1,47 @@
1
+ /**
2
+ * RFC-compliant OAuth 2.1 discovery and flow orchestration.
3
+ *
4
+ * Implements:
5
+ * - RFC 9728: OAuth 2.0 Protected Resource Metadata
6
+ * - RFC 8414 & OpenID Connect: Authorization Server Metadata
7
+ * - RFC 9207: Authorization Server Issuer Identification
8
+ * - RFC 8707: Resource Indicators
9
+ * - RFC 7636: PKCE (S256)
10
+ *
11
+ * @module
12
+ */
13
+ import type { AuthorizationServerMetadata, CreatePkceFlowOptions, ExchangePkceCodeOptions, ExchangePkceCodeResult, OAuth2Credential, OAuthTokens, PkceFlowResult, ProtectedResourceMetadata, RefreshOAuthTokenOptions } from './types.js';
14
+ /**
15
+ * Discover Protected Resource Metadata (RFC 9728 Section 3).
16
+ * Fetches `/.well-known/oauth-protected-resource`.
17
+ */
18
+ export declare function discoverResourceMetadata(resourceUrl: string, fetchFn?: typeof fetch): Promise<ProtectedResourceMetadata>;
19
+ export declare function discoverAuthServerMetadata(authServerUrl: string, fetchFn?: typeof fetch): Promise<AuthorizationServerMetadata>;
20
+ /**
21
+ * Initiate an OAuth 2.1 PKCE authorization flow statelesssly.
22
+ *
23
+ * Discovers resource & AS metadata (or uses preResolved), generates PKCE code_verifier/code_challenge,
24
+ * and packs the verifier, expected issuer, and resource into an HMAC-signed state token.
25
+ */
26
+ export declare function createOAuthPkceFlow(options: CreatePkceFlowOptions): Promise<PkceFlowResult>;
27
+ /**
28
+ * Validates the authorization response `iss` parameter per RFC 9207 Section 2.4.
29
+ *
30
+ * "The client MUST validate that the 'iss' parameter in the authorization response matches
31
+ * the issuer identifier of the authorization server... using simple string comparison (RFC 3986 Section 6.2.1)."
32
+ */
33
+ export declare function validateIssuer(expectedIssuer: string, receivedIss?: string): void;
34
+ /**
35
+ * Exchange authorization code for access and refresh tokens.
36
+ *
37
+ * Verifies state HMAC, performs RFC 9207 `iss` validation, and posts
38
+ * code_verifier and resource to the token_endpoint.
39
+ */
40
+ export declare function exchangeOAuthPkce(options: ExchangePkceCodeOptions): Promise<ExchangePkceCodeResult>;
41
+ /**
42
+ * Refresh an expired OAuth 2.1 access token.
43
+ */
44
+ export declare function refreshOAuthToken(options: RefreshOAuthTokenOptions): Promise<{
45
+ tokens: OAuthTokens;
46
+ credential: OAuth2Credential;
47
+ }>;
@@ -0,0 +1,278 @@
1
+ /**
2
+ * RFC-compliant OAuth 2.1 discovery and flow orchestration.
3
+ *
4
+ * Implements:
5
+ * - RFC 9728: OAuth 2.0 Protected Resource Metadata
6
+ * - RFC 8414 & OpenID Connect: Authorization Server Metadata
7
+ * - RFC 9207: Authorization Server Issuer Identification
8
+ * - RFC 8707: Resource Indicators
9
+ * - RFC 7636: PKCE (S256)
10
+ *
11
+ * @module
12
+ */
13
+ import { computeCodeChallenge, generateCodeVerifier, sealStatePayload, unsealStatePayload, } from './crypto.js';
14
+ /**
15
+ * Discover Protected Resource Metadata (RFC 9728 Section 3).
16
+ * Fetches `/.well-known/oauth-protected-resource`.
17
+ */
18
+ export async function discoverResourceMetadata(resourceUrl, fetchFn = fetch) {
19
+ const origin = new URL(resourceUrl).origin;
20
+ const metadataUrl = `${origin}/.well-known/oauth-protected-resource`;
21
+ const response = await fetchFn(metadataUrl, {
22
+ headers: { Accept: 'application/json' },
23
+ });
24
+ if (!response.ok) {
25
+ throw new Error(`Failed to discover protected resource metadata at ${metadataUrl} (HTTP ${response.status})`);
26
+ }
27
+ const data = (await response.json());
28
+ const authServers = Array.isArray(data.authorization_servers)
29
+ ? data.authorization_servers
30
+ : [];
31
+ if (authServers.length === 0) {
32
+ throw new Error(`Protected resource metadata at ${metadataUrl} did not declare any authorization_servers`);
33
+ }
34
+ return {
35
+ resource: typeof data.resource === 'string' ? data.resource : resourceUrl,
36
+ authorization_servers: authServers,
37
+ scopes_supported: Array.isArray(data.scopes_supported)
38
+ ? data.scopes_supported
39
+ : undefined,
40
+ bearer_methods_supported: Array.isArray(data.bearer_methods_supported)
41
+ ? data.bearer_methods_supported
42
+ : undefined,
43
+ };
44
+ }
45
+ /**
46
+ * Discover Authorization Server Metadata (RFC 8414 Section 3 & OpenID Connect Discovery 1.0).
47
+ * Queries `/.well-known/oauth-authorization-server` then `/.well-known/openid-configuration`.
48
+ */
49
+ function parseAuthServerMetadata(data, fallbackIssuer) {
50
+ if (typeof data.authorization_endpoint !== 'string' || typeof data.token_endpoint !== 'string') {
51
+ return undefined;
52
+ }
53
+ return {
54
+ issuer: typeof data.issuer === 'string' ? data.issuer : fallbackIssuer,
55
+ authorization_endpoint: data.authorization_endpoint,
56
+ token_endpoint: data.token_endpoint,
57
+ registration_endpoint: typeof data.registration_endpoint === 'string' ? data.registration_endpoint : undefined,
58
+ scopes_supported: Array.isArray(data.scopes_supported)
59
+ ? data.scopes_supported
60
+ : undefined,
61
+ response_types_supported: Array.isArray(data.response_types_supported)
62
+ ? data.response_types_supported
63
+ : undefined,
64
+ grant_types_supported: Array.isArray(data.grant_types_supported)
65
+ ? data.grant_types_supported
66
+ : undefined,
67
+ code_challenge_methods_supported: Array.isArray(data.code_challenge_methods_supported)
68
+ ? data.code_challenge_methods_supported
69
+ : undefined,
70
+ authorization_response_iss_parameter_supported: data.authorization_response_iss_parameter_supported === true,
71
+ client_id_metadata_document_supported: data.client_id_metadata_document_supported === true,
72
+ };
73
+ }
74
+ export async function discoverAuthServerMetadata(authServerUrl, fetchFn = fetch) {
75
+ const base = authServerUrl.replace(/\/$/, '');
76
+ const paths = ['/.well-known/oauth-authorization-server', '/.well-known/openid-configuration'];
77
+ for (const path of paths) {
78
+ try {
79
+ const response = await fetchFn(`${base}${path}`, {
80
+ headers: { Accept: 'application/json' },
81
+ });
82
+ if (!response.ok)
83
+ continue;
84
+ const data = (await response.json());
85
+ const parsed = parseAuthServerMetadata(data, authServerUrl);
86
+ if (parsed)
87
+ return parsed;
88
+ }
89
+ catch {
90
+ // Try next discovery endpoint
91
+ }
92
+ }
93
+ throw new Error(`Failed to discover authorization server metadata for "${authServerUrl}"`); // lexicon-exempt: developer contract / internal diagnostic — not end-user or model copy (P2)
94
+ }
95
+ /**
96
+ * Initiate an OAuth 2.1 PKCE authorization flow statelesssly.
97
+ *
98
+ * Discovers resource & AS metadata (or uses preResolved), generates PKCE code_verifier/code_challenge,
99
+ * and packs the verifier, expected issuer, and resource into an HMAC-signed state token.
100
+ */
101
+ export async function createOAuthPkceFlow(options) {
102
+ const fetchFn = options.fetchFn ?? fetch;
103
+ const pre = options.preResolved;
104
+ let issuer = pre?.issuer;
105
+ let authEndpoint = pre?.authorizationEndpoint;
106
+ let resource = pre?.resource;
107
+ if (!authEndpoint || !issuer) {
108
+ let asUrl = options.resourceServerUrl;
109
+ try {
110
+ // 1. Try RFC 9728 discovery on resource server
111
+ const resourceMeta = await discoverResourceMetadata(options.resourceServerUrl, fetchFn);
112
+ resource = resourceMeta.resource;
113
+ asUrl = resourceMeta.authorization_servers[0];
114
+ }
115
+ catch {
116
+ // If resource discovery fails, treat resourceServerUrl as direct AS URL
117
+ resource = options.resourceServerUrl;
118
+ }
119
+ // 2. Discover AS metadata
120
+ const asMeta = await discoverAuthServerMetadata(asUrl, fetchFn);
121
+ issuer = asMeta.issuer;
122
+ authEndpoint = asMeta.authorization_endpoint;
123
+ }
124
+ // 3. Generate PKCE verifier and S256 challenge
125
+ const codeVerifier = generateCodeVerifier();
126
+ const codeChallenge = await computeCodeChallenge(codeVerifier);
127
+ // 4. Seal stateless state envelope
128
+ const ttl = options.stateTtlMs ?? 10 * 60 * 1000; // 10 mins
129
+ const state = await sealStatePayload({
130
+ codeVerifier,
131
+ expectedIssuer: issuer,
132
+ resource,
133
+ redirectUri: options.redirectUri,
134
+ expiresAt: Date.now() + ttl,
135
+ clientId: options.clientId,
136
+ }, options.signingSecret);
137
+ // 5. Construct authorization URL
138
+ const authUrl = new URL(authEndpoint);
139
+ authUrl.searchParams.set('response_type', 'code');
140
+ authUrl.searchParams.set('client_id', options.clientId);
141
+ authUrl.searchParams.set('redirect_uri', options.redirectUri);
142
+ authUrl.searchParams.set('code_challenge', codeChallenge);
143
+ authUrl.searchParams.set('code_challenge_method', 'S256');
144
+ authUrl.searchParams.set('state', state);
145
+ if (resource) {
146
+ authUrl.searchParams.set('resource', resource);
147
+ }
148
+ if (options.scopes && options.scopes.length > 0) {
149
+ authUrl.searchParams.set('scope', options.scopes.join(' '));
150
+ }
151
+ return {
152
+ authorizationUrl: authUrl.toString(),
153
+ state,
154
+ codeVerifier,
155
+ codeChallenge,
156
+ issuer,
157
+ resource,
158
+ };
159
+ }
160
+ /**
161
+ * Validates the authorization response `iss` parameter per RFC 9207 Section 2.4.
162
+ *
163
+ * "The client MUST validate that the 'iss' parameter in the authorization response matches
164
+ * the issuer identifier of the authorization server... using simple string comparison (RFC 3986 Section 6.2.1)."
165
+ */
166
+ export function validateIssuer(expectedIssuer, receivedIss) {
167
+ if (!receivedIss) {
168
+ // If not provided, allowed only if AS did not support it, but if provided, must match exactly
169
+ return;
170
+ }
171
+ // Simple byte-exact string comparison (no URL normalization per 2026-07-28 spec)
172
+ if (expectedIssuer !== receivedIss) {
173
+ throw new Error(`RFC 9207 Issuer mismatch detected (potential mix-up attack): expected "${expectedIssuer}", received "${receivedIss}"`);
174
+ }
175
+ }
176
+ /**
177
+ * POST a form-encoded token request and parse the response.
178
+ *
179
+ * The authorization-code exchange and the refresh flow differ only in the body
180
+ * they send and the label on a failure, so the transport, the error shape, and
181
+ * the expiry computation live here once.
182
+ */
183
+ async function postTokenRequest(fetchFn, tokenEndpoint, body, failureLabel) {
184
+ const response = await fetchFn(tokenEndpoint, {
185
+ method: 'POST',
186
+ headers: {
187
+ 'Content-Type': 'application/x-www-form-urlencoded',
188
+ Accept: 'application/json',
189
+ },
190
+ body: body.toString(),
191
+ });
192
+ if (!response.ok) {
193
+ const errText = await response.text();
194
+ throw new Error(`${failureLabel} at ${tokenEndpoint} (HTTP ${response.status}): ${errText}`);
195
+ }
196
+ const tokens = (await response.json());
197
+ const expiresAt = tokens.expires_in !== undefined ? Date.now() + tokens.expires_in * 1000 : undefined;
198
+ return { tokens, expiresAt };
199
+ }
200
+ /**
201
+ * Exchange authorization code for access and refresh tokens.
202
+ *
203
+ * Verifies state HMAC, performs RFC 9207 `iss` validation, and posts
204
+ * code_verifier and resource to the token_endpoint.
205
+ */
206
+ export async function exchangeOAuthPkce(options) {
207
+ const fetchFn = options.fetchFn ?? fetch;
208
+ // 1. Unseal state envelope and verify HMAC + expiration
209
+ const statePayload = await unsealStatePayload(options.state, options.signingSecret);
210
+ // Validate redirect_uri matches authorization request per RFC 6749 Section 4.1.3
211
+ const effectiveRedirectUri = options.redirectUri || statePayload.redirectUri;
212
+ if (options.redirectUri &&
213
+ statePayload.redirectUri &&
214
+ options.redirectUri !== statePayload.redirectUri) {
215
+ throw new Error(`Redirect URI mismatch: expected "${statePayload.redirectUri}", received "${options.redirectUri}"`);
216
+ }
217
+ // 2. Validate RFC 9207 issuer
218
+ validateIssuer(statePayload.expectedIssuer, options.iss);
219
+ // 3. Resolve token endpoint
220
+ let tokenEndpoint = options.tokenEndpoint;
221
+ if (!tokenEndpoint) {
222
+ const asMeta = await discoverAuthServerMetadata(statePayload.expectedIssuer, fetchFn);
223
+ tokenEndpoint = asMeta.token_endpoint;
224
+ }
225
+ // 4. POST to token endpoint
226
+ const body = new URLSearchParams();
227
+ body.set('grant_type', 'authorization_code');
228
+ body.set('code', options.code);
229
+ body.set('redirect_uri', effectiveRedirectUri);
230
+ body.set('client_id', statePayload.clientId);
231
+ body.set('code_verifier', statePayload.codeVerifier);
232
+ if (statePayload.resource) {
233
+ body.set('resource', statePayload.resource);
234
+ }
235
+ const { tokens, expiresAt } = await postTokenRequest(fetchFn, tokenEndpoint, body, 'Token exchange failed');
236
+ const credential = {
237
+ type: 'oauth2',
238
+ issuer: statePayload.expectedIssuer,
239
+ resource: statePayload.resource,
240
+ accessToken: tokens.access_token,
241
+ refreshToken: tokens.refresh_token,
242
+ expiresAt,
243
+ tokenEndpoint,
244
+ clientId: statePayload.clientId,
245
+ scope: tokens.scope,
246
+ };
247
+ return { tokens, credential };
248
+ }
249
+ /**
250
+ * Refresh an expired OAuth 2.1 access token.
251
+ */
252
+ export async function refreshOAuthToken(options) {
253
+ const fetchFn = options.fetchFn ?? fetch;
254
+ const body = new URLSearchParams();
255
+ body.set('grant_type', 'refresh_token');
256
+ body.set('refresh_token', options.refreshToken);
257
+ body.set('client_id', options.clientId);
258
+ if (options.resource) {
259
+ body.set('resource', options.resource);
260
+ }
261
+ if (options.scope) {
262
+ body.set('scope', options.scope);
263
+ }
264
+ const { tokens, expiresAt } = await postTokenRequest(fetchFn, options.tokenEndpoint, body, 'Token refresh failed');
265
+ const credential = {
266
+ type: 'oauth2',
267
+ issuer: options.issuer,
268
+ resource: options.resource,
269
+ accessToken: tokens.access_token,
270
+ // Preserve existing refresh token if new one is not issued
271
+ refreshToken: tokens.refresh_token ?? options.refreshToken,
272
+ expiresAt,
273
+ tokenEndpoint: options.tokenEndpoint,
274
+ clientId: options.clientId,
275
+ scope: tokens.scope ?? options.scope,
276
+ };
277
+ return { tokens, credential };
278
+ }