@konneal/engine 0.1.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 (285) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +13 -0
  3. package/dist/admin.d.ts +26 -0
  4. package/dist/ai.d.ts +6 -0
  5. package/dist/anchors.d.ts +6 -0
  6. package/dist/answercache.d.ts +22 -0
  7. package/dist/ask.d.ts +5 -0
  8. package/dist/auth.d.ts +12 -0
  9. package/dist/bubble.d.ts +14 -0
  10. package/dist/chunk-LLWPT2XV.js +49 -0
  11. package/dist/chunk-MB74PTRM.js +114 -0
  12. package/dist/chunk-WOGQM7DJ.js +197 -0
  13. package/dist/chunk-WWNCWKKC.js +42 -0
  14. package/dist/completion.d.ts +5 -0
  15. package/dist/config.d.ts +154 -0
  16. package/dist/config.js +37 -0
  17. package/dist/context.d.ts +115 -0
  18. package/dist/conversations.d.ts +5 -0
  19. package/dist/drafts.d.ts +129 -0
  20. package/dist/env.d.ts +57 -0
  21. package/dist/faithfulness.d.ts +5 -0
  22. package/dist/grader.d.ts +3 -0
  23. package/dist/graph.d.ts +13 -0
  24. package/dist/hybrid.d.ts +7 -0
  25. package/dist/index.d.ts +9 -0
  26. package/dist/index.js +5373 -0
  27. package/dist/internal_gateway.d.ts +14 -0
  28. package/dist/lexical.d.ts +7 -0
  29. package/dist/livedata.d.ts +77 -0
  30. package/dist/memories.d.ts +10 -0
  31. package/dist/modelplane.d.ts +61 -0
  32. package/dist/oidc.d.ts +73 -0
  33. package/dist/pipeline.d.ts +57 -0
  34. package/dist/profile.d.ts +2 -0
  35. package/dist/profile.gen.d.ts +70 -0
  36. package/dist/profile.js +8 -0
  37. package/dist/projects.d.ts +8 -0
  38. package/dist/prompts/conversational.md +8 -0
  39. package/dist/prompts/enrichment.md +3 -0
  40. package/dist/prompts/faithfulness.md +1 -0
  41. package/dist/prompts/grader.md +5 -0
  42. package/dist/prompts/listwise.md +3 -0
  43. package/dist/prompts/precision.md +1 -0
  44. package/dist/prompts/reflect.md +1 -0
  45. package/dist/prompts/relevancy.md +1 -0
  46. package/dist/prompts/research.md +10 -0
  47. package/dist/prompts/section-summary.md +5 -0
  48. package/dist/prompts/summarize.md +1 -0
  49. package/dist/prompts/system.md +18 -0
  50. package/dist/prompts/understanding.md +17 -0
  51. package/dist/quota.d.ts +13 -0
  52. package/dist/reflect.d.ts +5 -0
  53. package/dist/refs.d.ts +40 -0
  54. package/dist/refusal.d.ts +9 -0
  55. package/dist/refusal.js +9 -0
  56. package/dist/requestScope.d.ts +26 -0
  57. package/dist/requestScope.js +10 -0
  58. package/dist/research.d.ts +8 -0
  59. package/dist/search.d.ts +4 -0
  60. package/dist/selfquery.d.ts +7 -0
  61. package/dist/session.d.ts +1 -0
  62. package/dist/share.d.ts +2 -0
  63. package/dist/structural.d.ts +27 -0
  64. package/dist/tablecontext.d.ts +11 -0
  65. package/dist/understand.d.ts +11 -0
  66. package/dist/understandContract.d.ts +29 -0
  67. package/dist/verdict.d.ts +24 -0
  68. package/docs/API.md +451 -0
  69. package/docs/ARCHITECTURE.md +302 -0
  70. package/docs/AUDIT-2026-08-24.md +71 -0
  71. package/docs/CONTRIBUTOR-AUDIT-2026-08-25.md +147 -0
  72. package/docs/INGEST-ARCHITECTURE.md +158 -0
  73. package/docs/MCP.md +92 -0
  74. package/docs/METANORMA-AI-SERIALIZATION.md +247 -0
  75. package/docs/MKO-EXPORT-PIPELINE.md +147 -0
  76. package/docs/REDESIGN-NORMATIVE-RAG-ETSI.md +485 -0
  77. package/docs/RESEARCH-SOTA-2026.md +243 -0
  78. package/docs/ROADMAP-SOTA.md +130 -0
  79. package/docs/SOTA-STAGE-SPECS.md +509 -0
  80. package/docs/annealment/F1-verdict.md +27 -0
  81. package/docs/annealment/F10-notes.md +19 -0
  82. package/docs/annealment/F11-composition.md +17 -0
  83. package/docs/annealment/F12-passport.md +17 -0
  84. package/docs/annealment/F2-counterfactual.md +20 -0
  85. package/docs/annealment/F3-absence.md +21 -0
  86. package/docs/annealment/F4-instance.md +18 -0
  87. package/docs/annealment/F5-workflow.md +21 -0
  88. package/docs/annealment/F6-impact.md +21 -0
  89. package/docs/annealment/F7-editions.md +18 -0
  90. package/docs/annealment/F8-selfverify.md +19 -0
  91. package/docs/annealment/F9-projection-qa.md +17 -0
  92. package/docs/annealment/L0-locate.md +19 -0
  93. package/docs/annealment/L1-extract.md +18 -0
  94. package/docs/annealment/L2-nomenclature.md +22 -0
  95. package/docs/annealment/L3-geometry.md +23 -0
  96. package/docs/annealment/L4-composition.md +21 -0
  97. package/docs/annealment/L5-cross-standard.md +20 -0
  98. package/docs/annealment/L6-diachrony.md +21 -0
  99. package/docs/annealment/L7-perception.md +20 -0
  100. package/docs/annealment/L8-computation.md +22 -0
  101. package/docs/annealment/L9-instance-process.md +23 -0
  102. package/docs/annealment/README.md +10 -0
  103. package/docs/guidelines-metanorma-ai-programme.md +279 -0
  104. package/docs/identity-onboarding-rag.md +65 -0
  105. package/docs/identity-service.md +219 -0
  106. package/docs/knowledge-annealment.md +273 -0
  107. package/docs/konneal-extraction-plan.md +481 -0
  108. package/docs/metanorma-for-ai.md +270 -0
  109. package/docs/mirror-plan.md +36 -0
  110. package/docs/multi-sdo-architecture.md +191 -0
  111. package/docs/paper-annealment-comparison.md +259 -0
  112. package/docs/paper-assets/architecture.svg +94 -0
  113. package/docs/paper-assets/contract-v2.svg +94 -0
  114. package/docs/paper-assets/mko-ingest.svg +91 -0
  115. package/docs/paper-oiml-bulletin.md +402 -0
  116. package/docs/paper-oiml-bulletin.mdx +419 -0
  117. package/docs/product-branding-options.md +172 -0
  118. package/docs/projects-design.md +88 -0
  119. package/docs/sota-mechanisms.md +184 -0
  120. package/docs/spec-api.md +77 -0
  121. package/docs/spec-pipeline.md +126 -0
  122. package/docs/vector-adapter.md +88 -0
  123. package/package.json +70 -0
  124. package/profile/corpora.yaml +5 -0
  125. package/profile/datasets.yaml +14 -0
  126. package/profile/prompts.yaml +5 -0
  127. package/profile/publisher.yaml +17 -0
  128. package/profile/retrieval.yaml +1 -0
  129. package/profile/sources.yaml +5 -0
  130. package/profile/ui.yaml +7 -0
  131. package/scripts/gen_profile.mjs +33 -0
  132. package/workers/shared/ai.ts +21 -0
  133. package/workers/shared/auth.ts +16 -0
  134. package/workers/shared/chunk.ts +108 -0
  135. package/workers/shared/oidc.ts +312 -0
  136. package/workers/shared/router.ts +45 -0
  137. package/workers/shared/session.ts +104 -0
  138. package/workers/worker_internal/src/index.ts +157 -0
  139. package/workers/worker_internal/tsconfig.json +15 -0
  140. package/workers/worker_internal/wrangler.toml +32 -0
  141. package/workers/worker_mcp/src/index.ts +175 -0
  142. package/workers/worker_mcp/tsconfig.json +13 -0
  143. package/workers/worker_mcp/wrangler.toml +18 -0
  144. package/workers/worker_public/migrations/0002_conversations.sql +22 -0
  145. package/workers/worker_public/migrations/0003_shared_conversations.sql +9 -0
  146. package/workers/worker_public/migrations/0004_graph.sql +16 -0
  147. package/workers/worker_public/migrations/0005_documents.sql +19 -0
  148. package/workers/worker_public/migrations/0006_conversation_entities.sql +11 -0
  149. package/workers/worker_public/migrations/0007_chunks_fts.sql +43 -0
  150. package/workers/worker_public/migrations/0008_unit_payloads.sql +15 -0
  151. package/workers/worker_public/migrations/0009_chunks_unit.sql +8 -0
  152. package/workers/worker_public/migrations/0009_message_context.sql +7 -0
  153. package/workers/worker_public/migrations/0010_model_nodes.sql +33 -0
  154. package/workers/worker_public/migrations/0011_schema_union.sql +38 -0
  155. package/workers/worker_public/migrations/0012_memories.sql +15 -0
  156. package/workers/worker_public/migrations/0013_projects.sql +21 -0
  157. package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.d.ts +16306 -0
  158. package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.ts +16261 -0
  159. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.d.ts +16373 -0
  160. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.ts +16328 -0
  161. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.d.ts +16382 -0
  162. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.ts +16337 -0
  163. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.d.ts +16383 -0
  164. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.ts +16338 -0
  165. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.d.ts +16403 -0
  166. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.ts +16358 -0
  167. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.d.ts +16408 -0
  168. package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.ts +16363 -0
  169. package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.d.ts +16414 -0
  170. package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.ts +16369 -0
  171. package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.d.ts +16414 -0
  172. package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.ts +16369 -0
  173. package/workers/worker_public/node_modules/@cloudflare/workers-types/README.md +135 -0
  174. package/workers/worker_public/node_modules/@cloudflare/workers-types/entrypoints.svg +53 -0
  175. package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.d.ts +17095 -0
  176. package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.ts +17050 -0
  177. package/workers/worker_public/node_modules/@cloudflare/workers-types/index.d.ts +16306 -0
  178. package/workers/worker_public/node_modules/@cloudflare/workers-types/index.ts +16261 -0
  179. package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.d.ts +16447 -0
  180. package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.ts +16402 -0
  181. package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.d.ts +16306 -0
  182. package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.ts +16261 -0
  183. package/workers/worker_public/node_modules/@cloudflare/workers-types/package.json +11 -0
  184. package/workers/worker_public/package.json +13 -0
  185. package/workers/worker_public/prompts/conversational.md +8 -0
  186. package/workers/worker_public/prompts/enrichment.md +3 -0
  187. package/workers/worker_public/prompts/faithfulness.md +1 -0
  188. package/workers/worker_public/prompts/grader.md +5 -0
  189. package/workers/worker_public/prompts/listwise.md +3 -0
  190. package/workers/worker_public/prompts/precision.md +1 -0
  191. package/workers/worker_public/prompts/reflect.md +1 -0
  192. package/workers/worker_public/prompts/relevancy.md +1 -0
  193. package/workers/worker_public/prompts/research.md +10 -0
  194. package/workers/worker_public/prompts/section-summary.md +5 -0
  195. package/workers/worker_public/prompts/summarize.md +1 -0
  196. package/workers/worker_public/prompts/system.md +18 -0
  197. package/workers/worker_public/prompts/understanding.md +17 -0
  198. package/workers/worker_public/public/app.js +166 -0
  199. package/workers/worker_public/public/index.html +48 -0
  200. package/workers/worker_public/public/style.css +147 -0
  201. package/workers/worker_public/schema.sql +248 -0
  202. package/workers/worker_public/src/admin.ts +358 -0
  203. package/workers/worker_public/src/ai.ts +71 -0
  204. package/workers/worker_public/src/anchors.ts +41 -0
  205. package/workers/worker_public/src/answercache.ts +72 -0
  206. package/workers/worker_public/src/ask.ts +1094 -0
  207. package/workers/worker_public/src/auth.ts +252 -0
  208. package/workers/worker_public/src/bubble.ts +111 -0
  209. package/workers/worker_public/src/completion.ts +75 -0
  210. package/workers/worker_public/src/config.ts +238 -0
  211. package/workers/worker_public/src/context.ts +238 -0
  212. package/workers/worker_public/src/conversations.ts +162 -0
  213. package/workers/worker_public/src/drafts.ts +497 -0
  214. package/workers/worker_public/src/env.ts +90 -0
  215. package/workers/worker_public/src/faithfulness.ts +63 -0
  216. package/workers/worker_public/src/grader.ts +89 -0
  217. package/workers/worker_public/src/graph.ts +63 -0
  218. package/workers/worker_public/src/hybrid.ts +77 -0
  219. package/workers/worker_public/src/index.ts +441 -0
  220. package/workers/worker_public/src/internal_gateway.ts +41 -0
  221. package/workers/worker_public/src/lexical.ts +86 -0
  222. package/workers/worker_public/src/lib/hit.ts +4 -0
  223. package/workers/worker_public/src/lib/http.ts +83 -0
  224. package/workers/worker_public/src/lib/router.ts +4 -0
  225. package/workers/worker_public/src/livedata.ts +334 -0
  226. package/workers/worker_public/src/memories.ts +81 -0
  227. package/workers/worker_public/src/modelplane.ts +213 -0
  228. package/workers/worker_public/src/oidc.ts +333 -0
  229. package/workers/worker_public/src/pipeline.ts +377 -0
  230. package/workers/worker_public/src/ports/blobs.ts +7 -0
  231. package/workers/worker_public/src/ports/cloudflare/adapters.ts +177 -0
  232. package/workers/worker_public/src/ports/kv.ts +8 -0
  233. package/workers/worker_public/src/ports/model.ts +28 -0
  234. package/workers/worker_public/src/ports/runtime.ts +13 -0
  235. package/workers/worker_public/src/ports/store.ts +20 -0
  236. package/workers/worker_public/src/ports/vector.ts +26 -0
  237. package/workers/worker_public/src/profile.gen.ts +101 -0
  238. package/workers/worker_public/src/profile.ts +16 -0
  239. package/workers/worker_public/src/projects.ts +108 -0
  240. package/workers/worker_public/src/prompts.d.ts +6 -0
  241. package/workers/worker_public/src/quota.ts +54 -0
  242. package/workers/worker_public/src/reflect.ts +67 -0
  243. package/workers/worker_public/src/refs.ts +107 -0
  244. package/workers/worker_public/src/refusal.ts +65 -0
  245. package/workers/worker_public/src/requestScope.ts +71 -0
  246. package/workers/worker_public/src/research.ts +126 -0
  247. package/workers/worker_public/src/search.ts +56 -0
  248. package/workers/worker_public/src/selfquery.ts +25 -0
  249. package/workers/worker_public/src/session.ts +4 -0
  250. package/workers/worker_public/src/share.ts +53 -0
  251. package/workers/worker_public/src/stages/conceptGraph.ts +68 -0
  252. package/workers/worker_public/src/stages/conceptSteer.ts +39 -0
  253. package/workers/worker_public/src/stages/corpusScope.ts +25 -0
  254. package/workers/worker_public/src/stages/dedup.ts +10 -0
  255. package/workers/worker_public/src/stages/dense.ts +73 -0
  256. package/workers/worker_public/src/stages/diversity.ts +33 -0
  257. package/workers/worker_public/src/stages/editionCover.ts +63 -0
  258. package/workers/worker_public/src/stages/editionSteer.ts +88 -0
  259. package/workers/worker_public/src/stages/familyBoost.ts +22 -0
  260. package/workers/worker_public/src/stages/federate.ts +22 -0
  261. package/workers/worker_public/src/stages/glossary.ts +65 -0
  262. package/workers/worker_public/src/stages/graphLane.ts +31 -0
  263. package/workers/worker_public/src/stages/hyde.ts +29 -0
  264. package/workers/worker_public/src/stages/index.ts +69 -0
  265. package/workers/worker_public/src/stages/lexicalUnion.ts +21 -0
  266. package/workers/worker_public/src/stages/multiQuery.ts +57 -0
  267. package/workers/worker_public/src/stages/overviewDemote.ts +14 -0
  268. package/workers/worker_public/src/stages/poolOpen.ts +10 -0
  269. package/workers/worker_public/src/stages/propagate.ts +15 -0
  270. package/workers/worker_public/src/stages/rerank.ts +47 -0
  271. package/workers/worker_public/src/stages/seal.ts +16 -0
  272. package/workers/worker_public/src/stages/sectionDescent.ts +61 -0
  273. package/workers/worker_public/src/stages/stdRefNudge.ts +35 -0
  274. package/workers/worker_public/src/stages/subQuery.ts +42 -0
  275. package/workers/worker_public/src/stages/termNudge.ts +24 -0
  276. package/workers/worker_public/src/stages/typedPin.ts +131 -0
  277. package/workers/worker_public/src/stages/types.ts +112 -0
  278. package/workers/worker_public/src/stages/windowFloor.ts +23 -0
  279. package/workers/worker_public/src/structural.ts +171 -0
  280. package/workers/worker_public/src/tablecontext.ts +41 -0
  281. package/workers/worker_public/src/understand.ts +72 -0
  282. package/workers/worker_public/src/understandContract.ts +67 -0
  283. package/workers/worker_public/src/verdict.ts +255 -0
  284. package/workers/worker_public/tsconfig.json +18 -0
  285. package/workers/worker_public/wrangler.toml +104 -0
@@ -0,0 +1,33 @@
1
+ // Profile codegen (multi-SDO): profile/*.yaml → the compile-time profile
2
+ // modules (workers + site). The generated files are committed; the drift
3
+ // test (tests/profile.test.ts) fails CI when someone edits one side
4
+ // without regenerating — the same discipline as the estate's gen:data.
5
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
6
+ import YAML from "yaml";
7
+
8
+ const HEADER =
9
+ "// GENERATED from profile/*.yaml — regenerate: node scripts/gen_profile.mjs\n" +
10
+ "// (never edit; the drift test compares this file to the sources)\n";
11
+
12
+ const render = (profileDir = "profile") => {
13
+ const read = (f) => YAML.parse(readFileSync(`${profileDir}/${f}`, "utf8"));
14
+ const publisher = read("publisher.yaml");
15
+ const datasets = read("datasets.yaml").datasets;
16
+ const corpora = read("corpora.yaml");
17
+ const sources = read("sources.yaml");
18
+ const ui = read("ui.yaml");
19
+ const retrieval = read("retrieval.yaml");
20
+ const prompts = read("prompts.yaml");
21
+ const body = JSON.stringify({ publisher, datasets, corpora, sources, ui, retrieval, prompts }, null, 2);
22
+ return `${HEADER}export const PROFILE = ${body} as const;\n`;
23
+ };
24
+
25
+ if (process.argv[1].endsWith("gen_profile.mjs") && !process.env.PROFILE_RENDER_ONLY) {
26
+ for (const out of ["workers/worker_public/src/profile.gen.ts", "site/src/profile.gen.ts"]) {
27
+ if (out.startsWith("site/") && !existsSync("site")) continue; // engine repo: no site plane
28
+ writeFileSync(out, render());
29
+ console.log(`generated ${out}`);
30
+ }
31
+ }
32
+
33
+ export { render };
@@ -0,0 +1,21 @@
1
+ // Shared AI helper: embed with retry.
2
+ const EMBED_MODEL = "@cf/qwen/qwen3-embedding-0.6b";
3
+
4
+ export async function embed(ai: any, text: string): Promise<number[]> {
5
+ const shapes = [{ text: [text] }, { input: { input: [text] } }];
6
+ for (let attempt = 0; attempt < 3; attempt++) {
7
+ for (const body of shapes) {
8
+ try {
9
+ const res: any = await ai.run(EMBED_MODEL, body);
10
+ const d = res?.data ?? res?.result?.data;
11
+ if (Array.isArray(d)) {
12
+ const first = d[0];
13
+ if (Array.isArray(first)) return first.map(Number);
14
+ if (first?.embedding) return first.embedding.map(Number);
15
+ }
16
+ if (Array.isArray(res?.embedding)) return res.embedding.map(Number);
17
+ } catch { /* next shape */ }
18
+ }
19
+ }
20
+ throw new Error("embedding failed");
21
+ }
@@ -0,0 +1,16 @@
1
+ // Shared auth: session validation + role constants used by both workers.
2
+ import { readSession } from "./session";
3
+
4
+ // Two tiers: anonymous (public) and member (both). No role gates.
5
+
6
+ export function authConfig(env: any) {
7
+ const sessionSecret = env.SESSION_SECRET;
8
+ if (!sessionSecret) return null;
9
+ return { sessionSecret };
10
+ }
11
+
12
+ export async function sessionFrom(req: Request, env: any) {
13
+ const cfg = authConfig(env);
14
+ if (!cfg) return null;
15
+ return readSession(req, cfg.sessionSecret);
16
+ }
@@ -0,0 +1,108 @@
1
+ // The chunk wire contract — the shape of every Vectorize vector's
2
+ // metadata, shared by both workers. The CANONICAL producer definition is
3
+ // ingest/vector_adapter.py (ChunkMetaModel, pydantic); this module is its
4
+ // serving-side mirror, and tests/chunkmeta.test.ts fails when either side
5
+ // drifts. Change a field → change vector_adapter.py and this file in the
6
+ // same commit.
7
+
8
+ export interface ChunkMeta {
9
+ doc_id: string;
10
+ docidentifier: string;
11
+ doctype: string;
12
+ doc_number: string;
13
+ edition: string;
14
+ language: string;
15
+ clause_anchor: string;
16
+ clause_title: string;
17
+ tier: string;
18
+ corpus: string;
19
+ text_ref: string;
20
+ status?: string;
21
+ superseded_by?: string;
22
+ /** answer contract v2: typed MKO units carry their unit id + block type */
23
+ unit_id?: string;
24
+ block?: string;
25
+ unit_hash?: string;
26
+ producer?: string;
27
+ source_lane?: string;
28
+ linked_clause?: string;
29
+ linked_document?: string;
30
+ /** document order (metanorma-document#56): reading order is a sort */
31
+ ordinal?: number;
32
+ /** the model plane (retrieval-export derivation): unit versioning and
33
+ * identity, canonical per primmel/primmel-ts#65 */
34
+ model_version?: string;
35
+ model_node?: string;
36
+ model_kind?: string;
37
+ standard?: string;
38
+ /** section-summary unit (FABLE multi-granularity, arXiv:2601.18116): a
39
+ * depth-1 clause summary vector — a navigation node whose children
40
+ * (child_anchors CSV) are quotable leaf clauses. The corpus's real
41
+ * chunks start at depth 2, so these nodes cannot collide with them. */
42
+ section_summary?: string;
43
+ child_anchors?: string;
44
+ /** contextual enrichment (one-time corpus spend, KV-cached) */
45
+ ctx?: string;
46
+ /** the capped chunk text riding inside vector metadata */
47
+ chunk_text?: string;
48
+ }
49
+
50
+ export interface Hit {
51
+ id: string;
52
+ score: number;
53
+ rerank_score?: number;
54
+ metadata: ChunkMeta;
55
+ text: string;
56
+ }
57
+
58
+ // Runtime manifest of ChunkMeta's keys — the bridge the contract test
59
+ // reads (interfaces erase at runtime; this array does not).
60
+ export const CHUNK_META_FIELDS = [
61
+ "doc_id",
62
+ "docidentifier",
63
+ "doctype",
64
+ "doc_number",
65
+ "edition",
66
+ "language",
67
+ "clause_anchor",
68
+ "clause_title",
69
+ "tier",
70
+ "corpus",
71
+ "text_ref",
72
+ "status",
73
+ "superseded_by",
74
+ "unit_id",
75
+ "block",
76
+ "unit_hash",
77
+ "producer",
78
+ "source_lane",
79
+ "linked_clause",
80
+ "linked_document",
81
+ "ordinal",
82
+ "model_version",
83
+ "model_node",
84
+ "model_kind",
85
+ "standard",
86
+ "section_summary",
87
+ "child_anchors",
88
+ "ctx",
89
+ "chunk_text",
90
+ ] as const;
91
+
92
+ /** Vectorize match list → Hit list (the wire conversion both workers
93
+ * perform; one definition, like the type above it). */
94
+ export function toHits(matches: any[]): Hit[] {
95
+ return matches.map((m) => ({
96
+ id: m.id,
97
+ score: m.score,
98
+ metadata: (m.metadata ?? {}) as ChunkMeta,
99
+ text: (m.metadata?.chunk_text as string) ?? "",
100
+ }));
101
+ }
102
+
103
+ // Compile-time: the manifest and the interface are the same set — both
104
+ // directions. A mismatch fails typecheck here, not at the wire.
105
+ type AssertNever<T extends never> = T;
106
+ type ManifestField = (typeof CHUNK_META_FIELDS)[number];
107
+ export type _manifestMissingFromInterface = AssertNever<Exclude<ManifestField, keyof ChunkMeta>>;
108
+ export type _interfaceMissingFromManifest = AssertNever<Exclude<keyof ChunkMeta, ManifestField>>;
@@ -0,0 +1,312 @@
1
+ // OIDC relying-party half of the Authorization Code + PKCE flow,
2
+ // hand-rolled on WebCrypto + fetch. Ported from the estate's reference
3
+ // implementation (oimlsmart/smart packages/platform-server/src/oidc.ts)
4
+ // per docs/identity-service.md — zero library dependencies by design.
5
+
6
+ export type OidcFailureReason =
7
+ | 'not_configured'
8
+ | 'discovery'
9
+ | 'issuer_mismatch'
10
+ | 'exchange'
11
+ | 'state'
12
+ | 'token_malformed'
13
+ | 'token_alg'
14
+ | 'token_signature'
15
+ | 'token_issuer'
16
+ | 'token_audience'
17
+ | 'token_expired'
18
+ | 'token_nonce';
19
+
20
+ export class OidcError extends Error {
21
+ constructor(readonly reason: OidcFailureReason, message: string) {
22
+ super(message);
23
+ this.name = "OidcError";
24
+ }
25
+ }
26
+
27
+ export interface OidcMetadata {
28
+ issuer: string;
29
+ authorization_endpoint: string;
30
+ token_endpoint: string;
31
+ jwks_uri: string;
32
+ end_session_endpoint?: string;
33
+ userinfo_endpoint?: string;
34
+ }
35
+
36
+ interface CachedMetadata {
37
+ metadata: OidcMetadata;
38
+ fetchedAt: number;
39
+ }
40
+ const metadataCache = new Map<string, CachedMetadata>();
41
+ const METADATA_TTL_MS = 60 * 60 * 1000;
42
+
43
+ export async function discoverIssuer(issuer: string): Promise<OidcMetadata> {
44
+ const cached = metadataCache.get(issuer);
45
+ if (cached && Date.now() - cached.fetchedAt < METADATA_TTL_MS) return cached.metadata;
46
+
47
+ const wellKnown = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
48
+ let body: unknown;
49
+ try {
50
+ const res = await fetch(wellKnown);
51
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
52
+ body = await res.json();
53
+ } catch (err) {
54
+ throw new OidcError("discovery", `could not fetch ${wellKnown}: ${(err as Error).message}`);
55
+ }
56
+ const meta = body as Partial<OidcMetadata>;
57
+ if (
58
+ typeof meta?.issuer !== "string" ||
59
+ typeof meta?.authorization_endpoint !== "string" ||
60
+ typeof meta?.token_endpoint !== "string" ||
61
+ typeof meta?.jwks_uri !== "string"
62
+ ) {
63
+ throw new OidcError("discovery", `the metadata at ${wellKnown} is incomplete`);
64
+ }
65
+ if (meta.issuer.replace(/\/$/, "") !== issuer.replace(/\/$/, "")) {
66
+ throw new OidcError("issuer_mismatch", `the metadata declares issuer ${meta.issuer}, not ${issuer}`);
67
+ }
68
+ const metadata: OidcMetadata = {
69
+ issuer: meta.issuer,
70
+ authorization_endpoint: meta.authorization_endpoint,
71
+ token_endpoint: meta.token_endpoint,
72
+ jwks_uri: meta.jwks_uri,
73
+ ...(typeof meta.end_session_endpoint === "string" ? { end_session_endpoint: meta.end_session_endpoint } : {}),
74
+ ...(typeof meta.userinfo_endpoint === "string" ? { userinfo_endpoint: meta.userinfo_endpoint } : {}),
75
+ };
76
+ metadataCache.set(issuer, { metadata, fetchedAt: Date.now() });
77
+ return metadata;
78
+ }
79
+
80
+ function base64url(bytes: Uint8Array): string {
81
+ let bin = "";
82
+ for (const b of bytes) bin += String.fromCharCode(b);
83
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
84
+ }
85
+
86
+ function base64urlDecode(s: string): Uint8Array {
87
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice(0, (4 - (s.length % 4)) % 4);
88
+ const bin = atob(b64);
89
+ const out = new Uint8Array(bin.length);
90
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
91
+ return out;
92
+ }
93
+
94
+ export function randomToken(): string {
95
+ return base64url(crypto.getRandomValues(new Uint8Array(24)));
96
+ }
97
+
98
+ export interface PkcePair {
99
+ verifier: string;
100
+ challenge: string;
101
+ }
102
+
103
+ export async function generatePkce(): Promise<PkcePair> {
104
+ const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
105
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
106
+ return { verifier, challenge: base64url(new Uint8Array(digest)) };
107
+ }
108
+
109
+ export function buildAuthorizationUrl(
110
+ metadata: OidcMetadata,
111
+ params: {
112
+ clientId: string;
113
+ redirectUri: string;
114
+ scopes: string;
115
+ state: string;
116
+ nonce: string;
117
+ codeChallenge: string;
118
+ },
119
+ ): string {
120
+ const url = new URL(metadata.authorization_endpoint);
121
+ url.searchParams.set("response_type", "code");
122
+ url.searchParams.set("client_id", params.clientId);
123
+ url.searchParams.set("redirect_uri", params.redirectUri);
124
+ url.searchParams.set("scope", params.scopes);
125
+ url.searchParams.set("state", params.state);
126
+ url.searchParams.set("nonce", params.nonce);
127
+ url.searchParams.set("code_challenge", params.codeChallenge);
128
+ url.searchParams.set("code_challenge_method", "S256");
129
+ return url.toString();
130
+ }
131
+
132
+ export interface OidcTokenResponse {
133
+ id_token: string;
134
+ access_token?: string;
135
+ token_type?: string;
136
+ expires_in?: number;
137
+ }
138
+
139
+ export async function exchangeCode(
140
+ metadata: OidcMetadata,
141
+ params: { clientId: string; clientSecret?: string; code: string; redirectUri: string; codeVerifier: string },
142
+ ): Promise<OidcTokenResponse> {
143
+ const body = new URLSearchParams({
144
+ grant_type: "authorization_code",
145
+ code: params.code,
146
+ redirect_uri: params.redirectUri,
147
+ client_id: params.clientId,
148
+ code_verifier: params.codeVerifier,
149
+ });
150
+ const headers: Record<string, string> = { "content-type": "application/x-www-form-urlencoded" };
151
+ if (params.clientSecret) {
152
+ headers.authorization = `Basic ${btoa(`${encodeURIComponent(params.clientId)}:${encodeURIComponent(params.clientSecret)}`)}`;
153
+ }
154
+ let json: unknown;
155
+ try {
156
+ const res = await fetch(metadata.token_endpoint, { method: "POST", headers, body });
157
+ json = await res.json();
158
+ if (!res.ok) {
159
+ const err = (json as { error?: string; error_description?: string }) ?? {};
160
+ throw new Error(`HTTP ${res.status} ${err.error ?? ""} ${err.error_description ?? ""}`.trim());
161
+ }
162
+ } catch (err) {
163
+ throw new OidcError("exchange", `the token endpoint refused the exchange: ${(err as Error).message}`);
164
+ }
165
+ const token = json as Partial<OidcTokenResponse>;
166
+ if (typeof token?.id_token !== "string") {
167
+ throw new OidcError("exchange", "the token response carries no id_token");
168
+ }
169
+ return token as OidcTokenResponse;
170
+ }
171
+
172
+ interface Jwk {
173
+ kty: string;
174
+ kid?: string;
175
+ alg?: string;
176
+ use?: string;
177
+ n?: string;
178
+ e?: string;
179
+ x?: string;
180
+ y?: string;
181
+ crv?: string;
182
+ }
183
+
184
+ interface CachedJwks {
185
+ keys: Jwk[];
186
+ fetchedAt: number;
187
+ }
188
+ const jwksCache = new Map<string, CachedJwks>();
189
+ const JWKS_TTL_MS = 60 * 60 * 1000;
190
+
191
+ async function fetchJwks(jwksUri: string, force: boolean): Promise<Jwk[]> {
192
+ const cached = jwksCache.get(jwksUri);
193
+ if (!force && cached && Date.now() - cached.fetchedAt < JWKS_TTL_MS) return cached.keys;
194
+ let body: unknown;
195
+ try {
196
+ const res = await fetch(jwksUri);
197
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
198
+ body = await res.json();
199
+ } catch (err) {
200
+ throw new OidcError("token_signature", `could not fetch the signing keys: ${(err as Error).message}`);
201
+ }
202
+ const keys = (body as { keys?: Jwk[] })?.keys;
203
+ if (!Array.isArray(keys)) {
204
+ throw new OidcError("token_signature", "the JWKS carries no keys array");
205
+ }
206
+ jwksCache.set(jwksUri, { keys, fetchedAt: Date.now() });
207
+ return keys;
208
+ }
209
+
210
+ export interface OidcIdTokenClaims {
211
+ iss: string;
212
+ sub: string;
213
+ aud: string | string[];
214
+ exp: number;
215
+ iat?: number;
216
+ nonce?: string;
217
+ azp?: string;
218
+ email?: string;
219
+ email_verified?: boolean;
220
+ name?: string;
221
+ roles?: string[];
222
+ groups?: string[];
223
+ org?: unknown;
224
+ [claim: string]: unknown;
225
+ }
226
+
227
+ const EXPIRY_LEEWAY_MS = 60_000;
228
+
229
+ export async function validateIdToken(
230
+ idToken: string,
231
+ expectations: { issuer: string; clientId: string; nonce: string; jwksUri: string },
232
+ ): Promise<OidcIdTokenClaims> {
233
+ const parts = idToken.split(".");
234
+ if (parts.length !== 3) {
235
+ throw new OidcError("token_malformed", "the ID token is not a three-part JWT");
236
+ }
237
+ let header: { alg?: string; kid?: string };
238
+ let claims: OidcIdTokenClaims;
239
+ try {
240
+ header = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[0])));
241
+ claims = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[1]))) as OidcIdTokenClaims;
242
+ } catch {
243
+ throw new OidcError("token_malformed", "the ID token header/claims are not JSON");
244
+ }
245
+ if (header.alg !== "RS256" && header.alg !== "ES256") {
246
+ throw new OidcError("token_alg", `the ID token uses ${header.alg ?? "no declared algorithm"}`);
247
+ }
248
+
249
+ const signedContent = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
250
+ const signature = base64urlDecode(parts[2]) as BufferSource;
251
+ let verified = false;
252
+ for (const force of [false, true]) {
253
+ const keys = await fetchJwks(expectations.jwksUri, force);
254
+ const candidates = keys.filter(
255
+ (k) => (!header.kid || k.kid === header.kid) && (header.alg === "RS256" ? k.kty === "RSA" : k.kty === "EC"),
256
+ );
257
+ for (const jwk of candidates) {
258
+ try {
259
+ const key = await crypto.subtle.importKey(
260
+ "jwk",
261
+ jwk as JsonWebKey,
262
+ header.alg === "RS256" ? { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } : { name: "ECDSA", namedCurve: "P-256" },
263
+ false,
264
+ ["verify"],
265
+ );
266
+ verified = await crypto.subtle.verify(
267
+ header.alg === "RS256" ? { name: "RSASSA-PKCS1-v1_5" } : { name: "ECDSA", hash: "SHA-256" },
268
+ key,
269
+ signature,
270
+ signedContent,
271
+ );
272
+ } catch {
273
+ verified = false;
274
+ }
275
+ if (verified) break;
276
+ }
277
+ if (verified) break;
278
+ }
279
+ if (!verified) {
280
+ throw new OidcError("token_signature", "the ID token signature does not verify against the issuer's published keys");
281
+ }
282
+
283
+ if (claims.iss?.replace(/\/$/, "") !== expectations.issuer.replace(/\/$/, "")) {
284
+ throw new OidcError("token_issuer", "the ID token's issuer is not the configured issuer");
285
+ }
286
+ const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
287
+ if (!audiences.includes(expectations.clientId)) {
288
+ throw new OidcError("token_audience", "the ID token was not issued for this application");
289
+ }
290
+ if (audiences.length > 1 && claims.azp && claims.azp !== expectations.clientId) {
291
+ throw new OidcError("token_audience", "the ID token's authorized party is not this application");
292
+ }
293
+ if (typeof claims.exp !== "number" || claims.exp * 1000 + EXPIRY_LEEWAY_MS < Date.now()) {
294
+ throw new OidcError("token_expired", "the ID token has expired");
295
+ }
296
+ if (claims.nonce !== expectations.nonce) {
297
+ throw new OidcError("token_nonce", "the ID token's nonce does not match the request");
298
+ }
299
+ return claims;
300
+ }
301
+
302
+ export function buildEndSessionUrl(
303
+ metadata: OidcMetadata,
304
+ params: { idTokenHint?: string | null; clientId: string; postLogoutRedirectUri: string },
305
+ ): string | null {
306
+ if (!metadata.end_session_endpoint) return null;
307
+ const url = new URL(metadata.end_session_endpoint);
308
+ if (params.idTokenHint) url.searchParams.set("id_token_hint", params.idTokenHint);
309
+ url.searchParams.set("client_id", params.clientId);
310
+ url.searchParams.set("post_logout_redirect_uri", params.postLogoutRedirectUri);
311
+ return url.toString();
312
+ }
@@ -0,0 +1,45 @@
1
+ // The declarative router (shared by both workers — one HTTP idiom):
2
+ // adding a route = adding an entry, never router modification. Each
3
+ // entry declares its method, path pattern, and handler; matchRoute
4
+ // dispatches by segment-exact match with :param capture and a trailing
5
+ // * wildcard.
6
+
7
+ export interface RouteContext {
8
+ env: any;
9
+ req: Request;
10
+ ctx: ExecutionContext;
11
+ url: URL;
12
+ path: string;
13
+ params: Record<string, string>;
14
+ }
15
+
16
+ export type RouteHandler = (c: RouteContext) => Promise<Response>;
17
+
18
+ export interface Route {
19
+ method: string;
20
+ pattern: string; // "/api/conversations/:id/messages" — :name captures params
21
+ handler: RouteHandler;
22
+ }
23
+
24
+ export function matchRoute(routes: Route[], method: string, path: string): { route: Route; params: Record<string, string> } | null {
25
+ const segments = path.split("/").filter(Boolean);
26
+ for (const route of routes) {
27
+ if (route.method !== method && route.method !== "*") continue;
28
+ const patternSegs = route.pattern.split("/").filter(Boolean);
29
+ if (patternSegs.length !== segments.length && !patternSegs[patternSegs.length - 1]?.startsWith("*")) continue;
30
+ const params: Record<string, string> = {};
31
+ let matched = true;
32
+ for (let i = 0; i < patternSegs.length; i++) {
33
+ const ps = patternSegs[i];
34
+ if (ps.startsWith("*")) break; // wildcard matches rest
35
+ if (ps.startsWith(":")) {
36
+ params[ps.slice(1)] = segments[i];
37
+ } else if (ps !== segments[i]) {
38
+ matched = false;
39
+ break;
40
+ }
41
+ }
42
+ if (matched) return { route, params };
43
+ }
44
+ return null;
45
+ }
@@ -0,0 +1,104 @@
1
+ // RAG's own session layer (the CANONICAL implementation — TODO.impl/16):
2
+ // the OP's ID token is sign-in evidence, never a session
3
+ // (docs/identity-service.md §5.5). We mint an HMAC-signed cookie with the
4
+ // claims we act on; enforcement happens inside each worker. Both workers
5
+ // import THIS module; worker_public/src/session.ts is a compat re-export.
6
+
7
+
8
+ export interface SessionClaims {
9
+ sub: string;
10
+ name?: string;
11
+ email?: string;
12
+ /** OIDC picture claim (avatar URL) — present when the OP issues it */
13
+ picture?: string;
14
+ roles: string[];
15
+ iat: number;
16
+ exp: number;
17
+ }
18
+
19
+ export const SESSION_COOKIE = "rag_session";
20
+ const SESSION_TTL_SEC = 7 * 24 * 3600;
21
+
22
+ async function hmac(secret: string, data: string): Promise<string> {
23
+ const key = await crypto.subtle.importKey(
24
+ "raw",
25
+ new TextEncoder().encode(secret),
26
+ { name: "HMAC", hash: "SHA-256" },
27
+ false,
28
+ ["sign"],
29
+ );
30
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
31
+ return btoa(String.fromCharCode(...new Uint8Array(sig)))
32
+ .replace(/\+/g, "-")
33
+ .replace(/\//g, "_")
34
+ .replace(/=+$/, "");
35
+ }
36
+
37
+ export async function mintSessionToken(secret: string, claims: Omit<SessionClaims, "exp" | "iat">): Promise<{ token: string; expiresAt: number }> {
38
+ const full: SessionClaims = { ...claims, iat: Date.now(), exp: Date.now() + SESSION_TTL_SEC * 1000 };
39
+ const payload = btoa(JSON.stringify(full))
40
+ .replace(/\+/g, "-")
41
+ .replace(/\//g, "_")
42
+ .replace(/=+$/, "");
43
+ const sig = await hmac(secret, payload);
44
+ return { token: `${payload}.${sig}`, expiresAt: full.exp };
45
+ }
46
+
47
+ export async function mintSessionCookie(secret: string, claims: Omit<SessionClaims, "exp" | "iat">): Promise<string> {
48
+ const { token } = await mintSessionToken(secret, claims);
49
+ return sessionCookieFromToken(token);
50
+ }
51
+
52
+ /** The Set-Cookie value for an already-minted session token (the sign-in
53
+ * mints ONCE and both the cookie and the bubble Bearer carry it —
54
+ * TODO.ai-platform/03's live-data window keys off the one token). */
55
+ export function sessionCookieFromToken(token: string): string {
56
+ return `${SESSION_COOKIE}=${token}; Path=/; Max-Age=${SESSION_TTL_SEC}; HttpOnly; Secure; SameSite=Lax`;
57
+ }
58
+
59
+ function parseCookies(req: Request): Record<string, string> {
60
+ const out: Record<string, string> = {};
61
+ const header = req.headers.get("cookie") ?? "";
62
+ for (const part of header.split(";")) {
63
+ const i = part.indexOf("=");
64
+ if (i > 0) out[part.slice(0, i).trim()] = part.slice(i + 1).trim();
65
+ }
66
+ return out;
67
+ }
68
+
69
+ export async function readSession(req: Request, secret: string | undefined): Promise<SessionClaims | null> {
70
+ if (!secret) return null;
71
+ // The cookie is the same-origin posture; the Bearer form is the bubble
72
+ // bridge (bubble.ts) — the same signed payload, sent cross-origin by
73
+ // the embedded panel. Cookie first so a stale stored token never
74
+ // shadows a live cookie session on the ai property itself.
75
+ const raw = rawSessionToken(req);
76
+ if (!raw) return null;
77
+ const [payload, sig] = raw.split(".");
78
+ if (!payload || !sig) return null;
79
+ const expected = await hmac(secret, payload);
80
+ if (sig !== expected) return null;
81
+ try {
82
+ const b64 = payload.replace(/-/g, "+").replace(/_/g, "/") + "===".slice(0, (4 - (payload.length % 4)) % 4);
83
+ const claims = JSON.parse(atob(b64)) as SessionClaims;
84
+ if (typeof claims.sub !== "string" || typeof claims.exp !== "number") return null;
85
+ if (claims.exp + 60_000 < Date.now()) return null;
86
+ return claims;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+
92
+ /** The raw session token as presented (the cookie wins the tie, exactly
93
+ * as readSession) — the live-data window's KV key derives from it
94
+ * (TODO.ai-platform/03; the key is the token's hash, never the token). */
95
+ export function rawSessionToken(req: Request): string | null {
96
+ const raw =
97
+ parseCookies(req)[SESSION_COOKIE] ??
98
+ (req.headers.get("authorization") ?? "").match(/^Bearer\s+(.+)$/i)?.[1]?.trim();
99
+ return raw || null;
100
+ }
101
+
102
+ export function clearSessionCookie(): string {
103
+ return `${SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax`;
104
+ }