@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.
- package/LICENSE +29 -0
- package/README.md +13 -0
- package/dist/admin.d.ts +26 -0
- package/dist/ai.d.ts +6 -0
- package/dist/anchors.d.ts +6 -0
- package/dist/answercache.d.ts +22 -0
- package/dist/ask.d.ts +5 -0
- package/dist/auth.d.ts +12 -0
- package/dist/bubble.d.ts +14 -0
- package/dist/chunk-LLWPT2XV.js +49 -0
- package/dist/chunk-MB74PTRM.js +114 -0
- package/dist/chunk-WOGQM7DJ.js +197 -0
- package/dist/chunk-WWNCWKKC.js +42 -0
- package/dist/completion.d.ts +5 -0
- package/dist/config.d.ts +154 -0
- package/dist/config.js +37 -0
- package/dist/context.d.ts +115 -0
- package/dist/conversations.d.ts +5 -0
- package/dist/drafts.d.ts +129 -0
- package/dist/env.d.ts +57 -0
- package/dist/faithfulness.d.ts +5 -0
- package/dist/grader.d.ts +3 -0
- package/dist/graph.d.ts +13 -0
- package/dist/hybrid.d.ts +7 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5373 -0
- package/dist/internal_gateway.d.ts +14 -0
- package/dist/lexical.d.ts +7 -0
- package/dist/livedata.d.ts +77 -0
- package/dist/memories.d.ts +10 -0
- package/dist/modelplane.d.ts +61 -0
- package/dist/oidc.d.ts +73 -0
- package/dist/pipeline.d.ts +57 -0
- package/dist/profile.d.ts +2 -0
- package/dist/profile.gen.d.ts +70 -0
- package/dist/profile.js +8 -0
- package/dist/projects.d.ts +8 -0
- package/dist/prompts/conversational.md +8 -0
- package/dist/prompts/enrichment.md +3 -0
- package/dist/prompts/faithfulness.md +1 -0
- package/dist/prompts/grader.md +5 -0
- package/dist/prompts/listwise.md +3 -0
- package/dist/prompts/precision.md +1 -0
- package/dist/prompts/reflect.md +1 -0
- package/dist/prompts/relevancy.md +1 -0
- package/dist/prompts/research.md +10 -0
- package/dist/prompts/section-summary.md +5 -0
- package/dist/prompts/summarize.md +1 -0
- package/dist/prompts/system.md +18 -0
- package/dist/prompts/understanding.md +17 -0
- package/dist/quota.d.ts +13 -0
- package/dist/reflect.d.ts +5 -0
- package/dist/refs.d.ts +40 -0
- package/dist/refusal.d.ts +9 -0
- package/dist/refusal.js +9 -0
- package/dist/requestScope.d.ts +26 -0
- package/dist/requestScope.js +10 -0
- package/dist/research.d.ts +8 -0
- package/dist/search.d.ts +4 -0
- package/dist/selfquery.d.ts +7 -0
- package/dist/session.d.ts +1 -0
- package/dist/share.d.ts +2 -0
- package/dist/structural.d.ts +27 -0
- package/dist/tablecontext.d.ts +11 -0
- package/dist/understand.d.ts +11 -0
- package/dist/understandContract.d.ts +29 -0
- package/dist/verdict.d.ts +24 -0
- package/docs/API.md +451 -0
- package/docs/ARCHITECTURE.md +302 -0
- package/docs/AUDIT-2026-08-24.md +71 -0
- package/docs/CONTRIBUTOR-AUDIT-2026-08-25.md +147 -0
- package/docs/INGEST-ARCHITECTURE.md +158 -0
- package/docs/MCP.md +92 -0
- package/docs/METANORMA-AI-SERIALIZATION.md +247 -0
- package/docs/MKO-EXPORT-PIPELINE.md +147 -0
- package/docs/REDESIGN-NORMATIVE-RAG-ETSI.md +485 -0
- package/docs/RESEARCH-SOTA-2026.md +243 -0
- package/docs/ROADMAP-SOTA.md +130 -0
- package/docs/SOTA-STAGE-SPECS.md +509 -0
- package/docs/annealment/F1-verdict.md +27 -0
- package/docs/annealment/F10-notes.md +19 -0
- package/docs/annealment/F11-composition.md +17 -0
- package/docs/annealment/F12-passport.md +17 -0
- package/docs/annealment/F2-counterfactual.md +20 -0
- package/docs/annealment/F3-absence.md +21 -0
- package/docs/annealment/F4-instance.md +18 -0
- package/docs/annealment/F5-workflow.md +21 -0
- package/docs/annealment/F6-impact.md +21 -0
- package/docs/annealment/F7-editions.md +18 -0
- package/docs/annealment/F8-selfverify.md +19 -0
- package/docs/annealment/F9-projection-qa.md +17 -0
- package/docs/annealment/L0-locate.md +19 -0
- package/docs/annealment/L1-extract.md +18 -0
- package/docs/annealment/L2-nomenclature.md +22 -0
- package/docs/annealment/L3-geometry.md +23 -0
- package/docs/annealment/L4-composition.md +21 -0
- package/docs/annealment/L5-cross-standard.md +20 -0
- package/docs/annealment/L6-diachrony.md +21 -0
- package/docs/annealment/L7-perception.md +20 -0
- package/docs/annealment/L8-computation.md +22 -0
- package/docs/annealment/L9-instance-process.md +23 -0
- package/docs/annealment/README.md +10 -0
- package/docs/guidelines-metanorma-ai-programme.md +279 -0
- package/docs/identity-onboarding-rag.md +65 -0
- package/docs/identity-service.md +219 -0
- package/docs/knowledge-annealment.md +273 -0
- package/docs/konneal-extraction-plan.md +481 -0
- package/docs/metanorma-for-ai.md +270 -0
- package/docs/mirror-plan.md +36 -0
- package/docs/multi-sdo-architecture.md +191 -0
- package/docs/paper-annealment-comparison.md +259 -0
- package/docs/paper-assets/architecture.svg +94 -0
- package/docs/paper-assets/contract-v2.svg +94 -0
- package/docs/paper-assets/mko-ingest.svg +91 -0
- package/docs/paper-oiml-bulletin.md +402 -0
- package/docs/paper-oiml-bulletin.mdx +419 -0
- package/docs/product-branding-options.md +172 -0
- package/docs/projects-design.md +88 -0
- package/docs/sota-mechanisms.md +184 -0
- package/docs/spec-api.md +77 -0
- package/docs/spec-pipeline.md +126 -0
- package/docs/vector-adapter.md +88 -0
- package/package.json +70 -0
- package/profile/corpora.yaml +5 -0
- package/profile/datasets.yaml +14 -0
- package/profile/prompts.yaml +5 -0
- package/profile/publisher.yaml +17 -0
- package/profile/retrieval.yaml +1 -0
- package/profile/sources.yaml +5 -0
- package/profile/ui.yaml +7 -0
- package/scripts/gen_profile.mjs +33 -0
- package/workers/shared/ai.ts +21 -0
- package/workers/shared/auth.ts +16 -0
- package/workers/shared/chunk.ts +108 -0
- package/workers/shared/oidc.ts +312 -0
- package/workers/shared/router.ts +45 -0
- package/workers/shared/session.ts +104 -0
- package/workers/worker_internal/src/index.ts +157 -0
- package/workers/worker_internal/tsconfig.json +15 -0
- package/workers/worker_internal/wrangler.toml +32 -0
- package/workers/worker_mcp/src/index.ts +175 -0
- package/workers/worker_mcp/tsconfig.json +13 -0
- package/workers/worker_mcp/wrangler.toml +18 -0
- package/workers/worker_public/migrations/0002_conversations.sql +22 -0
- package/workers/worker_public/migrations/0003_shared_conversations.sql +9 -0
- package/workers/worker_public/migrations/0004_graph.sql +16 -0
- package/workers/worker_public/migrations/0005_documents.sql +19 -0
- package/workers/worker_public/migrations/0006_conversation_entities.sql +11 -0
- package/workers/worker_public/migrations/0007_chunks_fts.sql +43 -0
- package/workers/worker_public/migrations/0008_unit_payloads.sql +15 -0
- package/workers/worker_public/migrations/0009_chunks_unit.sql +8 -0
- package/workers/worker_public/migrations/0009_message_context.sql +7 -0
- package/workers/worker_public/migrations/0010_model_nodes.sql +33 -0
- package/workers/worker_public/migrations/0011_schema_union.sql +38 -0
- package/workers/worker_public/migrations/0012_memories.sql +15 -0
- package/workers/worker_public/migrations/0013_projects.sql +21 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2021-11-03/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.d.ts +16373 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-01-31/index.ts +16328 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.d.ts +16382 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-03-21/index.ts +16337 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.d.ts +16383 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-08-04/index.ts +16338 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.d.ts +16403 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-10-31/index.ts +16358 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.d.ts +16408 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2022-11-30/index.ts +16363 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.d.ts +16414 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-03-01/index.ts +16369 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.d.ts +16414 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/2023-07-01/index.ts +16369 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/README.md +135 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/entrypoints.svg +53 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.d.ts +17095 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/experimental/index.ts +17050 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.d.ts +16447 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/latest/index.ts +16402 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.d.ts +16306 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/oldest/index.ts +16261 -0
- package/workers/worker_public/node_modules/@cloudflare/workers-types/package.json +11 -0
- package/workers/worker_public/package.json +13 -0
- package/workers/worker_public/prompts/conversational.md +8 -0
- package/workers/worker_public/prompts/enrichment.md +3 -0
- package/workers/worker_public/prompts/faithfulness.md +1 -0
- package/workers/worker_public/prompts/grader.md +5 -0
- package/workers/worker_public/prompts/listwise.md +3 -0
- package/workers/worker_public/prompts/precision.md +1 -0
- package/workers/worker_public/prompts/reflect.md +1 -0
- package/workers/worker_public/prompts/relevancy.md +1 -0
- package/workers/worker_public/prompts/research.md +10 -0
- package/workers/worker_public/prompts/section-summary.md +5 -0
- package/workers/worker_public/prompts/summarize.md +1 -0
- package/workers/worker_public/prompts/system.md +18 -0
- package/workers/worker_public/prompts/understanding.md +17 -0
- package/workers/worker_public/public/app.js +166 -0
- package/workers/worker_public/public/index.html +48 -0
- package/workers/worker_public/public/style.css +147 -0
- package/workers/worker_public/schema.sql +248 -0
- package/workers/worker_public/src/admin.ts +358 -0
- package/workers/worker_public/src/ai.ts +71 -0
- package/workers/worker_public/src/anchors.ts +41 -0
- package/workers/worker_public/src/answercache.ts +72 -0
- package/workers/worker_public/src/ask.ts +1094 -0
- package/workers/worker_public/src/auth.ts +252 -0
- package/workers/worker_public/src/bubble.ts +111 -0
- package/workers/worker_public/src/completion.ts +75 -0
- package/workers/worker_public/src/config.ts +238 -0
- package/workers/worker_public/src/context.ts +238 -0
- package/workers/worker_public/src/conversations.ts +162 -0
- package/workers/worker_public/src/drafts.ts +497 -0
- package/workers/worker_public/src/env.ts +90 -0
- package/workers/worker_public/src/faithfulness.ts +63 -0
- package/workers/worker_public/src/grader.ts +89 -0
- package/workers/worker_public/src/graph.ts +63 -0
- package/workers/worker_public/src/hybrid.ts +77 -0
- package/workers/worker_public/src/index.ts +441 -0
- package/workers/worker_public/src/internal_gateway.ts +41 -0
- package/workers/worker_public/src/lexical.ts +86 -0
- package/workers/worker_public/src/lib/hit.ts +4 -0
- package/workers/worker_public/src/lib/http.ts +83 -0
- package/workers/worker_public/src/lib/router.ts +4 -0
- package/workers/worker_public/src/livedata.ts +334 -0
- package/workers/worker_public/src/memories.ts +81 -0
- package/workers/worker_public/src/modelplane.ts +213 -0
- package/workers/worker_public/src/oidc.ts +333 -0
- package/workers/worker_public/src/pipeline.ts +377 -0
- package/workers/worker_public/src/ports/blobs.ts +7 -0
- package/workers/worker_public/src/ports/cloudflare/adapters.ts +177 -0
- package/workers/worker_public/src/ports/kv.ts +8 -0
- package/workers/worker_public/src/ports/model.ts +28 -0
- package/workers/worker_public/src/ports/runtime.ts +13 -0
- package/workers/worker_public/src/ports/store.ts +20 -0
- package/workers/worker_public/src/ports/vector.ts +26 -0
- package/workers/worker_public/src/profile.gen.ts +101 -0
- package/workers/worker_public/src/profile.ts +16 -0
- package/workers/worker_public/src/projects.ts +108 -0
- package/workers/worker_public/src/prompts.d.ts +6 -0
- package/workers/worker_public/src/quota.ts +54 -0
- package/workers/worker_public/src/reflect.ts +67 -0
- package/workers/worker_public/src/refs.ts +107 -0
- package/workers/worker_public/src/refusal.ts +65 -0
- package/workers/worker_public/src/requestScope.ts +71 -0
- package/workers/worker_public/src/research.ts +126 -0
- package/workers/worker_public/src/search.ts +56 -0
- package/workers/worker_public/src/selfquery.ts +25 -0
- package/workers/worker_public/src/session.ts +4 -0
- package/workers/worker_public/src/share.ts +53 -0
- package/workers/worker_public/src/stages/conceptGraph.ts +68 -0
- package/workers/worker_public/src/stages/conceptSteer.ts +39 -0
- package/workers/worker_public/src/stages/corpusScope.ts +25 -0
- package/workers/worker_public/src/stages/dedup.ts +10 -0
- package/workers/worker_public/src/stages/dense.ts +73 -0
- package/workers/worker_public/src/stages/diversity.ts +33 -0
- package/workers/worker_public/src/stages/editionCover.ts +63 -0
- package/workers/worker_public/src/stages/editionSteer.ts +88 -0
- package/workers/worker_public/src/stages/familyBoost.ts +22 -0
- package/workers/worker_public/src/stages/federate.ts +22 -0
- package/workers/worker_public/src/stages/glossary.ts +65 -0
- package/workers/worker_public/src/stages/graphLane.ts +31 -0
- package/workers/worker_public/src/stages/hyde.ts +29 -0
- package/workers/worker_public/src/stages/index.ts +69 -0
- package/workers/worker_public/src/stages/lexicalUnion.ts +21 -0
- package/workers/worker_public/src/stages/multiQuery.ts +57 -0
- package/workers/worker_public/src/stages/overviewDemote.ts +14 -0
- package/workers/worker_public/src/stages/poolOpen.ts +10 -0
- package/workers/worker_public/src/stages/propagate.ts +15 -0
- package/workers/worker_public/src/stages/rerank.ts +47 -0
- package/workers/worker_public/src/stages/seal.ts +16 -0
- package/workers/worker_public/src/stages/sectionDescent.ts +61 -0
- package/workers/worker_public/src/stages/stdRefNudge.ts +35 -0
- package/workers/worker_public/src/stages/subQuery.ts +42 -0
- package/workers/worker_public/src/stages/termNudge.ts +24 -0
- package/workers/worker_public/src/stages/typedPin.ts +131 -0
- package/workers/worker_public/src/stages/types.ts +112 -0
- package/workers/worker_public/src/stages/windowFloor.ts +23 -0
- package/workers/worker_public/src/structural.ts +171 -0
- package/workers/worker_public/src/tablecontext.ts +41 -0
- package/workers/worker_public/src/understand.ts +72 -0
- package/workers/worker_public/src/understandContract.ts +67 -0
- package/workers/worker_public/src/verdict.ts +255 -0
- package/workers/worker_public/tsconfig.json +18 -0
- package/workers/worker_public/wrangler.toml +104 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// The model plane (TODO.ai-platform/05) — the ask path's model-native
|
|
2
|
+
// grounding. The packages' machine content (the requirements' constraints,
|
|
3
|
+
// the applicability rules, the acceptance criteria, the term definitions)
|
|
4
|
+
// indexes alongside the prose corpus; this module is the deterministic
|
|
5
|
+
// half at ask time:
|
|
6
|
+
//
|
|
7
|
+
// 1. BIND — "this requirement" on a model surface (the platform's
|
|
8
|
+
// entity chip) names the model node by its canonical id (the label
|
|
9
|
+
// leads with it, e.g. "this requirement /req/metrological/mpe — …");
|
|
10
|
+
// a question may name one too. The grammar is strict — a node id is
|
|
11
|
+
// a machine shape, never fuzzy-matched.
|
|
12
|
+
// 2. GROUND — the bound node's own content (from the model_nodes D1
|
|
13
|
+
// store, loaded from the smart repo's derived bundles) joins the
|
|
14
|
+
// prompt as a structured grounding block: the constraint (quoted
|
|
15
|
+
// verbatim), the applicability, the acceptance, the provenance, the
|
|
16
|
+
// tests. A DECLARED source discrepancy (the model and the text
|
|
17
|
+
// disagree) rides verbatim — the clause-drift doctrine's posture is
|
|
18
|
+
// structural: the answer must surface it and cite both.
|
|
19
|
+
// 3. ECHO — context_applied.model names the bound node so the panel's
|
|
20
|
+
// honest context line never invents the grounding.
|
|
21
|
+
//
|
|
22
|
+
// The verdict-explanation discipline is the smart repo's (its engine's
|
|
23
|
+
// trace seam — never reimplemented here): this module grounds in the
|
|
24
|
+
// model NODE (the static constraint + clause + acceptance); a live
|
|
25
|
+
// verdict's explanation is the platform's computation, quoted when the
|
|
26
|
+
// platform serves it — this service never recomputes one.
|
|
27
|
+
|
|
28
|
+
/** The strict model-node id grammar: the kind prefix + one or two
|
|
29
|
+
* slug segments (the packages' identifier shapes — /req/<class>/<id>,
|
|
30
|
+
* /conf/<class>/<id>, /term/<id>, /constraint/<id>, /characteristic/<id>,
|
|
31
|
+
* /state-machine/<id>, /dimension/<id>). */
|
|
32
|
+
const NODE_RE = /(?:^|[\s("'`])\/(req|conf|term|constraint|characteristic|state-machine|dimension)\/([a-z0-9][a-z0-9_-]*(?:\/[a-z0-9][a-z0-9_-]*)?)(?=[\s)"'`,;:.]|$)/i;
|
|
33
|
+
|
|
34
|
+
/** The first model-node id a text names (the declared chip label first,
|
|
35
|
+
* the question second — the caller orders). */
|
|
36
|
+
export function modelNodeRefIn(text: string | undefined | null): string | null {
|
|
37
|
+
if (!text) return null;
|
|
38
|
+
const m = text.match(NODE_RE);
|
|
39
|
+
if (!m) return null;
|
|
40
|
+
const id = `/${m[1]!.toLowerCase()}/${m[2]}`;
|
|
41
|
+
return id.length <= 120 ? id : null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The doc scope's publication family → the model plane's standard
|
|
45
|
+
* (doc_number "60" → oiml-r60). Only the four modeled Recommendations
|
|
46
|
+
* carry a plane; anything else resolves null (honest: no model to bind). */
|
|
47
|
+
export function standardForDocNumber(docNumber: string | undefined): string | null {
|
|
48
|
+
if (!docNumber) return null;
|
|
49
|
+
return ["60", "91", "129", "144"].includes(docNumber) ? `oiml-r${docNumber}` : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface BoundModelNode {
|
|
53
|
+
standard: string;
|
|
54
|
+
node_id: string;
|
|
55
|
+
kind: string;
|
|
56
|
+
name: string;
|
|
57
|
+
clause: { doc: string; ref: string; urn: string } | null;
|
|
58
|
+
/** The node's bundle projection (verbatim JSON). */
|
|
59
|
+
content: any;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function fetchNode(env: any, standard: string, nodeId: string): Promise<BoundModelNode | null> {
|
|
63
|
+
try {
|
|
64
|
+
const row = await env.DB.prepare(
|
|
65
|
+
"SELECT standard, node_id, kind, name, clause_doc, clause_ref, content FROM model_nodes WHERE standard = ?1 AND node_id = ?2",
|
|
66
|
+
)
|
|
67
|
+
.bind(standard, nodeId)
|
|
68
|
+
.first();
|
|
69
|
+
if (!row) return null;
|
|
70
|
+
const content = JSON.parse(String(row.content));
|
|
71
|
+
const clause =
|
|
72
|
+
row.clause_doc && row.clause_ref
|
|
73
|
+
? { doc: String(row.clause_doc), ref: String(row.clause_ref), urn: `${row.clause_doc}#clause-${row.clause_ref}` }
|
|
74
|
+
: row.clause_doc
|
|
75
|
+
? { doc: String(row.clause_doc), ref: "", urn: String(row.clause_doc) }
|
|
76
|
+
: null;
|
|
77
|
+
return {
|
|
78
|
+
standard: String(row.standard),
|
|
79
|
+
node_id: String(row.node_id),
|
|
80
|
+
kind: String(row.kind),
|
|
81
|
+
name: String(row.name ?? row.node_id),
|
|
82
|
+
clause,
|
|
83
|
+
content,
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
// the table predates the migration on this deployment, or D1 hiccuped:
|
|
87
|
+
// the model plane degrades honestly (retrieval still carries the model
|
|
88
|
+
// chunks) — never a hard failure of the ask.
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Bind the ask's model node: the declared entity label's id wins (the
|
|
94
|
+
* model-aware chip), then a node id the question names. The standard
|
|
95
|
+
* comes from the declared doc scope when it carries one; without a scope
|
|
96
|
+
* the node binds only when it exists in EXACTLY ONE indexed standard —
|
|
97
|
+
* ambiguity is refused honestly (retrieval still surfaces the chunks). */
|
|
98
|
+
export async function bindModelNode(
|
|
99
|
+
env: any,
|
|
100
|
+
opts: { label?: string; query: string; standard?: string | null },
|
|
101
|
+
): Promise<BoundModelNode | null> {
|
|
102
|
+
const nodeId = modelNodeRefIn(opts.label) ?? modelNodeRefIn(opts.query);
|
|
103
|
+
if (!nodeId) return null;
|
|
104
|
+
if (opts.standard) return fetchNode(env, opts.standard, nodeId);
|
|
105
|
+
try {
|
|
106
|
+
const rows = await env.DB.prepare("SELECT standard FROM model_nodes WHERE node_id = ?1 LIMIT 2").bind(nodeId).all();
|
|
107
|
+
const standards = (rows?.results ?? []).map((r: any) => String(r.standard));
|
|
108
|
+
if (standards.length === 1) return fetchNode(env, standards[0]!, nodeId);
|
|
109
|
+
return null; // zero (not indexed) or ambiguous (several standards) — no silent pick
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── the grounding block (the node's own content, composed) ───────────
|
|
116
|
+
|
|
117
|
+
function clip(s: unknown, n = 500): string {
|
|
118
|
+
const t = String(s ?? "").trim();
|
|
119
|
+
return t.length <= n ? t : t.slice(0, n - 1).trimEnd() + " …";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function applicabilityText(app: any): string {
|
|
123
|
+
if (!app || typeof app !== "object") return "";
|
|
124
|
+
const parts: string[] = [];
|
|
125
|
+
for (const [dim, cond] of Object.entries(app)) {
|
|
126
|
+
if (Array.isArray(cond)) parts.push(`${dim.replace(/_/g, " ")}: ${cond.join(", ")}`);
|
|
127
|
+
else if (cond && typeof cond === "object" && Array.isArray((cond as any).values)) {
|
|
128
|
+
parts.push(`${dim.replace(/_/g, " ")} (${(cond as any).match ?? "any"}): ${(cond as any).values.join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return parts.join("; ");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The structured grounding block for the prompt — every line is the
|
|
135
|
+
* node's own declared content (the bundle projection), never a model
|
|
136
|
+
* paraphrase. The discrepancy block, when the model declares one, is the
|
|
137
|
+
* model/prose disagreement posture made structural. */
|
|
138
|
+
export function modelGroundingBlock(node: BoundModelNode): string {
|
|
139
|
+
const c = node.content ?? {};
|
|
140
|
+
const lines: string[] = [];
|
|
141
|
+
lines.push(
|
|
142
|
+
`Model grounding — the OIML SMART model plane (the platform's machine-readable Recommendation model, derived from the Primmel packages, the models' single source of truth):`,
|
|
143
|
+
);
|
|
144
|
+
lines.push(`Node: ${node.node_id} (${node.kind.replace(/_/g, " ")}) — ${node.name} [${node.standard}]`);
|
|
145
|
+
if (node.clause) lines.push(`Provenance: ${node.clause.urn}`);
|
|
146
|
+
if (c.statement) lines.push(`Statement: ${clip(c.statement)}`);
|
|
147
|
+
if (c.definition) lines.push(`Definition: ${clip(c.definition)}`);
|
|
148
|
+
if (c.purpose) lines.push(`Purpose: ${clip(c.purpose)}`);
|
|
149
|
+
const limit = c.limit ?? {};
|
|
150
|
+
if (limit.expression) lines.push(`Machine limit (the constraint the platform's verdict engine evaluates — quote it verbatim): ${limit.expression}`);
|
|
151
|
+
if (limit.accepts?.verdict) lines.push(`Machine limit: ${limit.accepts.verdict} ${limit.accepts.op} ${limit.accepts.limit} (the canonical acceptance chain)`);
|
|
152
|
+
if (c.check) lines.push(`Machine check: ${c.check}`);
|
|
153
|
+
if (c.derive) lines.push(`Derivation: ${c.derive}${Array.isArray(c.inputs) ? ` (inputs: ${c.inputs.join(", ")})` : ""}`);
|
|
154
|
+
const app = applicabilityText(c.applicability);
|
|
155
|
+
const scopeApp = applicabilityText(c.scope_applicability);
|
|
156
|
+
if (app || scopeApp) lines.push(`Applicability: ${[scopeApp, app].filter(Boolean).join("; ")}`);
|
|
157
|
+
if (Array.isArray(c.binds_to) && c.binds_to.length) lines.push(`Binds to: ${c.binds_to.join(", ")}`);
|
|
158
|
+
if (Array.isArray(c.targets) && c.targets.length) lines.push(`Verifies requirements: ${c.targets.join(", ")}`);
|
|
159
|
+
if (Array.isArray(c.preconditions) && c.preconditions.length) {
|
|
160
|
+
const pcs = c.preconditions.map((p: any) => `${p.id}: ${clip(p.check ?? (p.state ? `state = ${p.state}` : ""), 120)}`).join("; ");
|
|
161
|
+
lines.push(`Run-validity preconditions (a violation voids the run — invalid, never a fail): ${pcs}`);
|
|
162
|
+
}
|
|
163
|
+
if (c.acceptance_criteria?.description) lines.push(`Acceptance: ${clip(c.acceptance_criteria.description, 300)}`);
|
|
164
|
+
if (c.violation_meaning) lines.push(`Violation meaning (verbatim): ${clip(c.violation_meaning, 300)} — on violation: ${c.on_violation ?? "invalid"}`);
|
|
165
|
+
if (Array.isArray(c.values) && c.values.length) {
|
|
166
|
+
lines.push(`Values: ${c.values.map((v: any) => `${v.id}${v.implies?.length ? ` (implies ${v.implies.join(", ")})` : ""}`).join("; ")}`);
|
|
167
|
+
}
|
|
168
|
+
if (c.source_discrepancy) {
|
|
169
|
+
const sd = c.source_discrepancy;
|
|
170
|
+
lines.push(
|
|
171
|
+
`DECLARED SOURCE DISCREPANCY — the model and the text disagree; you MUST surface this and cite both: ${clip(sd.summary, 300)} ` +
|
|
172
|
+
`Sources: ${(sd.sources ?? []).join(" and ")}. The model ${sd.resolution === "follows_clause_x" ? "follows one side" : "records the conflict without resolving it"}: ${clip(sd.rationale, 300)}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
lines.push(
|
|
176
|
+
"Rules for this answer: the machine facts (the constraint, the applicability, the acceptance, the provenance) come from THIS node — quote the machine limit verbatim, never invent one the node does not carry. If this model content and a prose passage disagree — including a passage from a different edition — say so explicitly and cite both (this node and the prose clause).",
|
|
177
|
+
);
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The citation the panel renders for the bound node (the model plane is
|
|
182
|
+
* a first-class corpus: the citation names the node + its clause). */
|
|
183
|
+
export function modelCitation(node: BoundModelNode) {
|
|
184
|
+
return {
|
|
185
|
+
doc_id: `model:${node.standard}`,
|
|
186
|
+
docidentifier: `OIML SMART model (${node.standard.replace(/^oiml-r/, "R ")})`,
|
|
187
|
+
edition: "",
|
|
188
|
+
language: "en",
|
|
189
|
+
clause_anchor: node.clause?.ref || "model",
|
|
190
|
+
clause_title: `${node.kind.replace(/_/g, " ")} — ${node.name} (${node.node_id})`,
|
|
191
|
+
status: "in-force",
|
|
192
|
+
corpus: "smart-model",
|
|
193
|
+
url: undefined,
|
|
194
|
+
snippet: `${node.node_id}${node.clause ? ` · ${node.clause.urn}` : ""}${node.content?.statement ? ` — ${clip(node.content.statement, 240)}` : ""}`,
|
|
195
|
+
score: 1,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** The context_applied echo's model block — the honest context line's
|
|
200
|
+
* grounding record. */
|
|
201
|
+
export function modelEcho(node: BoundModelNode) {
|
|
202
|
+
return {
|
|
203
|
+
node_id: node.node_id.slice(0, 120),
|
|
204
|
+
kind: node.kind.slice(0, 40),
|
|
205
|
+
standard: node.standard.slice(0, 40),
|
|
206
|
+
...(node.clause?.urn ? { clause: node.clause.urn.slice(0, 120) } : {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The per-corpus guidance note (config.ts's DATASETS pattern — every
|
|
211
|
+
* retrieved model-plane chunk carries it, chip or no chip). */
|
|
212
|
+
export const MODEL_CORPUS_NOTE =
|
|
213
|
+
"Some passages are the OIML SMART model plane (labeled OIML SMART model) — the platform's machine-readable Recommendation models derived from the Primmel packages. Treat their machine limits, applicability rules and acceptance criteria as the model's own statement of them (quote machine limits verbatim); where a model passage and a prose passage disagree, say so explicitly and cite both.";
|
|
@@ -0,0 +1,333 @@
|
|
|
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
|
+
/** UserInfo (the OP declares the endpoint): the ID token carries the
|
|
44
|
+
* profile contract (name/email + policy families) — `picture` rides
|
|
45
|
+
* userinfo. Returns {} on any failure (the picture is a nicety, never
|
|
46
|
+
* a gate); the CALLER must check `sub` matches the ID token's. */
|
|
47
|
+
export async function fetchUserinfo(meta: OidcMetadata, accessToken: string): Promise<Record<string, unknown>> {
|
|
48
|
+
if (!meta.userinfo_endpoint) return {};
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(meta.userinfo_endpoint, { headers: { authorization: `Bearer ${accessToken}` } });
|
|
51
|
+
if (!res.ok) return {};
|
|
52
|
+
const claims: unknown = await res.json();
|
|
53
|
+
return claims && typeof claims === "object" ? (claims as Record<string, unknown>) : {};
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function discoverIssuer(issuer: string): Promise<OidcMetadata> {
|
|
60
|
+
const cached = metadataCache.get(issuer);
|
|
61
|
+
if (cached && Date.now() - cached.fetchedAt < METADATA_TTL_MS) return cached.metadata;
|
|
62
|
+
|
|
63
|
+
const wellKnown = `${issuer.replace(/\/$/, "")}/.well-known/openid-configuration`;
|
|
64
|
+
let body: unknown;
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(wellKnown);
|
|
67
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
68
|
+
body = await res.json();
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw new OidcError("discovery", `could not fetch ${wellKnown}: ${(err as Error).message}`);
|
|
71
|
+
}
|
|
72
|
+
const meta = body as Partial<OidcMetadata>;
|
|
73
|
+
if (
|
|
74
|
+
typeof meta?.issuer !== "string" ||
|
|
75
|
+
typeof meta?.authorization_endpoint !== "string" ||
|
|
76
|
+
typeof meta?.token_endpoint !== "string" ||
|
|
77
|
+
typeof meta?.jwks_uri !== "string"
|
|
78
|
+
) {
|
|
79
|
+
throw new OidcError("discovery", `the metadata at ${wellKnown} is incomplete`);
|
|
80
|
+
}
|
|
81
|
+
if (meta.issuer.replace(/\/$/, "") !== issuer.replace(/\/$/, "")) {
|
|
82
|
+
throw new OidcError("issuer_mismatch", `the metadata declares issuer ${meta.issuer}, not ${issuer}`);
|
|
83
|
+
}
|
|
84
|
+
const metadata: OidcMetadata = {
|
|
85
|
+
issuer: meta.issuer,
|
|
86
|
+
authorization_endpoint: meta.authorization_endpoint,
|
|
87
|
+
token_endpoint: meta.token_endpoint,
|
|
88
|
+
jwks_uri: meta.jwks_uri,
|
|
89
|
+
...(typeof meta.end_session_endpoint === "string" ? { end_session_endpoint: meta.end_session_endpoint } : {}),
|
|
90
|
+
...(typeof meta.userinfo_endpoint === "string" ? { userinfo_endpoint: meta.userinfo_endpoint } : {}),
|
|
91
|
+
};
|
|
92
|
+
metadataCache.set(issuer, { metadata, fetchedAt: Date.now() });
|
|
93
|
+
return metadata;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function base64url(bytes: Uint8Array): string {
|
|
97
|
+
let bin = "";
|
|
98
|
+
for (const b of bytes) bin += String.fromCharCode(b);
|
|
99
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function base64urlDecode(s: string): Uint8Array {
|
|
103
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice(0, (4 - (s.length % 4)) % 4);
|
|
104
|
+
const bin = atob(b64);
|
|
105
|
+
const out = new Uint8Array(bin.length);
|
|
106
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function randomToken(): string {
|
|
111
|
+
return base64url(crypto.getRandomValues(new Uint8Array(24)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface PkcePair {
|
|
115
|
+
verifier: string;
|
|
116
|
+
challenge: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function generatePkce(): Promise<PkcePair> {
|
|
120
|
+
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
|
|
121
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier));
|
|
122
|
+
return { verifier, challenge: base64url(new Uint8Array(digest)) };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildAuthorizationUrl(
|
|
126
|
+
metadata: OidcMetadata,
|
|
127
|
+
params: {
|
|
128
|
+
clientId: string;
|
|
129
|
+
redirectUri: string;
|
|
130
|
+
scopes: string;
|
|
131
|
+
state: string;
|
|
132
|
+
nonce: string;
|
|
133
|
+
codeChallenge: string;
|
|
134
|
+
},
|
|
135
|
+
): string {
|
|
136
|
+
const url = new URL(metadata.authorization_endpoint);
|
|
137
|
+
url.searchParams.set("response_type", "code");
|
|
138
|
+
url.searchParams.set("client_id", params.clientId);
|
|
139
|
+
url.searchParams.set("redirect_uri", params.redirectUri);
|
|
140
|
+
url.searchParams.set("scope", params.scopes);
|
|
141
|
+
url.searchParams.set("state", params.state);
|
|
142
|
+
url.searchParams.set("nonce", params.nonce);
|
|
143
|
+
url.searchParams.set("code_challenge", params.codeChallenge);
|
|
144
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
145
|
+
return url.toString();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface OidcTokenResponse {
|
|
149
|
+
id_token: string;
|
|
150
|
+
access_token?: string;
|
|
151
|
+
token_type?: string;
|
|
152
|
+
expires_in?: number;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function exchangeCode(
|
|
156
|
+
metadata: OidcMetadata,
|
|
157
|
+
params: { clientId: string; clientSecret?: string; code: string; redirectUri: string; codeVerifier: string },
|
|
158
|
+
): Promise<OidcTokenResponse> {
|
|
159
|
+
const body = new URLSearchParams({
|
|
160
|
+
grant_type: "authorization_code",
|
|
161
|
+
code: params.code,
|
|
162
|
+
redirect_uri: params.redirectUri,
|
|
163
|
+
client_id: params.clientId,
|
|
164
|
+
code_verifier: params.codeVerifier,
|
|
165
|
+
});
|
|
166
|
+
const headers: Record<string, string> = { "content-type": "application/x-www-form-urlencoded" };
|
|
167
|
+
// The OP's WAF classifies Origin-less POSTs as cross-site form
|
|
168
|
+
// submissions and rejects them with 403. The backchannel is
|
|
169
|
+
// server-to-server (PKCE + client auth are the real security), so we
|
|
170
|
+
// present the issuer's own origin to pass its same-origin check.
|
|
171
|
+
headers.origin = new URL(metadata.token_endpoint).origin;
|
|
172
|
+
if (params.clientSecret) {
|
|
173
|
+
headers.authorization = `Basic ${btoa(`${encodeURIComponent(params.clientId)}:${encodeURIComponent(params.clientSecret)}`)}`;
|
|
174
|
+
}
|
|
175
|
+
let json: unknown;
|
|
176
|
+
try {
|
|
177
|
+
const res = await fetch(metadata.token_endpoint, { method: "POST", headers, body });
|
|
178
|
+
json = await res.json();
|
|
179
|
+
if (!res.ok) {
|
|
180
|
+
const err = (json as { error?: string; error_description?: string }) ?? {};
|
|
181
|
+
throw new Error(`HTTP ${res.status} ${err.error ?? ""} ${err.error_description ?? ""}`.trim());
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
throw new OidcError("exchange", `the token endpoint refused the exchange: ${(err as Error).message}`);
|
|
185
|
+
}
|
|
186
|
+
const token = json as Partial<OidcTokenResponse>;
|
|
187
|
+
if (typeof token?.id_token !== "string") {
|
|
188
|
+
throw new OidcError("exchange", "the token response carries no id_token");
|
|
189
|
+
}
|
|
190
|
+
return token as OidcTokenResponse;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
interface Jwk {
|
|
194
|
+
kty: string;
|
|
195
|
+
kid?: string;
|
|
196
|
+
alg?: string;
|
|
197
|
+
use?: string;
|
|
198
|
+
n?: string;
|
|
199
|
+
e?: string;
|
|
200
|
+
x?: string;
|
|
201
|
+
y?: string;
|
|
202
|
+
crv?: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
interface CachedJwks {
|
|
206
|
+
keys: Jwk[];
|
|
207
|
+
fetchedAt: number;
|
|
208
|
+
}
|
|
209
|
+
const jwksCache = new Map<string, CachedJwks>();
|
|
210
|
+
const JWKS_TTL_MS = 60 * 60 * 1000;
|
|
211
|
+
|
|
212
|
+
async function fetchJwks(jwksUri: string, force: boolean): Promise<Jwk[]> {
|
|
213
|
+
const cached = jwksCache.get(jwksUri);
|
|
214
|
+
if (!force && cached && Date.now() - cached.fetchedAt < JWKS_TTL_MS) return cached.keys;
|
|
215
|
+
let body: unknown;
|
|
216
|
+
try {
|
|
217
|
+
const res = await fetch(jwksUri);
|
|
218
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
219
|
+
body = await res.json();
|
|
220
|
+
} catch (err) {
|
|
221
|
+
throw new OidcError("token_signature", `could not fetch the signing keys: ${(err as Error).message}`);
|
|
222
|
+
}
|
|
223
|
+
const keys = (body as { keys?: Jwk[] })?.keys;
|
|
224
|
+
if (!Array.isArray(keys)) {
|
|
225
|
+
throw new OidcError("token_signature", "the JWKS carries no keys array");
|
|
226
|
+
}
|
|
227
|
+
jwksCache.set(jwksUri, { keys, fetchedAt: Date.now() });
|
|
228
|
+
return keys;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface OidcIdTokenClaims {
|
|
232
|
+
iss: string;
|
|
233
|
+
sub: string;
|
|
234
|
+
aud: string | string[];
|
|
235
|
+
exp: number;
|
|
236
|
+
iat?: number;
|
|
237
|
+
nonce?: string;
|
|
238
|
+
azp?: string;
|
|
239
|
+
email?: string;
|
|
240
|
+
email_verified?: boolean;
|
|
241
|
+
name?: string;
|
|
242
|
+
roles?: string[];
|
|
243
|
+
groups?: string[];
|
|
244
|
+
org?: unknown;
|
|
245
|
+
[claim: string]: unknown;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const EXPIRY_LEEWAY_MS = 60_000;
|
|
249
|
+
|
|
250
|
+
export async function validateIdToken(
|
|
251
|
+
idToken: string,
|
|
252
|
+
expectations: { issuer: string; clientId: string; nonce: string; jwksUri: string },
|
|
253
|
+
): Promise<OidcIdTokenClaims> {
|
|
254
|
+
const parts = idToken.split(".");
|
|
255
|
+
if (parts.length !== 3) {
|
|
256
|
+
throw new OidcError("token_malformed", "the ID token is not a three-part JWT");
|
|
257
|
+
}
|
|
258
|
+
let header: { alg?: string; kid?: string };
|
|
259
|
+
let claims: OidcIdTokenClaims;
|
|
260
|
+
try {
|
|
261
|
+
header = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[0])));
|
|
262
|
+
claims = JSON.parse(new TextDecoder().decode(base64urlDecode(parts[1]))) as OidcIdTokenClaims;
|
|
263
|
+
} catch {
|
|
264
|
+
throw new OidcError("token_malformed", "the ID token header/claims are not JSON");
|
|
265
|
+
}
|
|
266
|
+
if (header.alg !== "RS256" && header.alg !== "ES256") {
|
|
267
|
+
throw new OidcError("token_alg", `the ID token uses ${header.alg ?? "no declared algorithm"}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const signedContent = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
|
|
271
|
+
const signature = base64urlDecode(parts[2]) as BufferSource;
|
|
272
|
+
let verified = false;
|
|
273
|
+
for (const force of [false, true]) {
|
|
274
|
+
const keys = await fetchJwks(expectations.jwksUri, force);
|
|
275
|
+
const candidates = keys.filter(
|
|
276
|
+
(k) => (!header.kid || k.kid === header.kid) && (header.alg === "RS256" ? k.kty === "RSA" : k.kty === "EC"),
|
|
277
|
+
);
|
|
278
|
+
for (const jwk of candidates) {
|
|
279
|
+
try {
|
|
280
|
+
const key = await crypto.subtle.importKey(
|
|
281
|
+
"jwk",
|
|
282
|
+
jwk as JsonWebKey,
|
|
283
|
+
header.alg === "RS256" ? { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" } : { name: "ECDSA", namedCurve: "P-256" },
|
|
284
|
+
false,
|
|
285
|
+
["verify"],
|
|
286
|
+
);
|
|
287
|
+
verified = await crypto.subtle.verify(
|
|
288
|
+
header.alg === "RS256" ? { name: "RSASSA-PKCS1-v1_5" } : { name: "ECDSA", hash: "SHA-256" },
|
|
289
|
+
key,
|
|
290
|
+
signature,
|
|
291
|
+
signedContent,
|
|
292
|
+
);
|
|
293
|
+
} catch {
|
|
294
|
+
verified = false;
|
|
295
|
+
}
|
|
296
|
+
if (verified) break;
|
|
297
|
+
}
|
|
298
|
+
if (verified) break;
|
|
299
|
+
}
|
|
300
|
+
if (!verified) {
|
|
301
|
+
throw new OidcError("token_signature", "the ID token signature does not verify against the issuer's published keys");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (claims.iss?.replace(/\/$/, "") !== expectations.issuer.replace(/\/$/, "")) {
|
|
305
|
+
throw new OidcError("token_issuer", "the ID token's issuer is not the configured issuer");
|
|
306
|
+
}
|
|
307
|
+
const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
|
|
308
|
+
if (!audiences.includes(expectations.clientId)) {
|
|
309
|
+
throw new OidcError("token_audience", "the ID token was not issued for this application");
|
|
310
|
+
}
|
|
311
|
+
if (audiences.length > 1 && claims.azp && claims.azp !== expectations.clientId) {
|
|
312
|
+
throw new OidcError("token_audience", "the ID token's authorized party is not this application");
|
|
313
|
+
}
|
|
314
|
+
if (typeof claims.exp !== "number" || claims.exp * 1000 + EXPIRY_LEEWAY_MS < Date.now()) {
|
|
315
|
+
throw new OidcError("token_expired", "the ID token has expired");
|
|
316
|
+
}
|
|
317
|
+
if (claims.nonce !== expectations.nonce) {
|
|
318
|
+
throw new OidcError("token_nonce", "the ID token's nonce does not match the request");
|
|
319
|
+
}
|
|
320
|
+
return claims;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function buildEndSessionUrl(
|
|
324
|
+
metadata: OidcMetadata,
|
|
325
|
+
params: { idTokenHint?: string | null; clientId: string; postLogoutRedirectUri: string },
|
|
326
|
+
): string | null {
|
|
327
|
+
if (!metadata.end_session_endpoint) return null;
|
|
328
|
+
const url = new URL(metadata.end_session_endpoint);
|
|
329
|
+
if (params.idTokenHint) url.searchParams.set("id_token_hint", params.idTokenHint);
|
|
330
|
+
url.searchParams.set("client_id", params.clientId);
|
|
331
|
+
url.searchParams.set("post_logout_redirect_uri", params.postLogoutRedirectUri);
|
|
332
|
+
return url.toString();
|
|
333
|
+
}
|