@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 +2 -0
- package/dist/{ask-RX3W5JJW.js → ask-6V6AJG2V.js} +2 -2
- package/dist/{chunk-GCL73ZLT.js → chunk-7UT76RQO.js} +11 -1
- package/dist/{chunk-7K5XP4PB.js → chunk-LWEVSMH6.js} +1 -1
- package/dist/{chunk-NRC4FBDQ.js → chunk-YFDWKXEB.js} +1 -1
- package/dist/oidc.d.ts +3 -0
- package/dist/{search-OXHXXT5S.js → search-RFKT7ZZY.js} +2 -2
- package/dist/worker_public/src/index.js +41 -4
- package/docs/ADOPTION.md +61 -0
- package/package.json +1 -1
- package/workers/worker_public/src/auth.ts +11 -0
- package/workers/worker_public/src/index.ts +45 -0
- package/workers/worker_public/src/oidc.ts +4 -0
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
|
|
@@ -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, {
|
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,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleSearch
|
|
3
|
-
} from "../../chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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 }
|
package/docs/ADOPTION.md
ADDED
|
@@ -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.
|
|
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
|
|