@konneal/engine 0.2.5 → 0.2.7

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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Konneal Engine
2
2
 
3
+ **New here?** Read [docs/ADOPTION.md](docs/ADOPTION.md) — adopt Konneal for your own standards and your own identity service.
4
+
3
5
  The publisher-agnostic serving layer for standards intelligence: hybrid
4
6
  retrieval over clause-structured corpora, citation-grounded answers,
5
7
  typed tables/formulas/figures, conformance checking by execution, and
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-7K5XP4PB.js";
4
- import "./chunk-GCL73ZLT.js";
3
+ } from "./chunk-LWEVSMH6.js";
4
+ import "./chunk-7UT76RQO.js";
5
5
  import "./chunk-EFQALN2Z.js";
6
6
  import "./chunk-DBBGOOMZ.js";
7
7
  import "./chunk-5MBWE7WD.js";
@@ -2294,6 +2294,7 @@ function buildAuthorizationUrl(metadata, params) {
2294
2294
  url.searchParams.set("nonce", params.nonce);
2295
2295
  url.searchParams.set("code_challenge", params.codeChallenge);
2296
2296
  url.searchParams.set("code_challenge_method", "S256");
2297
+ if (params.prompt) url.searchParams.set("prompt", params.prompt);
2297
2298
  return url.toString();
2298
2299
  }
2299
2300
  async function exchangeCode(metadata, params) {
@@ -2473,13 +2474,19 @@ async function handleLogin(env, req) {
2473
2474
  expirationTtl: 600
2474
2475
  }
2475
2476
  );
2477
+ const silent = url0.searchParams.get("prompt") === "none";
2476
2478
  const url = buildAuthorizationUrl(meta, {
2477
2479
  clientId: cfg.clientId,
2478
2480
  redirectUri: cfg.redirectUri,
2479
2481
  scopes: "openid profile email roles",
2480
2482
  state,
2481
2483
  nonce,
2482
- codeChallenge: pkce.challenge
2484
+ codeChallenge: pkce.challenge,
2485
+ // silent SSO: the OP answers from its existing session or errors
2486
+ // login_required — the callback then lands quietly, signed in or
2487
+ // still anonymous, and the estate session carries to this site
2488
+ // without a click
2489
+ ...silent ? { prompt: "none" } : {}
2483
2490
  });
2484
2491
  return new Response(null, { status: 302, headers: { location: url } });
2485
2492
  } catch (e) {
@@ -2491,6 +2498,9 @@ async function handleCallback(env, req) {
2491
2498
  if (!cfg) return redirectWithError("not_configured");
2492
2499
  const url = new URL(req.url);
2493
2500
  const opError = url.searchParams.get("error");
2501
+ if (opError === "login_required") {
2502
+ return new Response(null, { status: 302, headers: { location: "/?auth_silent=none" } });
2503
+ }
2494
2504
  if (opError) {
2495
2505
  const msg = opError === "access_denied" ? "Sign-in was cancelled." : opError === "temporarily_unavailable" ? "The sign-in service is busy. Please try again in a moment." : "The sign-in service reported a problem. Please try again.";
2496
2506
  return new Response(null, {
@@ -27,7 +27,7 @@ import {
27
27
  syntheticUnderstanding,
28
28
  telemetry,
29
29
  understandQuery
30
- } from "./chunk-GCL73ZLT.js";
30
+ } from "./chunk-7UT76RQO.js";
31
31
  import {
32
32
  corsHeaders,
33
33
  err,
@@ -7,7 +7,7 @@ import {
7
7
  sessionFrom,
8
8
  telemetry,
9
9
  understandQuery
10
- } from "./chunk-GCL73ZLT.js";
10
+ } from "./chunk-7UT76RQO.js";
11
11
  import {
12
12
  corsHeaders,
13
13
  err,
package/dist/oidc.d.ts CHANGED
@@ -30,6 +30,9 @@ export declare function buildAuthorizationUrl(metadata: OidcMetadata, params: {
30
30
  state: string;
31
31
  nonce: string;
32
32
  codeChallenge: string;
33
+ /** OIDC prompt ('none' = silent: the OP answers from its session or
34
+ * errors login_required — never an interaction). */
35
+ prompt?: string;
33
36
  }): string;
34
37
  export interface OidcTokenResponse {
35
38
  id_token: string;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleSearch
3
- } from "./chunk-NRC4FBDQ.js";
4
- import "./chunk-GCL73ZLT.js";
3
+ } from "./chunk-YFDWKXEB.js";
4
+ import "./chunk-7UT76RQO.js";
5
5
  import "./chunk-EFQALN2Z.js";
6
6
  import "./chunk-DBBGOOMZ.js";
7
7
  import "./chunk-5MBWE7WD.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  handleSearch
3
- } from "../../chunk-NRC4FBDQ.js";
3
+ } from "../../chunk-YFDWKXEB.js";
4
4
  import {
5
5
  checkQuoteAnchors,
6
6
  handleAsk,
7
7
  handleMemories,
8
8
  scoreJudge,
9
9
  standardForDocNumber
10
- } from "../../chunk-7K5XP4PB.js";
10
+ } from "../../chunk-LWEVSMH6.js";
11
11
  import {
12
12
  buildMessages,
13
13
  citations,
@@ -29,7 +29,7 @@ import {
29
29
  sessionFrom,
30
30
  telemetry,
31
31
  understandQuery
32
- } from "../../chunk-GCL73ZLT.js";
32
+ } from "../../chunk-7UT76RQO.js";
33
33
  import {
34
34
  authenticate,
35
35
  corsHeaders,
@@ -979,7 +979,7 @@ async function handleMcp(env, ctx, req, tier, key) {
979
979
  // stream:false forces the JSON lane (anon defaults to SSE)
980
980
  body: JSON.stringify({ ...args, stream: false })
981
981
  });
982
- const res = name === "ask" ? await (await import("../../ask-RX3W5JJW.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-OXHXXT5S.js")).handleSearch(env, ctx, inner, tier, key);
982
+ const res = name === "ask" ? await (await import("../../ask-6V6AJG2V.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-RFKT7ZZY.js")).handleSearch(env, ctx, inner, tier, key);
983
983
  return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
984
984
  });
985
985
  if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
@@ -1585,9 +1585,46 @@ async function researchRoute(c) {
1585
1585
  const session = c.env.SESSION_SECRET ? await sessionFrom(c.req, c.env) : null;
1586
1586
  return handleResearch(c.env, c.ctx, c.req, session);
1587
1587
  }
1588
+ function permissionsCatalog() {
1589
+ const groups = [];
1590
+ const perms = [];
1591
+ for (const d of P().datasets ?? []) {
1592
+ if (d.session && d.permission) {
1593
+ perms.push({
1594
+ id: "ai.dataset.externally-licensed",
1595
+ description: `Access the ${d.label} dataset (federated, externally licensed content)`
1596
+ });
1597
+ }
1598
+ }
1599
+ if (P().publisher.features?.drafts) {
1600
+ perms.push({ id: "ai.drafts", description: "Ask the assistant to prepare draft acts" });
1601
+ }
1602
+ perms.push({ id: "ai.memories", description: "Personalized memory files on the assistant" });
1603
+ perms.push({ id: "ai.research", description: "Deep-research multi-pass questions" });
1604
+ perms.push({ id: "ai.keys.admin", description: "Create and revoke API keys" });
1605
+ groups.push({ id: "ai", description: `${P().publisher.product_name} gated capabilities`, permissions: perms });
1606
+ return {
1607
+ version: 1,
1608
+ verbs: ["read", "write", "admin"],
1609
+ groups
1610
+ };
1611
+ }
1612
+ async function openapiCatalogRoute(c) {
1613
+ return json(
1614
+ {
1615
+ openapi: "3.1.0",
1616
+ info: { title: `${P().publisher.product_name} API`, version: c.env.INDEX_VERSION ?? "0" },
1617
+ paths: {},
1618
+ "x-oiml-permissions-catalog": permissionsCatalog()
1619
+ },
1620
+ 200,
1621
+ corsHeaders(c.req)
1622
+ );
1623
+ }
1588
1624
  var INFRA_ROUTES = [
1589
1625
  { method: "GET", pattern: "/", handler: serveIndexPage },
1590
1626
  { method: "GET", pattern: "/api/", handler: serveIndexPage },
1627
+ { method: "GET", pattern: "/api/openapi.json", handler: openapiCatalogRoute },
1591
1628
  { method: "GET", pattern: "/index.html", handler: serveIndexPage },
1592
1629
  { method: "GET", pattern: "/assets/*", handler: unitAssetRoute },
1593
1630
  { method: "GET", pattern: "/docs/*", handler: docsRoute }
@@ -0,0 +1,61 @@
1
+ # Adopting Konneal: your own standards-intelligence site
2
+
3
+ Konneal is a publisher-agnostic engine: clause-cited, verifiable answers
4
+ over your own Metanorma documents and Primmel models — deployed on your
5
+ own infrastructure, with licensed third-party content (IEC, ISO and
6
+ peers) gated behind your identity service.
7
+
8
+ ## Start
9
+
10
+ npm create @konneal/publisher my-sdo
11
+
12
+ The scaffolder interviews you for your publisher's facts — id, name,
13
+ domains, identity issuer, first dataset — and writes the whole
14
+ deployment skeleton: the worker, the site, the profile, the ingest
15
+ pipeline, CI. Every generated file is ordinary reviewable code.
16
+
17
+ Then the four declarations:
18
+
19
+ 1. **Your profile** (`profile/*.yaml`): publisher identity, domains,
20
+ datasets, the corpora registry, the licensed-standards catalog,
21
+ prompt variables, smoke probes. Everything the serving surface knows
22
+ about you is this data, never engine code.
23
+ 2. **Your corpus** (`sources/`): your Metanorma documents. The ingest
24
+ pipeline chunks along the documents' own clause boundaries, embeds,
25
+ and upserts into your indexes.
26
+ 3. **Your models** (Primmel packages): the machine-readable layer —
27
+ requirements, constraints with threshold expressions, severity
28
+ condition sets — so answers can carry server-computed verdicts, not
29
+ paraphrases.
30
+ 4. **Your identity**: any discovery-compliant OIDC provider. Register
31
+ the site (authorization code + PKCE), map roles to datasets and
32
+ groups to licensed keys in the provider's configuration — revoking a
33
+ role removes access on the next request, no deploy.
34
+
35
+ ## Licensed content, gated
36
+
37
+ Licensed standards are fail-closed by construction: each licensed
38
+ package carries a license key; the deployment's profile declares the
39
+ whitelist; a request without an entitled scope never sees the licensed
40
+ text — it is filtered before the model reads anything. An anonymous
41
+ request cannot reach internal indexes at all: the public surface holds
42
+ no binding to them, and the internal worker re-verifies every session
43
+ itself.
44
+
45
+ ## The proof
46
+
47
+ Your golden suite gates every promotion: content witnesses, refusal
48
+ legs, licensed pairs in both directions (entitled content serves;
49
+ unentitled never appears), context-utilization and verdict legs. The
50
+ nightly runs unattended against production and fails loudly on drift.
51
+
52
+ ## Ask it from your terminal
53
+
54
+ The konneal CLI ships with the client package: npm i -g @konneal/client, then KONNEAL_BASE and KONNEAL_KEY in the environment give you konneal ask, konneal search and konneal keys list — streaming answers with citations and verdict blocks, the same API the site uses behind the same tiering.
55
+
56
+ ## The path
57
+
58
+ `npm create @konneal/publisher` → declare the profile → ingest your
59
+ corpus → author one model package and one golden leg → map identity →
60
+ promote through the gate. Adoption is done when your team runs the
61
+ nightly and reads the gate without help.
package/package.json CHANGED
@@ -34,7 +34,7 @@
34
34
  "@oimlsmart/oiml-pubid": "^1.2.1"
35
35
  },
36
36
  "dependencies": {},
37
- "version": "0.2.5",
37
+ "version": "0.2.7",
38
38
  "description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
39
39
  "license": "BSD-3-Clause",
40
40
  "type": "module",
@@ -85,6 +85,7 @@ export async function handleLogin(env: any, req: Request): Promise<Response> {
85
85
  expirationTtl: 600,
86
86
  },
87
87
  );
88
+ const silent = url0.searchParams.get("prompt") === "none";
88
89
  const url = buildAuthorizationUrl(meta, {
89
90
  clientId: cfg.clientId,
90
91
  redirectUri: cfg.redirectUri,
@@ -92,6 +93,11 @@ export async function handleLogin(env: any, req: Request): Promise<Response> {
92
93
  state,
93
94
  nonce,
94
95
  codeChallenge: pkce.challenge,
96
+ // silent SSO: the OP answers from its existing session or errors
97
+ // login_required — the callback then lands quietly, signed in or
98
+ // still anonymous, and the estate session carries to this site
99
+ // without a click
100
+ ...(silent ? { prompt: "none" } : {}),
95
101
  });
96
102
  return new Response(null, { status: 302, headers: { location: url } });
97
103
  } catch (e) {
@@ -104,6 +110,11 @@ export async function handleCallback(env: any, req: Request): Promise<Response>
104
110
  if (!cfg) return redirectWithError("not_configured");
105
111
  const url = new URL(req.url);
106
112
  const opError = url.searchParams.get("error");
113
+ if (opError === "login_required") {
114
+ // the silent probe found no OP session — anonymous is the honest
115
+ // state, never an error
116
+ return new Response(null, { status: 302, headers: { location: "/?auth_silent=none" } });
117
+ }
107
118
  if (opError) {
108
119
  // the OP redirected back with its own failure (user denied consent,
109
120
  // session expired at the OP, …) — fail closed, in plain language
@@ -398,9 +398,54 @@ async function researchRoute(c: RouteContext): Promise<Response> {
398
398
  // The non-API routes: the HTML pages, the unit-keyed figure assets and
399
399
  // the rendered publication documents. Everything else is generated from
400
400
  // the OpenAPI document.
401
+
402
+ // The service's own permission catalog (TODO.openapi/03): the identity
403
+ // service FETCHES this endpoint to verify every permission grant for
404
+ // this client — it never holds a copy. The catalog derives from the
405
+ // profile's own declarations (gated datasets, feature flags), so a
406
+ // deployment's catalog cannot drift from what it enforces.
407
+ function permissionsCatalog(): Record<string, unknown> {
408
+ const groups: Record<string, unknown>[] = [];
409
+ const perms: Record<string, unknown>[] = [];
410
+ for (const d of P().datasets ?? []) {
411
+ if (d.session && d.permission) {
412
+ perms.push({
413
+ id: "ai.dataset.externally-licensed",
414
+ description: `Access the ${d.label} dataset (federated, externally licensed content)`,
415
+ });
416
+ }
417
+ }
418
+ if (P().publisher.features?.drafts) {
419
+ perms.push({ id: "ai.drafts", description: "Ask the assistant to prepare draft acts" });
420
+ }
421
+ perms.push({ id: "ai.memories", description: "Personalized memory files on the assistant" });
422
+ perms.push({ id: "ai.research", description: "Deep-research multi-pass questions" });
423
+ perms.push({ id: "ai.keys.admin", description: "Create and revoke API keys" });
424
+ groups.push({ id: "ai", description: `${P().publisher.product_name} gated capabilities`, permissions: perms });
425
+ return {
426
+ version: 1,
427
+ verbs: ["read", "write", "admin"],
428
+ groups,
429
+ };
430
+ }
431
+
432
+ async function openapiCatalogRoute(c: RouteContext): Promise<Response> {
433
+ return json(
434
+ {
435
+ openapi: "3.1.0",
436
+ info: { title: `${P().publisher.product_name} API`, version: c.env.INDEX_VERSION ?? "0" },
437
+ paths: {},
438
+ "x-oiml-permissions-catalog": permissionsCatalog(),
439
+ },
440
+ 200,
441
+ corsHeaders(c.req),
442
+ );
443
+ }
444
+
401
445
  const INFRA_ROUTES: Route[] = [
402
446
  { method: "GET", pattern: "/", handler: serveIndexPage },
403
447
  { method: "GET", pattern: "/api/", handler: serveIndexPage },
448
+ { method: "GET", pattern: "/api/openapi.json", handler: openapiCatalogRoute },
404
449
  { method: "GET", pattern: "/index.html", handler: serveIndexPage },
405
450
  { method: "GET", pattern: "/assets/*", handler: unitAssetRoute },
406
451
  { method: "GET", pattern: "/docs/*", handler: docsRoute },
@@ -131,6 +131,9 @@ export function buildAuthorizationUrl(
131
131
  state: string;
132
132
  nonce: string;
133
133
  codeChallenge: string;
134
+ /** OIDC prompt ('none' = silent: the OP answers from its session or
135
+ * errors login_required — never an interaction). */
136
+ prompt?: string;
134
137
  },
135
138
  ): string {
136
139
  const url = new URL(metadata.authorization_endpoint);
@@ -142,6 +145,7 @@ export function buildAuthorizationUrl(
142
145
  url.searchParams.set("nonce", params.nonce);
143
146
  url.searchParams.set("code_challenge", params.codeChallenge);
144
147
  url.searchParams.set("code_challenge_method", "S256");
148
+ if (params.prompt) url.searchParams.set("prompt", params.prompt);
145
149
  return url.toString();
146
150
  }
147
151