@torknetwork/governance-contract 0.0.0-stage → 0.2.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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/cache/exact-key.d.ts +33 -0
  4. package/dist/cache/exact-key.d.ts.map +1 -0
  5. package/dist/cache/exact-key.js +63 -0
  6. package/dist/cache/exact-key.js.map +1 -0
  7. package/dist/cache/exact.d.ts +54 -0
  8. package/dist/cache/exact.d.ts.map +1 -0
  9. package/dist/cache/exact.js +96 -0
  10. package/dist/cache/exact.js.map +1 -0
  11. package/dist/cache/gateway.d.ts +27 -0
  12. package/dist/cache/gateway.d.ts.map +1 -0
  13. package/dist/cache/gateway.js +32 -0
  14. package/dist/cache/gateway.js.map +1 -0
  15. package/dist/cache/semantic.d.ts +46 -0
  16. package/dist/cache/semantic.d.ts.map +1 -0
  17. package/dist/cache/semantic.js +78 -0
  18. package/dist/cache/semantic.js.map +1 -0
  19. package/dist/canonical.d.ts +17 -0
  20. package/dist/canonical.d.ts.map +1 -0
  21. package/dist/canonical.js +86 -0
  22. package/dist/canonical.js.map +1 -0
  23. package/dist/client.d.ts +40 -0
  24. package/dist/client.d.ts.map +1 -0
  25. package/dist/client.js +82 -0
  26. package/dist/client.js.map +1 -0
  27. package/dist/feedback.d.ts +35 -0
  28. package/dist/feedback.d.ts.map +1 -0
  29. package/dist/feedback.js +51 -0
  30. package/dist/feedback.js.map +1 -0
  31. package/dist/golden-set/checks.d.ts +29 -0
  32. package/dist/golden-set/checks.d.ts.map +1 -0
  33. package/dist/golden-set/checks.js +155 -0
  34. package/dist/golden-set/checks.js.map +1 -0
  35. package/dist/golden-set/runner.d.ts +22 -0
  36. package/dist/golden-set/runner.d.ts.map +1 -0
  37. package/dist/golden-set/runner.js +110 -0
  38. package/dist/golden-set/runner.js.map +1 -0
  39. package/dist/golden-set/types.d.ts +126 -0
  40. package/dist/golden-set/types.d.ts.map +1 -0
  41. package/dist/golden-set/types.js +22 -0
  42. package/dist/golden-set/types.js.map +1 -0
  43. package/dist/golden-set/validate.d.ts +40 -0
  44. package/dist/golden-set/validate.d.ts.map +1 -0
  45. package/dist/golden-set/validate.js +138 -0
  46. package/dist/golden-set/validate.js.map +1 -0
  47. package/dist/index.d.ts +24 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +40 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/langfuse.d.ts +164 -0
  52. package/dist/langfuse.d.ts.map +1 -0
  53. package/dist/langfuse.js +98 -0
  54. package/dist/langfuse.js.map +1 -0
  55. package/dist/otel.d.ts +38 -0
  56. package/dist/otel.d.ts.map +1 -0
  57. package/dist/otel.js +194 -0
  58. package/dist/otel.js.map +1 -0
  59. package/dist/providers/decision.d.ts +84 -0
  60. package/dist/providers/decision.d.ts.map +1 -0
  61. package/dist/providers/decision.js +114 -0
  62. package/dist/providers/decision.js.map +1 -0
  63. package/dist/providers/pricing.d.ts +43 -0
  64. package/dist/providers/pricing.d.ts.map +1 -0
  65. package/dist/providers/pricing.js +78 -0
  66. package/dist/providers/pricing.js.map +1 -0
  67. package/dist/providers/prompt-guard.d.ts +40 -0
  68. package/dist/providers/prompt-guard.d.ts.map +1 -0
  69. package/dist/providers/prompt-guard.js +73 -0
  70. package/dist/providers/prompt-guard.js.map +1 -0
  71. package/dist/react/FeedbackButtons.d.ts +34 -0
  72. package/dist/react/FeedbackButtons.d.ts.map +1 -0
  73. package/dist/react/FeedbackButtons.js +81 -0
  74. package/dist/react/FeedbackButtons.js.map +1 -0
  75. package/dist/react/index.d.ts +2 -0
  76. package/dist/react/index.d.ts.map +1 -0
  77. package/dist/react/index.js +6 -0
  78. package/dist/react/index.js.map +1 -0
  79. package/dist/receipt.d.ts +261 -0
  80. package/dist/receipt.d.ts.map +1 -0
  81. package/dist/receipt.js +513 -0
  82. package/dist/receipt.js.map +1 -0
  83. package/dist/switches.d.ts +82 -0
  84. package/dist/switches.d.ts.map +1 -0
  85. package/dist/switches.js +116 -0
  86. package/dist/switches.js.map +1 -0
  87. package/dist/tower-link.d.ts +127 -0
  88. package/dist/tower-link.d.ts.map +1 -0
  89. package/dist/tower-link.js +296 -0
  90. package/dist/tower-link.js.map +1 -0
  91. package/package.json +59 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tork Network Pty Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,47 @@
1
- # Temporary Holding Version
1
+ # @torknetwork/governance-contract
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The Tork governance contract (receipt contract v3) for every product that calls an AI model: one receipt shape, one feedback call, one golden-set test runner, and a per-customer Tower link. Zero runtime dependencies.
4
+
5
+ ```bash
6
+ npm install @torknetwork/governance-contract
7
+ ```
8
+
9
+ ## What it gives you
10
+
11
+ - **Receipt contract v3.** Types and validation for the `contract` block a governed turn carries: model and version, prompt and configuration versions, tokens, cost, per-step timings (`steps`), citations, grounding score, trace and hop, surface, input mode, cache hit. Missing fields are flagged `incomplete`, never rejected. `sealContract` computes `steps_hash` and `contract_hash` over canonical JSON; the Governance API recomputes both and never trusts a client's hashes.
12
+ - **OpenTelemetry GenAI mapping.** `receiptStepToOtelSpan` and `otelSpanToReceiptStep` map one receipt step to one span (semantic conventions 1.43), so agents instrumented with OpenTelemetry can be governed without this SDK.
13
+ - **Feedback.** `TorkContractClient.recordFeedback(receiptId, { verdict, reason, correction, reviewerRole })` with verdicts `correct | partly | wrong | unsafe`, and an accessible `FeedbackButtons` React component from `@torknetwork/governance-contract/react`.
14
+ - **Golden-set tests.** `runGoldenSet` runs fixed test questions through your governed path and scores them with versioned built-in checks: cites a source, cites the expected document, refuses out-of-scope questions, no PII in the answer, under the token ceiling, routed to the expected agent, retrieval recall and retrieval precision. Judge and guideline scorers are interfaces you can implement.
15
+ - **Tower link.** A per-customer setting, `off` (default) | `summary` | `full`. With `off` the SDK starts no Tower client and makes no Tower request.
16
+ - **Optional providers and caching.** All switched off unless a tenant turns them on: a decision provider with a residency guard, a prompt-injection screening slot, gateway prompt caching, an exact-match cache, a guarded semantic cache for public website answers, and OpenTelemetry export to Langfuse. A cache hit still writes a receipt.
17
+
18
+ ## Quick start
19
+
20
+ ```ts
21
+ import { sealContract, TorkContractClient } from '@torknetwork/governance-contract';
22
+
23
+ const contract = await sealContract({
24
+ model_used: 'claude-sonnet-5-5',
25
+ prompt_version: 'support-v12',
26
+ config_version: 'cfg-2026-10-09',
27
+ tokens_in: 5400,
28
+ tokens_out: 480,
29
+ steps: [
30
+ { step_id: 'r1', kind: 'retrieve', name: 'retrieve:policies', started_at: new Date().toISOString(), duration_ms: 180 },
31
+ ],
32
+ citations: [{ document_id: 'credit-policy', version: '4' }],
33
+ });
34
+
35
+ // Send `contract` with your call to POST https://tork.network/api/v1/govern.
36
+
37
+ const client = new TorkContractClient({ apiKey: process.env.TORK_API_KEY! });
38
+ await client.recordFeedback('rcpt_123', { verdict: 'wrong', reason: 'Quoted the superseded policy', reviewerRole: 'end_user' });
39
+ ```
40
+
41
+ ## Compatibility
42
+
43
+ The Python SDK (`tork-governance` 0.10.0, `tork.contract`) and the Rust SDK (`tork-edge` 0.3.0, `contract`) produce byte-identical hashes for the same block; a shared golden fixture keeps them in step.
44
+
45
+ ## Licence
46
+
47
+ MIT. Tork is a seatbelt, not invincibility.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Tenant-scoped cache identities keep answer content private and invalidate
3
+ * when sources or generation settings change. Role-dependent answers bypass
4
+ * caching; embedding identities preserve the exact text sent to the model.
5
+ */
6
+ import type { Surface } from '../receipt';
7
+ export declare const EXACT_CACHE_KEY_VERSION = "v1";
8
+ export interface SourceVersion {
9
+ document_id: string;
10
+ version: string;
11
+ }
12
+ export interface ExactCacheKeyInput {
13
+ org_id: string;
14
+ product: string;
15
+ surface: Surface;
16
+ question: string;
17
+ model: string;
18
+ prompt_version: string;
19
+ config_version: string;
20
+ /** The documents (and their versions) the answer was built from. */
21
+ source_versions: readonly SourceVersion[];
22
+ /** True when the answer depends on the asker's role or clearance — never cached. */
23
+ role_dependent: boolean;
24
+ locale?: string;
25
+ }
26
+ export declare function normaliseQuestion(q: string): string;
27
+ export declare function buildExactCacheKey(input: ExactCacheKeyInput): Promise<string | null>;
28
+ export declare function buildEmbeddingCacheKey(input: {
29
+ org_id: string;
30
+ model: string;
31
+ content: string;
32
+ }): Promise<string>;
33
+ //# sourceMappingURL=exact-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exact-key.d.ts","sourceRoot":"","sources":["../../src/cache/exact-key.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C,MAAM,WAAW,aAAa;IAAG,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAEvE,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,eAAe,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,oFAAoF;IACpF,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEnD;AAUD,wBAAsB,kBAAkB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA0B1F;AAED,wBAAsB,sBAAsB,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAOvH"}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ /**
3
+ * Tenant-scoped cache identities keep answer content private and invalidate
4
+ * when sources or generation settings change. Role-dependent answers bypass
5
+ * caching; embedding identities preserve the exact text sent to the model.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.EXACT_CACHE_KEY_VERSION = void 0;
9
+ exports.normaliseQuestion = normaliseQuestion;
10
+ exports.buildExactCacheKey = buildExactCacheKey;
11
+ exports.buildEmbeddingCacheKey = buildEmbeddingCacheKey;
12
+ const canonical_1 = require("../canonical");
13
+ exports.EXACT_CACHE_KEY_VERSION = 'v1';
14
+ const ORG_ID = /^[A-Za-z0-9_-]{1,64}$/;
15
+ function normaliseQuestion(q) {
16
+ return q.normalize('NFKC').trim().replace(/\s+/g, ' ').toLowerCase().replace(/[?!.\s]+$/g, '');
17
+ }
18
+ function nonEmpty(value) {
19
+ return typeof value === 'string' && value.length > 0;
20
+ }
21
+ function compare(a, b) {
22
+ return a < b ? -1 : a > b ? 1 : 0;
23
+ }
24
+ async function buildExactCacheKey(input) {
25
+ try {
26
+ if (input.role_dependent || !ORG_ID.test(input.org_id))
27
+ return null;
28
+ const q = normaliseQuestion(input.question);
29
+ if (!q || ![input.product, input.model, input.prompt_version, input.config_version].every(nonEmpty))
30
+ return null;
31
+ const sources = [];
32
+ const seen = new Set();
33
+ for (const source of input.source_versions) {
34
+ if (!nonEmpty(source.document_id) || !nonEmpty(source.version))
35
+ return null;
36
+ const pair = [source.document_id, source.version];
37
+ const identity = (0, canonical_1.canonicalJson)(pair);
38
+ if (!seen.has(identity)) {
39
+ seen.add(identity);
40
+ sources.push(pair);
41
+ }
42
+ }
43
+ sources.sort((a, b) => compare(a[0], b[0]) || compare(a[1], b[1]));
44
+ const hash = await (0, canonical_1.sha256Hex)((0, canonical_1.canonicalJson)({
45
+ product: input.product, surface: input.surface, q, model: input.model,
46
+ prompt_version: input.prompt_version, config_version: input.config_version,
47
+ locale: input.locale ?? null, sources,
48
+ }));
49
+ return `tork:xc:${exports.EXACT_CACHE_KEY_VERSION}:${input.org_id}:${hash}`;
50
+ }
51
+ catch {
52
+ return null; // Invalid input or unavailable hashing makes caching a miss.
53
+ }
54
+ }
55
+ async function buildEmbeddingCacheKey(input) {
56
+ if (!ORG_ID.test(input.org_id) || !nonEmpty(input.model)) {
57
+ throw new TypeError('Embedding cache requires a valid org_id and non-empty model');
58
+ }
59
+ const modelHash = await (0, canonical_1.sha256Hex)(input.model);
60
+ const contentHash = await (0, canonical_1.sha256Hex)(input.content);
61
+ return `tork:emb:${exports.EXACT_CACHE_KEY_VERSION}:${input.org_id}:${modelHash.slice(0, 16)}:${contentHash}`;
62
+ }
63
+ //# sourceMappingURL=exact-key.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exact-key.js","sourceRoot":"","sources":["../../src/cache/exact-key.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Exact-match answer cache and embedding cache, on any Redis-like store
3
+ * (Upstash Redis in production: `new Redis({...})` satisfies CacheStore).
4
+ *
5
+ * Rules enforced here, not left to the caller:
6
+ * - keys are scoped per tenant and include the source-document versions the
7
+ * answer used, so publishing a new version invalidates by construction
8
+ * (see exact-key.ts);
9
+ * - an answer that depends on the asker's role or clearance is NEVER cached
10
+ * (the key builder returns null and nothing is read or written);
11
+ * - the tenant's `exact_cache` switch gates every read and write;
12
+ * - a cache hit still produces a receipt: `cachedAnswer` calls `onReceipt`
13
+ * on hits and misses alike, with `cache_hit` set on hits.
14
+ *
15
+ * Flow: retrieve first (cheap), then look up with the retrieved documents'
16
+ * versions, and only generate on a miss.
17
+ */
18
+ import type { TenantSettings } from '../switches';
19
+ import type { CacheHit } from '../receipt';
20
+ import { type ExactCacheKeyInput } from './exact-key';
21
+ export interface CacheStore {
22
+ get(key: string): Promise<unknown>;
23
+ set(key: string, value: string, opts?: {
24
+ ex?: number;
25
+ }): Promise<unknown>;
26
+ }
27
+ export interface CachedAnswerResult<T> {
28
+ value: T;
29
+ cache_hit: CacheHit | null;
30
+ }
31
+ export declare function cachedAnswer<T>(opts: {
32
+ store: CacheStore | null | undefined;
33
+ settings: TenantSettings;
34
+ key: ExactCacheKeyInput;
35
+ compute: () => Promise<T>;
36
+ /** Always called — on a hit with cache_hit 'exact', on a miss with null. */
37
+ onReceipt: (r: {
38
+ cache_hit: CacheHit | null;
39
+ }) => Promise<void> | void;
40
+ ttlSeconds?: number;
41
+ }): Promise<CachedAnswerResult<T>>;
42
+ /** Embeddings cached by content hash, per tenant and per model. */
43
+ export declare function cachedEmbedding(opts: {
44
+ store: CacheStore | null | undefined;
45
+ settings: TenantSettings;
46
+ model: string;
47
+ content: string;
48
+ embed: (content: string) => Promise<number[]>;
49
+ ttlSeconds?: number;
50
+ }): Promise<{
51
+ embedding: number[];
52
+ cache_hit: boolean;
53
+ }>;
54
+ //# sourceMappingURL=exact.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exact.d.ts","sourceRoot":"","sources":["../../src/cache/exact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAA8C,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAElG,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3E;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC;IACnC,KAAK,EAAE,CAAC,CAAC;IACT,SAAS,EAAE,QAAQ,GAAG,IAAI,CAAC;CAC5B;AAiBD,wBAAsB,YAAY,CAAC,CAAC,EAAE,IAAI,EAAE;IAC1C,KAAK,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IACrC,QAAQ,EAAE,cAAc,CAAC;IACzB,GAAG,EAAE,kBAAkB,CAAC;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1B,4EAA4E;IAC5E,SAAS,EAAE,CAAC,CAAC,EAAE;QAAE,SAAS,EAAE,QAAQ,GAAG,IAAI,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC,CA2BjC;AAED,mEAAmE;AACnE,wBAAsB,eAAe,CAAC,IAAI,EAAE;IAC1C,KAAK,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IACrC,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAAC,CAwBvD"}
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ /**
3
+ * Exact-match answer cache and embedding cache, on any Redis-like store
4
+ * (Upstash Redis in production: `new Redis({...})` satisfies CacheStore).
5
+ *
6
+ * Rules enforced here, not left to the caller:
7
+ * - keys are scoped per tenant and include the source-document versions the
8
+ * answer used, so publishing a new version invalidates by construction
9
+ * (see exact-key.ts);
10
+ * - an answer that depends on the asker's role or clearance is NEVER cached
11
+ * (the key builder returns null and nothing is read or written);
12
+ * - the tenant's `exact_cache` switch gates every read and write;
13
+ * - a cache hit still produces a receipt: `cachedAnswer` calls `onReceipt`
14
+ * on hits and misses alike, with `cache_hit` set on hits.
15
+ *
16
+ * Flow: retrieve first (cheap), then look up with the retrieved documents'
17
+ * versions, and only generate on a miss.
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.cachedAnswer = cachedAnswer;
21
+ exports.cachedEmbedding = cachedEmbedding;
22
+ const exact_key_1 = require("./exact-key");
23
+ const DEFAULT_TTL_SECONDS = 24 * 60 * 60;
24
+ function decode(raw) {
25
+ if (raw === null || raw === undefined)
26
+ return undefined;
27
+ // Upstash auto-deserialises JSON; other clients return the string.
28
+ if (typeof raw === 'string') {
29
+ try {
30
+ return JSON.parse(raw);
31
+ }
32
+ catch {
33
+ return undefined;
34
+ }
35
+ }
36
+ return raw;
37
+ }
38
+ async function cachedAnswer(opts) {
39
+ const enabled = opts.settings.switches.exact_cache && !!opts.store;
40
+ const key = enabled ? await (0, exact_key_1.buildExactCacheKey)(opts.key) : null;
41
+ if (key && opts.store) {
42
+ let hit;
43
+ try {
44
+ hit = decode(await opts.store.get(key));
45
+ }
46
+ catch {
47
+ hit = undefined; // a cache outage is a miss, never an error
48
+ }
49
+ if (hit !== undefined) {
50
+ await opts.onReceipt({ cache_hit: 'exact' });
51
+ return { value: hit, cache_hit: 'exact' };
52
+ }
53
+ }
54
+ const value = await opts.compute();
55
+ if (key && opts.store) {
56
+ try {
57
+ await opts.store.set(key, JSON.stringify(value), { ex: opts.ttlSeconds ?? DEFAULT_TTL_SECONDS });
58
+ }
59
+ catch {
60
+ /* best effort */
61
+ }
62
+ }
63
+ await opts.onReceipt({ cache_hit: null });
64
+ return { value, cache_hit: null };
65
+ }
66
+ /** Embeddings cached by content hash, per tenant and per model. */
67
+ async function cachedEmbedding(opts) {
68
+ if (!opts.store || !opts.settings.switches.exact_cache) {
69
+ return { embedding: await opts.embed(opts.content), cache_hit: false };
70
+ }
71
+ let key;
72
+ try {
73
+ key = await (0, exact_key_1.buildEmbeddingCacheKey)({ org_id: opts.settings.org_id, model: opts.model, content: opts.content });
74
+ }
75
+ catch {
76
+ // An org id the key builder will not scope is never cached, never an error.
77
+ return { embedding: await opts.embed(opts.content), cache_hit: false };
78
+ }
79
+ try {
80
+ const hit = decode(await opts.store.get(key));
81
+ if (Array.isArray(hit))
82
+ return { embedding: hit, cache_hit: true };
83
+ }
84
+ catch {
85
+ /* miss */
86
+ }
87
+ const embedding = await opts.embed(opts.content);
88
+ try {
89
+ await opts.store.set(key, JSON.stringify(embedding), { ex: opts.ttlSeconds ?? 7 * DEFAULT_TTL_SECONDS });
90
+ }
91
+ catch {
92
+ /* best effort */
93
+ }
94
+ return { embedding, cache_hit: false };
95
+ }
96
+ //# sourceMappingURL=exact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exact.js","sourceRoot":"","sources":["../../src/cache/exact.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Vercel AI Gateway prompt caching.
3
+ *
4
+ * `providerOptions.gateway.caching = 'auto'` asks the Gateway to apply each
5
+ * provider's own prompt-caching strategy (Vercel docs, "Automatic caching",
6
+ * read 9 Oct 2026). `x-session-affinity` carries a stable value per
7
+ * conversation so turns of one conversation land where the cached prefix is.
8
+ *
9
+ * The affinity value is a SHA-256 of (org_id, conversation_id), never the raw
10
+ * conversation id: it leaves Tork's boundary in a request header.
11
+ */
12
+ import type { TenantSettings } from '../switches';
13
+ export interface GatewayCallOptions {
14
+ providerOptions?: {
15
+ gateway: {
16
+ caching: 'auto';
17
+ };
18
+ };
19
+ headers?: Record<string, string>;
20
+ }
21
+ export declare function sessionAffinity(org_id: string, conversation_id: string): Promise<string>;
22
+ /**
23
+ * Options to spread into an AI SDK call through the Gateway. Returns `{}`
24
+ * when the tenant's `gateway_caching` switch is off.
25
+ */
26
+ export declare function gatewayCallOptions(settings: TenantSettings, conversation_id: string | null | undefined): Promise<GatewayCallOptions>;
27
+ //# sourceMappingURL=gateway.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.d.ts","sourceRoot":"","sources":["../../src/cache/gateway.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAElD,MAAM,WAAW,kBAAkB;IACjC,eAAe,CAAC,EAAE;QAAE,OAAO,EAAE;YAAE,OAAO,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAE9F;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,QAAQ,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAK1I"}
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ /**
3
+ * Vercel AI Gateway prompt caching.
4
+ *
5
+ * `providerOptions.gateway.caching = 'auto'` asks the Gateway to apply each
6
+ * provider's own prompt-caching strategy (Vercel docs, "Automatic caching",
7
+ * read 9 Oct 2026). `x-session-affinity` carries a stable value per
8
+ * conversation so turns of one conversation land where the cached prefix is.
9
+ *
10
+ * The affinity value is a SHA-256 of (org_id, conversation_id), never the raw
11
+ * conversation id: it leaves Tork's boundary in a request header.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.sessionAffinity = sessionAffinity;
15
+ exports.gatewayCallOptions = gatewayCallOptions;
16
+ const canonical_1 = require("../canonical");
17
+ async function sessionAffinity(org_id, conversation_id) {
18
+ return (await (0, canonical_1.sha256Hex)(`tork-affinity:v1:${org_id}:${conversation_id}`)).slice(0, 32);
19
+ }
20
+ /**
21
+ * Options to spread into an AI SDK call through the Gateway. Returns `{}`
22
+ * when the tenant's `gateway_caching` switch is off.
23
+ */
24
+ async function gatewayCallOptions(settings, conversation_id) {
25
+ if (!settings.switches.gateway_caching)
26
+ return {};
27
+ const out = { providerOptions: { gateway: { caching: 'auto' } } };
28
+ if (conversation_id)
29
+ out.headers = { 'x-session-affinity': await sessionAffinity(settings.org_id, conversation_id) };
30
+ return out;
31
+ }
32
+ //# sourceMappingURL=gateway.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../src/cache/gateway.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG;;AAUH,0CAEC;AAMD,gDAKC;AArBD,4CAAyC;AAQlC,KAAK,UAAU,eAAe,CAAC,MAAc,EAAE,eAAuB;IAC3E,OAAO,CAAC,MAAM,IAAA,qBAAS,EAAC,oBAAoB,MAAM,IAAI,eAAe,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACzF,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,kBAAkB,CAAC,QAAwB,EAAE,eAA0C;IAC3G,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,eAAe;QAAE,OAAO,EAAE,CAAC;IAClD,MAAM,GAAG,GAAuB,EAAE,eAAe,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACtF,IAAI,eAAe;QAAE,GAAG,CAAC,OAAO,GAAG,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC,EAAE,CAAC;IACrH,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Semantic cache slot (Upstash `@upstash/semantic-cache`). OFF by default.
3
+ *
4
+ * Allowed only when ALL hold:
5
+ * - the tenant's `semantic_cache` switch is on;
6
+ * - the surface is `website_widget` (public content, same answer for every
7
+ * visitor) — never a product panel, voice or API surface;
8
+ * - the answer is not role- or clearance-dependent;
9
+ * - the similarity threshold is at least 0.95.
10
+ *
11
+ * The package is not a dependency: the product constructs
12
+ * `new SemanticCache({ index, minProximity, namespace })` and passes it in as
13
+ * a SemanticCacheStore. `semanticCacheNamespace` gives the per-tenant
14
+ * namespace to construct it with.
15
+ */
16
+ import type { TenantSettings } from '../switches';
17
+ import type { Surface } from '../receipt';
18
+ export declare const MIN_SEMANTIC_THRESHOLD = 0.95;
19
+ export interface SemanticCacheStore {
20
+ get(question: string): Promise<string | undefined | null>;
21
+ set(question: string, answer: string): Promise<unknown>;
22
+ }
23
+ export declare class SemanticCacheRefused extends Error {
24
+ readonly reason: string;
25
+ constructor(reason: string);
26
+ }
27
+ export declare function semanticCacheNamespace(org_id: string, source_versions_digest: string): string;
28
+ /** Why the semantic cache may not be used, or null when it may. */
29
+ export declare function semanticCacheRefusal(opts: {
30
+ settings: TenantSettings;
31
+ surface: Surface;
32
+ role_dependent: boolean;
33
+ threshold: number;
34
+ }): string | null;
35
+ export declare class GuardedSemanticCache {
36
+ private readonly store;
37
+ private readonly opts;
38
+ constructor(store: SemanticCacheStore, opts: {
39
+ settings: TenantSettings;
40
+ surface: Surface;
41
+ threshold: number;
42
+ });
43
+ get(question: string, role_dependent: boolean): Promise<string | null>;
44
+ set(question: string, answer: string, role_dependent: boolean): Promise<boolean>;
45
+ }
46
+ //# sourceMappingURL=semantic.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic.d.ts","sourceRoot":"","sources":["../../src/cache/semantic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC,CAAC;IAC1D,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACzD;AAED,qBAAa,oBAAqB,SAAQ,KAAK;aACjB,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM;CAI3C;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,GAAG,MAAM,CAE7F;AAED,mEAAmE;AACnE,wBAAgB,oBAAoB,CAAC,IAAI,EAAE;IACzC,QAAQ,EAAE,cAAc,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,GAAG,IAAI,CAMhB;AAED,qBAAa,oBAAoB;IAE7B,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,KAAK,EAAE,kBAAkB,EACzB,IAAI,EAAE;QAAE,QAAQ,EAAE,cAAc,CAAC;QAAC,OAAO,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE;IAQpF,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAStE,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;CASvF"}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ /**
3
+ * Semantic cache slot (Upstash `@upstash/semantic-cache`). OFF by default.
4
+ *
5
+ * Allowed only when ALL hold:
6
+ * - the tenant's `semantic_cache` switch is on;
7
+ * - the surface is `website_widget` (public content, same answer for every
8
+ * visitor) — never a product panel, voice or API surface;
9
+ * - the answer is not role- or clearance-dependent;
10
+ * - the similarity threshold is at least 0.95.
11
+ *
12
+ * The package is not a dependency: the product constructs
13
+ * `new SemanticCache({ index, minProximity, namespace })` and passes it in as
14
+ * a SemanticCacheStore. `semanticCacheNamespace` gives the per-tenant
15
+ * namespace to construct it with.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.GuardedSemanticCache = exports.SemanticCacheRefused = exports.MIN_SEMANTIC_THRESHOLD = void 0;
19
+ exports.semanticCacheNamespace = semanticCacheNamespace;
20
+ exports.semanticCacheRefusal = semanticCacheRefusal;
21
+ exports.MIN_SEMANTIC_THRESHOLD = 0.95;
22
+ class SemanticCacheRefused extends Error {
23
+ constructor(reason) {
24
+ super(`semantic cache refused: ${reason}`);
25
+ this.reason = reason;
26
+ this.name = 'SemanticCacheRefused';
27
+ }
28
+ }
29
+ exports.SemanticCacheRefused = SemanticCacheRefused;
30
+ function semanticCacheNamespace(org_id, source_versions_digest) {
31
+ return `tork:sc:v1:${org_id}:${source_versions_digest}`;
32
+ }
33
+ /** Why the semantic cache may not be used, or null when it may. */
34
+ function semanticCacheRefusal(opts) {
35
+ if (!opts.settings.switches.semantic_cache)
36
+ return 'semantic_cache switch is off';
37
+ if (opts.surface !== 'website_widget')
38
+ return `surface ${opts.surface} is not a public website surface`;
39
+ if (opts.role_dependent)
40
+ return 'answer depends on the asker role or clearance';
41
+ if (!(opts.threshold >= exports.MIN_SEMANTIC_THRESHOLD))
42
+ return `threshold ${opts.threshold} is below ${exports.MIN_SEMANTIC_THRESHOLD}`;
43
+ return null;
44
+ }
45
+ class GuardedSemanticCache {
46
+ constructor(store, opts) {
47
+ this.store = store;
48
+ this.opts = opts;
49
+ // Fail at construction for the static conditions, so a mis-wired cache
50
+ // is a startup error rather than a silent leak.
51
+ const r = semanticCacheRefusal({ ...opts, role_dependent: false });
52
+ if (r)
53
+ throw new SemanticCacheRefused(r);
54
+ }
55
+ async get(question, role_dependent) {
56
+ if (semanticCacheRefusal({ ...this.opts, role_dependent }))
57
+ return null;
58
+ try {
59
+ return (await this.store.get(question)) ?? null;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ async set(question, answer, role_dependent) {
66
+ if (semanticCacheRefusal({ ...this.opts, role_dependent }))
67
+ return false;
68
+ try {
69
+ await this.store.set(question, answer);
70
+ return true;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ }
77
+ exports.GuardedSemanticCache = GuardedSemanticCache;
78
+ //# sourceMappingURL=semantic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantic.js","sourceRoot":"","sources":["../../src/cache/semantic.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;GAcG;;;AAmBH,wDAEC;AAGD,oDAWC;AA9BY,QAAA,sBAAsB,GAAG,IAAI,CAAC;AAO3C,MAAa,oBAAqB,SAAQ,KAAK;IAC7C,YAA4B,MAAc;QACxC,KAAK,CAAC,2BAA2B,MAAM,EAAE,CAAC,CAAC;QADjB,WAAM,GAAN,MAAM,CAAQ;QAExC,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAC;IACrC,CAAC;CACF;AALD,oDAKC;AAED,SAAgB,sBAAsB,CAAC,MAAc,EAAE,sBAA8B;IACnF,OAAO,cAAc,MAAM,IAAI,sBAAsB,EAAE,CAAC;AAC1D,CAAC;AAED,mEAAmE;AACnE,SAAgB,oBAAoB,CAAC,IAKpC;IACC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,cAAc;QAAE,OAAO,8BAA8B,CAAC;IAClF,IAAI,IAAI,CAAC,OAAO,KAAK,gBAAgB;QAAE,OAAO,WAAW,IAAI,CAAC,OAAO,kCAAkC,CAAC;IACxG,IAAI,IAAI,CAAC,cAAc;QAAE,OAAO,+CAA+C,CAAC;IAChF,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,IAAI,8BAAsB,CAAC;QAAE,OAAO,aAAa,IAAI,CAAC,SAAS,aAAa,8BAAsB,EAAE,CAAC;IACzH,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAa,oBAAoB;IAC/B,YACmB,KAAyB,EACzB,IAAuE;QADvE,UAAK,GAAL,KAAK,CAAoB;QACzB,SAAI,GAAJ,IAAI,CAAmE;QAExF,uEAAuE;QACvE,gDAAgD;QAChD,MAAM,CAAC,GAAG,oBAAoB,CAAC,EAAE,GAAG,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,CAAC;QACnE,IAAI,CAAC;YAAE,MAAM,IAAI,oBAAoB,CAAC,CAAC,CAAC,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,QAAgB,EAAE,cAAuB;QACjD,IAAI,oBAAoB,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC;YAAE,OAAO,IAAI,CAAC;QACxE,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,IAAI,IAAI,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,QAAgB,EAAE,MAAc,EAAE,cAAuB;QACjE,IAAI,oBAAoB,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,cAAc,EAAE,CAAC;YAAE,OAAO,KAAK,CAAC;QACzE,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACvC,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;CACF;AA7BD,oDA6BC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Canonical serialisation and hashing used by every hash in the contract.
3
+ *
4
+ * Same rule as Tork Tower's canonicalJson (tork-tower-t2
5
+ * src/lib/crypto/canonical-json.ts): object keys sorted at every depth, arrays
6
+ * kept in order, `undefined` dropped. Two parties that agree on this function
7
+ * agree on the exact bytes a hash covers without agreeing on JSON key order.
8
+ *
9
+ * Pure: no I/O, no keys. Uses Web Crypto so it runs unchanged in Node 18+,
10
+ * the Vercel Edge runtime, and browsers.
11
+ */
12
+ export declare function canonicalJson(value: unknown): string;
13
+ /** SHA-256 over UTF-8, lowercase hex. */
14
+ export declare function sha256Hex(text: string): Promise<string>;
15
+ /** SHA-256 of the canonical JSON of `value`. */
16
+ export declare function hashCanonical(value: unknown): Promise<string>;
17
+ //# sourceMappingURL=canonical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEpD;AAqDD,yCAAyC;AACzC,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK7D;AAED,gDAAgD;AAChD,wBAAsB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAEnE"}