@tangle-network/agent-app 0.1.1 → 0.1.3
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 +21 -0
- package/README.md +116 -31
- package/dist/billing/index.d.ts +94 -1
- package/dist/billing/index.js +3 -1
- package/dist/{chunk-45MYQ3GD.js → chunk-EAJSWUU5.js} +56 -1
- package/dist/chunk-EAJSWUU5.js.map +1 -0
- package/dist/{chunk-SIDR6BH3.js → chunk-ZJGY7OMZ.js} +47 -2
- package/dist/chunk-ZJGY7OMZ.js.map +1 -0
- package/dist/chunk-ZXNXAQAH.js +69 -0
- package/dist/chunk-ZXNXAQAH.js.map +1 -0
- package/dist/crypto/index.d.ts +36 -1
- package/dist/crypto/index.js +13 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +25 -7
- package/dist/knowledge/index.d.ts +90 -0
- package/dist/knowledge/index.js +9 -0
- package/dist/knowledge/index.js.map +1 -0
- package/package.json +19 -2
- package/dist/chunk-45MYQ3GD.js.map +0 -1
- package/dist/chunk-SIDR6BH3.js.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tangle Network
|
|
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,59 +1,144 @@
|
|
|
1
1
|
# @tangle-network/agent-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@tangle-network/agent-app)
|
|
4
|
+
[](https://www.npmjs.com/package/@tangle-network/agent-app#provenance)
|
|
5
|
+
[](./LICENSE)
|
|
4
6
|
|
|
5
|
-
The
|
|
7
|
+
The application-shell layer for building agent products on the Tangle stack.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
The substrate packages — `@tangle-network/agent-runtime`, `agent-eval`, `agent-integrations`, `tcloud`, `sandbox` — are the **engine**. This package is the **shell**: the chat tool-loop, the structured agent→app side channel, the integration-hub client, per-workspace billing, field crypto, and the web boundary utilities that every agent app otherwise rewrites by hand. You supply your domain through typed seams; the package supplies the mechanism and imports none of your code.
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## Highlights
|
|
12
|
+
|
|
13
|
+
- **Structured tool side channel** — `submit_proposal` (approval-gated), `schedule_followup`, `render_ui`, `add_citation`, exposed as validated tool calls over three surfaces (HTTP route, per-turn MCP server, agent-runtime executor). No fenced-text parsing.
|
|
14
|
+
- **Bounded tool loop** — `runAppToolLoop` / `streamAppToolLoop`: stream a turn → collect tool calls → dispatch → fold results back → re-run, capped. Substrate-free behind a `streamTurn` seam, so it drives a sandboxed agent, a Worker, or an in-browser copilot unchanged.
|
|
15
|
+
- **Sandbox-optional** — the same tools, billing, eval, and loop work without a container. A `fetch`-only adapter maps any OpenAI-compatible stream (Tangle Router, tcloud) into the loop. See [`examples/browser-copilot.md`](./examples/browser-copilot.md).
|
|
16
|
+
- **Composes the engine, never forks it** — `/eval` re-exports `@tangle-network/agent-eval`'s verifier; `/integrations` wraps the hub; `/tangle` and `/billing` take the tcloud client as a structural contract. Engines are **peer dependencies** — you pin the version, nothing is bundled.
|
|
17
|
+
- **ESM, typed, zero runtime deps** in the substrate-free modules (`/runtime`, `/web`, `/crypto`, `/redact`, `/stream`). Ships with `.d.ts` and npm [provenance](https://www.npmjs.com/package/@tangle-network/agent-app#provenance).
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add @tangle-network/agent-app
|
|
23
|
+
```
|
|
10
24
|
|
|
11
|
-
|
|
25
|
+
The engine packages you actually use are **peer dependencies** — install the ones your modules touch:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# /eval composes the eval engine; /integrations composes the hub client
|
|
29
|
+
pnpm add @tangle-network/agent-eval @tangle-network/agent-integrations
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Peer | Required by | Range |
|
|
12
33
|
|---|---|---|
|
|
13
|
-
| `@tangle-network/agent-
|
|
14
|
-
| `@tangle-network/agent-
|
|
15
|
-
| `@tangle-network/agent-app/tangle` | ✅ **shipped + tested** | Tangle login (SSO) + the developer self-service **app-registration → broker-token** flow: `buildConsentUrl` (one-time user consent) + `createBrokerTokenProvider` (caches/auto-refreshes the `sk-tan-broker-` token per durable grant, shares in-flight mints). Structural (depends on the minter contract; pass the concrete `TangleAppsClient` from `@tangle-network/agent-integrations`). |
|
|
16
|
-
| `@tangle-network/agent-app/runtime` | ✅ **shipped + tested** | `runAppToolLoop` — the bounded multi-turn tool loop every app's chat runtime hand-rolls: stream a turn → collect tool calls → dispatch → fold results back → re-run, capped. Substrate-free via a `streamTurn` seam (wrap any backend / `runAgentTaskStream`) + an `executeToolCall` seam (route to integration + app-tool executors). |
|
|
17
|
-
| `@tangle-network/agent-app/eval` | ✅ **shipped + tested** | The inline completion gate: `producedFromToolEvents` (bridge `/tools` produced events), `verifyCompletion` (per-requirement `satisfiedBy` gate), `tokenRecallChecker` (deterministic content check), `weightedScore`. For full campaigns/traces/LLM-judge use `@tangle-network/agent-eval`; this composes with it. |
|
|
34
|
+
| `@tangle-network/agent-eval` | `/eval` | `>=0.50.0` |
|
|
35
|
+
| `@tangle-network/agent-integrations` | `/integrations`, `/tangle` | `>=0.32.0` |
|
|
18
36
|
|
|
19
|
-
|
|
37
|
+
The substrate-free modules (`/runtime`, `/tools`, `/web`, `/crypto`, `/redact`, `/stream`, `/billing`) need no peers.
|
|
20
38
|
|
|
21
|
-
##
|
|
39
|
+
## Quick start
|
|
22
40
|
|
|
23
|
-
A product supplies its taxonomy
|
|
41
|
+
A product supplies its **taxonomy** (which proposal types exist, which are approval-gated) and its **handlers** (the real DB/vault writes), then wires the tool side channel to whichever surface it runs on.
|
|
24
42
|
|
|
25
43
|
```ts
|
|
26
44
|
import {
|
|
27
|
-
buildAppToolOpenAITools,
|
|
28
|
-
|
|
45
|
+
buildAppToolOpenAITools,
|
|
46
|
+
createAppToolRuntimeExecutor,
|
|
47
|
+
type AppToolHandlers,
|
|
48
|
+
type AppToolTaxonomy,
|
|
29
49
|
} from '@tangle-network/agent-app/tools'
|
|
50
|
+
import { runAppToolLoop } from '@tangle-network/agent-app/runtime'
|
|
51
|
+
|
|
52
|
+
// 1. Declare the domain (the package bakes in no proposal types or rules).
|
|
53
|
+
const taxonomy: AppToolTaxonomy = {
|
|
54
|
+
proposalTypes: ['recommend', 'contact', 'other'],
|
|
55
|
+
regulatedTypes: ['recommend', 'contact'], // these require a certified approver
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 2. Provide the side effects — your store, your validation.
|
|
59
|
+
const handlers: AppToolHandlers = {
|
|
60
|
+
submitProposal,
|
|
61
|
+
scheduleFollowup,
|
|
62
|
+
renderUi,
|
|
63
|
+
addCitation,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. Advertise the tools to the model and route their execution.
|
|
67
|
+
const tools = buildAppToolOpenAITools(taxonomy)
|
|
68
|
+
const executeToolCall = createAppToolRuntimeExecutor({
|
|
69
|
+
handlers,
|
|
70
|
+
taxonomy,
|
|
71
|
+
ctx: { userId, workspaceId, threadId },
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// 4. Run a bounded, tool-driven turn loop over any backend.
|
|
75
|
+
const result = await runAppToolLoop({
|
|
76
|
+
systemPrompt,
|
|
77
|
+
userMessage,
|
|
78
|
+
streamTurn, // wrap your model / runAgentTaskStream
|
|
79
|
+
executeToolCall,
|
|
80
|
+
isExecutableTool: (name) => tools.some((t) => t.function.name === name),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
console.log(result.finalText, result.toolResults)
|
|
84
|
+
```
|
|
30
85
|
|
|
31
|
-
|
|
32
|
-
const handlers: AppToolHandlers = { submitProposal, scheduleFollowup, renderUi, addCitation } // your DB ops
|
|
33
|
-
|
|
34
|
-
// 1. Sandbox MCP path — one route file per tool:
|
|
35
|
-
export const action = ({ request }) =>
|
|
36
|
-
handleAppToolRequest(request, { tool: 'submit_proposal', handlers, taxonomy, verifyToken })
|
|
86
|
+
`streamTurn` is the one seam that varies by backend. For an in-browser or edge copilot talking to an OpenAI-compatible endpoint, you don't write it by hand:
|
|
37
87
|
|
|
38
|
-
|
|
39
|
-
|
|
88
|
+
```ts
|
|
89
|
+
import { createOpenAICompatStreamTurn, resolveTangleModelConfig } from '@tangle-network/agent-app/runtime'
|
|
40
90
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
appToolExecutor: createAppToolRuntimeExecutor({ handlers, taxonomy, ctx, onProduced }) })
|
|
91
|
+
const cfg = resolveTangleModelConfig() // reads provider/model/key/baseUrl from env, or pass literals
|
|
92
|
+
const streamTurn = createOpenAICompatStreamTurn({ ...cfg, tools })
|
|
44
93
|
```
|
|
45
94
|
|
|
46
|
-
|
|
95
|
+
The full three-transport walkthrough (Tangle Router, tcloud, Vercel AI SDK) is in [`examples/browser-copilot.md`](./examples/browser-copilot.md).
|
|
96
|
+
|
|
97
|
+
## How it's organised
|
|
98
|
+
|
|
99
|
+
One rule decides where anything lives:
|
|
100
|
+
|
|
101
|
+
> Does the capability make sense **without** a specific app's tool side channel, approval queue, or chat route?
|
|
102
|
+
> **Yes** → it belongs in an engine package (contribute it down).
|
|
103
|
+
> **No** → it's app-shell, and it belongs here.
|
|
104
|
+
|
|
105
|
+
Everything here is reached through a typed seam — `AppToolHandlers`, `AppToolTaxonomy`, `streamTurn`, `executeToolCall`, `verifyToken`, `KeyProvisioner` / `WorkspaceKeyStore` / `KeyCrypto`. The package never imports product code and never hard-codes a domain value (a proposal type, a premium, a disclaimer); each is a parameter. New capability arrives as a new subpath, never a breaking change to an existing one.
|
|
106
|
+
|
|
107
|
+
## Modules
|
|
108
|
+
|
|
109
|
+
Each is an independent entry point — import only what you use.
|
|
110
|
+
|
|
111
|
+
| Subpath | What it gives you |
|
|
112
|
+
|---|---|
|
|
113
|
+
| [`/tools`](src/tools) | The structured agent→app side channel: `buildAppToolOpenAITools`, `createAppToolRuntimeExecutor`, `handleAppToolRequest` (HTTP), `buildAppToolMcpServer` / `buildHttpMcpServer` (MCP), `createCapabilityToken` + `authenticateToolRequest` (capability auth), `ToolInputError`. |
|
|
114
|
+
| [`/runtime`](src/runtime) | `runAppToolLoop` / `streamAppToolLoop` (bounded tool loop), `resolveTangleModelConfig` (Tangle Router / Anthropic BYOK), and `toLoopEvents` / `createOpenAICompatStreamTurn` (OpenAI-compat stream → loop events, with fragmented tool-call args reassembled). |
|
|
115
|
+
| [`/integrations`](src/integrations) | Integration-hub client: `HubExecClient`, `resolveIntegrationAction`, `invokeIntegrationHub`. Composes `@tangle-network/agent-integrations`. |
|
|
116
|
+
| [`/eval`](src/eval) | `producedFromToolEvents` (bridge tool events into the eval verifier) and `createTokenRecallChecker` (deterministic content check). Re-exports `@tangle-network/agent-eval`'s `verifyCompletion`, `extractProducedState`, `weightedComposite`, `createLlmCorrectnessChecker`. |
|
|
117
|
+
| [`/tangle`](src/tangle) | App-registration consent URL (`buildConsentUrl`) and a cached, auto-refreshing broker-token provider (`createBrokerTokenProvider`). Structural over the tcloud client. |
|
|
118
|
+
| [`/billing`](src/billing) | `createWorkspaceKeyManager` — mint / rotate / roll over / report usage on per-workspace, budget-capped model keys. Seams for provisioner, store, and crypto. |
|
|
119
|
+
| [`/delegation`](src/delegation) | `buildDelegationMcpServer` — the agent-runtime driven-loop MCP (`delegate_research`, `delegate_code`, `delegation_status`) for multi-step work that runs to completion in its own sandbox. Opt-in. |
|
|
120
|
+
| [`/crypto`](src/crypto) | AES-GCM field encryption: `encryptAesGcm`, `decryptAesGcm`, `createFieldCrypto`. Key supplied by the caller. |
|
|
121
|
+
| [`/web`](src/web) | Request-boundary utilities: `parseJsonObjectBody`, `requireString`, `extractRequestContext`, `checkRateLimit`, `addSecurityHeaders`. |
|
|
122
|
+
| [`/stream`](src/stream) | SSE normalization and turn identity: `normalizeToolEvent`, `resolveChatTurn`, `encodeEvent`, message-part merging. |
|
|
123
|
+
| [`/redact`](src/redact) | `redactForIngestion` — PII redaction before content leaves the boundary. |
|
|
47
124
|
|
|
48
|
-
|
|
125
|
+
The root entry (`@tangle-network/agent-app`) re-exports every module, but importing the subpath keeps your bundle to what you use.
|
|
49
126
|
|
|
50
|
-
|
|
127
|
+
## Compatibility
|
|
51
128
|
|
|
52
|
-
|
|
129
|
+
- **ESM only.** Ships `import` + `types` conditions per subpath.
|
|
130
|
+
- **Runtimes:** Node ≥ 20, Cloudflare Workers / edge, and the browser (the substrate-free modules use only Web-standard APIs — `fetch`, Web Crypto, `TextEncoder`).
|
|
131
|
+
- **TypeScript:** strict; full `.d.ts` for every entry point.
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
53
134
|
|
|
54
135
|
```bash
|
|
55
136
|
pnpm install
|
|
56
137
|
pnpm typecheck && pnpm test && pnpm build
|
|
57
138
|
```
|
|
58
139
|
|
|
59
|
-
Build
|
|
140
|
+
Build is [tsup](https://tsup.egoist.dev) (ESM + `.d.ts`), tests are [vitest](https://vitest.dev). A change keeps the suite green and follows the layering rule above — anything engine-general is contributed down to the substrate, not duplicated here. See [AGENTS.md](./AGENTS.md) for the full contributor contract.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
[MIT](./LICENSE)
|
package/dist/billing/index.d.ts
CHANGED
|
@@ -103,6 +103,99 @@ interface WorkspaceKeyManager {
|
|
|
103
103
|
/** Live budget usage for the active key (drives the "$X of $Y used" panel). */
|
|
104
104
|
getUsage(workspaceId: string): Promise<WorkspaceModelKeyUsage | null>;
|
|
105
105
|
}
|
|
106
|
+
/** A user's resolved platform identity (from the app's SSO account store). */
|
|
107
|
+
interface PlatformIdentity {
|
|
108
|
+
platformUserId: string;
|
|
109
|
+
/** The user's per-user platform API key (reads), or null when unlinked. */
|
|
110
|
+
apiKey: string | null;
|
|
111
|
+
}
|
|
112
|
+
/** Spendable balance for a platform user. */
|
|
113
|
+
interface PlatformBalanceInfo {
|
|
114
|
+
balance: number;
|
|
115
|
+
lifetimeSpent: number;
|
|
116
|
+
}
|
|
117
|
+
/** Per-product spend aggregate. */
|
|
118
|
+
interface PlatformProductUsage {
|
|
119
|
+
product: string | null;
|
|
120
|
+
totalSpent: number;
|
|
121
|
+
count: number;
|
|
122
|
+
}
|
|
123
|
+
/** Plan limits — a PARAMETER per product (dollar allowance, concurrency,
|
|
124
|
+
* overage policy). Never baked into the framework. */
|
|
125
|
+
interface PlanLimit {
|
|
126
|
+
monthlyBalanceUsd: number;
|
|
127
|
+
concurrency: number;
|
|
128
|
+
overageAllowed: boolean;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The platform billing transport — the product wires these to id.tangle.tools
|
|
132
|
+
* (or any balance backend). Reads authenticate as the user (their `apiKey`);
|
|
133
|
+
* the deduct write is a service-token call naming the target user. This module
|
|
134
|
+
* never touches HTTP — it only sequences these calls.
|
|
135
|
+
*/
|
|
136
|
+
interface PlatformBillingClient<Plan extends string> {
|
|
137
|
+
/** Resolve the user's platform identity, or null when there is no SSO account. */
|
|
138
|
+
resolveIdentity(userId: string): Promise<PlatformIdentity | null>;
|
|
139
|
+
/** Subscription plan for the user (via their platform key). */
|
|
140
|
+
getPlan(apiKey: string): Promise<Plan>;
|
|
141
|
+
/** Spendable balance for the user (via their platform key). */
|
|
142
|
+
getBalance(apiKey: string): Promise<PlatformBalanceInfo>;
|
|
143
|
+
/** Per-product usage rows for the user (via their platform key). */
|
|
144
|
+
getUsageByProduct(apiKey: string): Promise<PlatformProductUsage[]>;
|
|
145
|
+
/** Deduct spend against the user's balance (service-token write). */
|
|
146
|
+
deduct(input: {
|
|
147
|
+
platformUserId: string;
|
|
148
|
+
amountUsd: number;
|
|
149
|
+
type: string;
|
|
150
|
+
description: string;
|
|
151
|
+
referenceId: string;
|
|
152
|
+
}): Promise<void>;
|
|
153
|
+
}
|
|
154
|
+
interface SharedBillingState<Plan extends string> {
|
|
155
|
+
/** Platform user id, or null when the user has no Tangle SSO account. */
|
|
156
|
+
platformUserId: string | null;
|
|
157
|
+
plan: Plan;
|
|
158
|
+
monthlyBalanceUsd: number;
|
|
159
|
+
remainingBalanceUsd: number;
|
|
160
|
+
lifetimeSpentUsd: number;
|
|
161
|
+
concurrency: number;
|
|
162
|
+
overageAllowed: boolean;
|
|
163
|
+
}
|
|
164
|
+
interface PlatformBalanceManagerOptions<Plan extends string> {
|
|
165
|
+
client: PlatformBillingClient<Plan>;
|
|
166
|
+
/** Plan → limits map (the product's pricing). */
|
|
167
|
+
planLimits: Record<Plan, PlanLimit>;
|
|
168
|
+
/** The plan an unlinked / outage user falls to (fails CLOSED). */
|
|
169
|
+
freePlan: Plan;
|
|
170
|
+
/** The product slug to attribute usage to (for `getProductUsage`). */
|
|
171
|
+
productSlug: string;
|
|
172
|
+
}
|
|
173
|
+
interface PlatformBalanceManager<Plan extends string> {
|
|
174
|
+
/** Resolve the user's plan + balance. Unlinked or platform-outage users fail
|
|
175
|
+
* CLOSED: free plan, zero remaining balance — a billable run is never started
|
|
176
|
+
* against an unknown balance. */
|
|
177
|
+
getState(userId: string): Promise<SharedBillingState<Plan>>;
|
|
178
|
+
/** Gate a billable turn: allowed when the plan permits overage or remaining
|
|
179
|
+
* balance is positive. Returns the state so the caller deducts against it. */
|
|
180
|
+
canStartBillableTurn(userId: string): Promise<{
|
|
181
|
+
allowed: boolean;
|
|
182
|
+
state: SharedBillingState<Plan>;
|
|
183
|
+
}>;
|
|
184
|
+
/** Deduct `amountUsd` against the user's platform balance. Throws when the
|
|
185
|
+
* user is not platform-linked. */
|
|
186
|
+
deduct(userId: string, params: {
|
|
187
|
+
amountUsd: number;
|
|
188
|
+
type: string;
|
|
189
|
+
description: string;
|
|
190
|
+
referenceId: string;
|
|
191
|
+
}): Promise<void>;
|
|
192
|
+
/** This product's spend for the user (drives a usage panel). */
|
|
193
|
+
getProductUsage(userId: string): Promise<{
|
|
194
|
+
spentUsd: number;
|
|
195
|
+
transactionCount: number;
|
|
196
|
+
}>;
|
|
197
|
+
}
|
|
198
|
+
declare function createPlatformBalanceManager<Plan extends string>(opts: PlatformBalanceManagerOptions<Plan>): PlatformBalanceManager<Plan>;
|
|
106
199
|
declare function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager;
|
|
107
200
|
|
|
108
|
-
export { type KeyCrypto, type KeyProvisioner, type WorkspaceKeyManager, type WorkspaceKeyManagerOptions, type WorkspaceKeyRecord, type WorkspaceKeyStore, type WorkspaceModelKeyUsage, createWorkspaceKeyManager };
|
|
201
|
+
export { type KeyCrypto, type KeyProvisioner, type PlanLimit, type PlatformBalanceInfo, type PlatformBalanceManager, type PlatformBalanceManagerOptions, type PlatformBillingClient, type PlatformIdentity, type PlatformProductUsage, type SharedBillingState, type WorkspaceKeyManager, type WorkspaceKeyManagerOptions, type WorkspaceKeyRecord, type WorkspaceKeyStore, type WorkspaceModelKeyUsage, createPlatformBalanceManager, createWorkspaceKeyManager };
|
package/dist/billing/index.js
CHANGED
|
@@ -2,6 +2,60 @@
|
|
|
2
2
|
function nextPeriodEnd(now) {
|
|
3
3
|
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0));
|
|
4
4
|
}
|
|
5
|
+
function createPlatformBalanceManager(opts) {
|
|
6
|
+
const { client, planLimits, freePlan, productSlug } = opts;
|
|
7
|
+
const getState = async (userId) => {
|
|
8
|
+
const identity = await client.resolveIdentity(userId);
|
|
9
|
+
if (!identity || !identity.apiKey) {
|
|
10
|
+
const limits2 = planLimits[freePlan];
|
|
11
|
+
return {
|
|
12
|
+
platformUserId: identity?.platformUserId ?? null,
|
|
13
|
+
plan: freePlan,
|
|
14
|
+
monthlyBalanceUsd: limits2.monthlyBalanceUsd,
|
|
15
|
+
remainingBalanceUsd: 0,
|
|
16
|
+
lifetimeSpentUsd: 0,
|
|
17
|
+
concurrency: limits2.concurrency,
|
|
18
|
+
overageAllowed: limits2.overageAllowed
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const [plan, balance] = await Promise.all([client.getPlan(identity.apiKey), client.getBalance(identity.apiKey)]);
|
|
22
|
+
const limits = planLimits[plan];
|
|
23
|
+
return {
|
|
24
|
+
platformUserId: identity.platformUserId,
|
|
25
|
+
plan,
|
|
26
|
+
monthlyBalanceUsd: limits.monthlyBalanceUsd,
|
|
27
|
+
remainingBalanceUsd: balance.balance,
|
|
28
|
+
lifetimeSpentUsd: balance.lifetimeSpent,
|
|
29
|
+
concurrency: limits.concurrency,
|
|
30
|
+
overageAllowed: limits.overageAllowed
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
const canStartBillableTurn = async (userId) => {
|
|
34
|
+
const state = await getState(userId);
|
|
35
|
+
if (!state.platformUserId) return { allowed: false, state };
|
|
36
|
+
const allowed = state.overageAllowed || state.remainingBalanceUsd > 0;
|
|
37
|
+
return { allowed, state };
|
|
38
|
+
};
|
|
39
|
+
const deduct = async (userId, params) => {
|
|
40
|
+
const identity = await client.resolveIdentity(userId);
|
|
41
|
+
if (!identity) throw new Error("Shared billing requires a platform-linked user");
|
|
42
|
+
await client.deduct({
|
|
43
|
+
platformUserId: identity.platformUserId,
|
|
44
|
+
amountUsd: params.amountUsd,
|
|
45
|
+
type: params.type,
|
|
46
|
+
description: params.description,
|
|
47
|
+
referenceId: params.referenceId
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const getProductUsage = async (userId) => {
|
|
51
|
+
const identity = await client.resolveIdentity(userId);
|
|
52
|
+
if (!identity?.apiKey) return { spentUsd: 0, transactionCount: 0 };
|
|
53
|
+
const rows = await client.getUsageByProduct(identity.apiKey);
|
|
54
|
+
const product = rows.find((row) => row.product === productSlug);
|
|
55
|
+
return { spentUsd: product?.totalSpent ?? 0, transactionCount: product?.count ?? 0 };
|
|
56
|
+
};
|
|
57
|
+
return { getState, canStartBillableTurn, deduct, getProductUsage };
|
|
58
|
+
}
|
|
5
59
|
function createWorkspaceKeyManager(opts) {
|
|
6
60
|
const clock = opts.now ?? (() => /* @__PURE__ */ new Date());
|
|
7
61
|
const product = opts.product ?? "router";
|
|
@@ -57,6 +111,7 @@ function createWorkspaceKeyManager(opts) {
|
|
|
57
111
|
}
|
|
58
112
|
|
|
59
113
|
export {
|
|
114
|
+
createPlatformBalanceManager,
|
|
60
115
|
createWorkspaceKeyManager
|
|
61
116
|
};
|
|
62
|
-
//# sourceMappingURL=chunk-
|
|
117
|
+
//# sourceMappingURL=chunk-EAJSWUU5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/billing/index.ts"],"sourcesContent":["/**\n * Per-workspace budget-capped model keys — app-owned billing, metered on Tangle.\n *\n * Each workspace (the paying entity) runs the agent on its OWN child API key\n * minted from the platform parent key. The child carries a hard USD budget the\n * Tangle Router enforces AT THE KEY — model spend can't exceed the allowance,\n * zero app-side accounting. The app charges its own subscription (e.g. 5× the\n * allowance) and re-provisions each period. Child budgets are IMMUTABLE on the\n * platform, so a new budget = a fresh key + revoke the prior (rotate).\n *\n * The mint / rotate / rollover / usage LOGIC is generic and lives here.\n * Persistence (which D1 table), secret encryption, and key provisioning are\n * SEAMS each product supplies — so this module imports no DB and no key-mgmt\n * SDK (structural contracts only, like `../tangle`). The `@tangle-network/tcloud`\n * SDK is the provisioner a product passes in; it is not a dependency here.\n */\n\n/** The key-provisioning operations this needs — the `@tangle-network/tcloud`\n * SDK's `TCloudClient` satisfies it structurally; pass it in. */\nexport interface KeyProvisioner {\n createKey(input: { name: string; product: string; budgetUsd: number; expiresAt: string }): Promise<{ id?: string; key?: string }>\n revokeKey(keyId: string): Promise<unknown>\n getKey(keyId: string): Promise<{ budgetUsd?: number; budgetSpent?: number; expiresAt?: string | null }>\n}\n\n/** A stored child-key record (the app's row, shape-normalized). */\nexport interface WorkspaceKeyRecord {\n /** App row id (opaque). */\n id: string\n keyId: string\n /** The encrypted secret — decrypted via {@link KeyCrypto.decrypt}. */\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date | null\n}\n\n/** Persistence seam — the product implements this against its own D1 table. */\nexport interface WorkspaceKeyStore {\n /** Most-recent active key for the workspace, or null. */\n getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null>\n /** All active keys (to revoke priors on rotate). */\n listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>>\n /** Persist a freshly minted active key. */\n insert(record: { workspaceId: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: Date }): Promise<void>\n /** Mark a prior row revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n}\n\n/** Secret encryption seam (the app's at-rest crypto). */\nexport interface KeyCrypto {\n encrypt(secret: string): Promise<string>\n decrypt(encrypted: string): Promise<string>\n}\n\nexport interface WorkspaceKeyManagerOptions {\n provisioner: KeyProvisioner\n store: WorkspaceKeyStore\n crypto: KeyCrypto\n /** Default monthly allowance (USD) when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. Default `() => new Date()`. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\nexport interface WorkspaceModelKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string | null\n exhausted: boolean\n}\n\nexport interface WorkspaceKeyManager {\n /** The workspace's active child-key secret, provisioning one if absent/expired. */\n ensureKey(workspaceId: string, opts?: { budgetUsd?: number }): Promise<string>\n /** Mint a fresh key + revoke priors (period renewal / top-up). `rollover`\n * carries the prior key's unused budget into the new one, bounded by\n * `rolloverCapUsd`. Returns the new secret. */\n rotateKey(workspaceId: string, opts?: { budgetUsd?: number; rollover?: boolean; rolloverCapUsd?: number }): Promise<string>\n /** Live budget usage for the active key (drives the \"$X of $Y used\" panel). */\n getUsage(workspaceId: string): Promise<WorkspaceModelKeyUsage | null>\n}\n\n/** Period end = first day of next month, midnight UTC. Keys expire at the period\n * boundary so a forgotten rotation fails closed rather than running free. */\nfunction nextPeriodEnd(now: Date): Date {\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0))\n}\n\n// ---------------------------------------------------------------------------\n// Shared-platform-balance billing\n//\n// A DIFFERENT model from the per-workspace child-key manager above: here every\n// user runs against a SHARED platform balance (id.tangle.tools), keyed by the\n// user's platform identity. The app owns no key minting — it reads the balance,\n// gates a billable turn, and deducts spend through the platform billing API.\n// Plan limits, the platform transport, and identity resolution are SEAMS the\n// product supplies; this module imports no DB and no HTTP client.\n// ---------------------------------------------------------------------------\n\n/** A user's resolved platform identity (from the app's SSO account store). */\nexport interface PlatformIdentity {\n platformUserId: string\n /** The user's per-user platform API key (reads), or null when unlinked. */\n apiKey: string | null\n}\n\n/** Spendable balance for a platform user. */\nexport interface PlatformBalanceInfo {\n balance: number\n lifetimeSpent: number\n}\n\n/** Per-product spend aggregate. */\nexport interface PlatformProductUsage {\n product: string | null\n totalSpent: number\n count: number\n}\n\n/** Plan limits — a PARAMETER per product (dollar allowance, concurrency,\n * overage policy). Never baked into the framework. */\nexport interface PlanLimit {\n monthlyBalanceUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\n/**\n * The platform billing transport — the product wires these to id.tangle.tools\n * (or any balance backend). Reads authenticate as the user (their `apiKey`);\n * the deduct write is a service-token call naming the target user. This module\n * never touches HTTP — it only sequences these calls.\n */\nexport interface PlatformBillingClient<Plan extends string> {\n /** Resolve the user's platform identity, or null when there is no SSO account. */\n resolveIdentity(userId: string): Promise<PlatformIdentity | null>\n /** Subscription plan for the user (via their platform key). */\n getPlan(apiKey: string): Promise<Plan>\n /** Spendable balance for the user (via their platform key). */\n getBalance(apiKey: string): Promise<PlatformBalanceInfo>\n /** Per-product usage rows for the user (via their platform key). */\n getUsageByProduct(apiKey: string): Promise<PlatformProductUsage[]>\n /** Deduct spend against the user's balance (service-token write). */\n deduct(input: { platformUserId: string; amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n}\n\nexport interface SharedBillingState<Plan extends string> {\n /** Platform user id, or null when the user has no Tangle SSO account. */\n platformUserId: string | null\n plan: Plan\n monthlyBalanceUsd: number\n remainingBalanceUsd: number\n lifetimeSpentUsd: number\n concurrency: number\n overageAllowed: boolean\n}\n\nexport interface PlatformBalanceManagerOptions<Plan extends string> {\n client: PlatformBillingClient<Plan>\n /** Plan → limits map (the product's pricing). */\n planLimits: Record<Plan, PlanLimit>\n /** The plan an unlinked / outage user falls to (fails CLOSED). */\n freePlan: Plan\n /** The product slug to attribute usage to (for `getProductUsage`). */\n productSlug: string\n}\n\nexport interface PlatformBalanceManager<Plan extends string> {\n /** Resolve the user's plan + balance. Unlinked or platform-outage users fail\n * CLOSED: free plan, zero remaining balance — a billable run is never started\n * against an unknown balance. */\n getState(userId: string): Promise<SharedBillingState<Plan>>\n /** Gate a billable turn: allowed when the plan permits overage or remaining\n * balance is positive. Returns the state so the caller deducts against it. */\n canStartBillableTurn(userId: string): Promise<{ allowed: boolean; state: SharedBillingState<Plan> }>\n /** Deduct `amountUsd` against the user's platform balance. Throws when the\n * user is not platform-linked. */\n deduct(userId: string, params: { amountUsd: number; type: string; description: string; referenceId: string }): Promise<void>\n /** This product's spend for the user (drives a usage panel). */\n getProductUsage(userId: string): Promise<{ spentUsd: number; transactionCount: number }>\n}\n\nexport function createPlatformBalanceManager<Plan extends string>(\n opts: PlatformBalanceManagerOptions<Plan>,\n): PlatformBalanceManager<Plan> {\n const { client, planLimits, freePlan, productSlug } = opts\n\n const getState: PlatformBalanceManager<Plan>['getState'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n // No SSO account, or linked without a platform key: unlinked free tier with\n // zero balance. Reads require the user's key — never call them empty.\n if (!identity || !identity.apiKey) {\n const limits = planLimits[freePlan]\n return {\n platformUserId: identity?.platformUserId ?? null,\n plan: freePlan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: 0,\n lifetimeSpentUsd: 0,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n const [plan, balance] = await Promise.all([client.getPlan(identity.apiKey), client.getBalance(identity.apiKey)])\n const limits = planLimits[plan]\n return {\n platformUserId: identity.platformUserId,\n plan,\n monthlyBalanceUsd: limits.monthlyBalanceUsd,\n remainingBalanceUsd: balance.balance,\n lifetimeSpentUsd: balance.lifetimeSpent,\n concurrency: limits.concurrency,\n overageAllowed: limits.overageAllowed,\n }\n }\n\n const canStartBillableTurn: PlatformBalanceManager<Plan>['canStartBillableTurn'] = async (userId) => {\n const state = await getState(userId)\n if (!state.platformUserId) return { allowed: false, state }\n const allowed = state.overageAllowed || state.remainingBalanceUsd > 0\n return { allowed, state }\n }\n\n const deduct: PlatformBalanceManager<Plan>['deduct'] = async (userId, params) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity) throw new Error('Shared billing requires a platform-linked user')\n await client.deduct({\n platformUserId: identity.platformUserId,\n amountUsd: params.amountUsd,\n type: params.type,\n description: params.description,\n referenceId: params.referenceId,\n })\n }\n\n const getProductUsage: PlatformBalanceManager<Plan>['getProductUsage'] = async (userId) => {\n const identity = await client.resolveIdentity(userId)\n if (!identity?.apiKey) return { spentUsd: 0, transactionCount: 0 }\n const rows = await client.getUsageByProduct(identity.apiKey)\n const product = rows.find((row) => row.product === productSlug)\n return { spentUsd: product?.totalSpent ?? 0, transactionCount: product?.count ?? 0 }\n }\n\n return { getState, canStartBillableTurn, deduct, getProductUsage }\n}\n\nexport function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager {\n const clock = opts.now ?? (() => new Date())\n const product = opts.product ?? 'router'\n\n const getUsage: WorkspaceKeyManager['getUsage'] = async (workspaceId) => {\n const active = await opts.store.getActive(workspaceId)\n if (!active) return null\n const info = await opts.provisioner.getKey(active.keyId)\n const budgetUsd = info.budgetUsd ?? active.budgetUsd\n const budgetSpent = info.budgetSpent ?? 0\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: active.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt: info.expiresAt ?? (active.expiresAt ? active.expiresAt.toISOString() : null),\n exhausted: budgetRemaining <= 0,\n }\n }\n\n const rotateKey: WorkspaceKeyManager['rotateKey'] = async (workspaceId, ropts) => {\n const now = clock()\n const allowance = ropts?.budgetUsd ?? opts.defaultBudgetUsd\n\n let budgetUsd = allowance\n if (ropts?.rollover) {\n const prior = await getUsage(workspaceId).catch(() => null)\n budgetUsd = allowance + (prior?.budgetRemaining ?? 0)\n if (ropts.rolloverCapUsd != null) budgetUsd = Math.min(budgetUsd, ropts.rolloverCapUsd)\n }\n\n const expiresAt = nextPeriodEnd(now)\n const created = await opts.provisioner.createKey({ name: `ws:${workspaceId}`, product, budgetUsd, expiresAt: expiresAt.toISOString() })\n if (!created.key || !created.id) throw new Error('tcloud createKey returned no key')\n const keyEncrypted = await opts.crypto.encrypt(created.key)\n\n const priors = await opts.store.listActive(workspaceId)\n await opts.store.insert({ workspaceId, keyId: created.id, keyEncrypted, budgetUsd, expiresAt })\n for (const p of priors) {\n await opts.store.markRevoked(p.id, now)\n // Best-effort upstream revoke — the row is already revoked and an expired\n // key fails closed regardless, so a transient error is non-fatal.\n try {\n await opts.provisioner.revokeKey(p.keyId)\n } catch {\n /* non-fatal */\n }\n }\n return created.key\n }\n\n const ensureKey: WorkspaceKeyManager['ensureKey'] = async (workspaceId, eopts) => {\n const now = clock()\n const active = await opts.store.getActive(workspaceId)\n if (active && (!active.expiresAt || active.expiresAt.getTime() > now.getTime())) {\n return opts.crypto.decrypt(active.keyEncrypted)\n }\n return rotateKey(workspaceId, { budgetUsd: eopts?.budgetUsd })\n }\n\n return { ensureKey, rotateKey, getUsage }\n}\n"],"mappings":"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","names":["limits"]}
|
|
@@ -47,11 +47,56 @@ function createFieldCrypto(key) {
|
|
|
47
47
|
decrypt: (s) => decryptAesGcm(s, resolve())
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
async function deriveKey(secret, opts) {
|
|
51
|
+
const salt = typeof opts.salt === "string" ? new TextEncoder().encode(opts.salt) : opts.salt;
|
|
52
|
+
const keyMaterial = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), "PBKDF2", false, ["deriveKey"]);
|
|
53
|
+
return crypto.subtle.deriveKey(
|
|
54
|
+
{ name: "PBKDF2", salt, iterations: opts.iterations, hash: opts.hash ?? "SHA-256" },
|
|
55
|
+
keyMaterial,
|
|
56
|
+
{ name: ALGORITHM, length: 256 },
|
|
57
|
+
false,
|
|
58
|
+
["encrypt", "decrypt"]
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
async function encryptWithKey(plaintext, key) {
|
|
62
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
63
|
+
const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, new TextEncoder().encode(plaintext));
|
|
64
|
+
const out = new Uint8Array(IV_LENGTH + ciphertext.byteLength);
|
|
65
|
+
out.set(iv, 0);
|
|
66
|
+
out.set(new Uint8Array(ciphertext), IV_LENGTH);
|
|
67
|
+
return toBase64(out);
|
|
68
|
+
}
|
|
69
|
+
async function decryptWithKey(encoded, key) {
|
|
70
|
+
const raw = fromBase64(encoded);
|
|
71
|
+
const iv = raw.slice(0, IV_LENGTH);
|
|
72
|
+
const ciphertext = raw.slice(IV_LENGTH);
|
|
73
|
+
const plain = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext);
|
|
74
|
+
return new TextDecoder().decode(plain);
|
|
75
|
+
}
|
|
76
|
+
async function encryptBytes(data, key) {
|
|
77
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
78
|
+
const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, data);
|
|
79
|
+
const out = new Uint8Array(IV_LENGTH + ciphertext.byteLength);
|
|
80
|
+
out.set(iv, 0);
|
|
81
|
+
out.set(new Uint8Array(ciphertext), IV_LENGTH);
|
|
82
|
+
return out.buffer;
|
|
83
|
+
}
|
|
84
|
+
async function decryptBytes(data, key) {
|
|
85
|
+
const raw = new Uint8Array(data);
|
|
86
|
+
const iv = raw.slice(0, IV_LENGTH);
|
|
87
|
+
const ciphertext = raw.slice(IV_LENGTH);
|
|
88
|
+
return crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext);
|
|
89
|
+
}
|
|
50
90
|
|
|
51
91
|
export {
|
|
52
92
|
decodeHexKey,
|
|
53
93
|
encryptAesGcm,
|
|
54
94
|
decryptAesGcm,
|
|
55
|
-
createFieldCrypto
|
|
95
|
+
createFieldCrypto,
|
|
96
|
+
deriveKey,
|
|
97
|
+
encryptWithKey,
|
|
98
|
+
decryptWithKey,
|
|
99
|
+
encryptBytes,
|
|
100
|
+
decryptBytes
|
|
56
101
|
};
|
|
57
|
-
//# sourceMappingURL=chunk-
|
|
102
|
+
//# sourceMappingURL=chunk-ZJGY7OMZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/crypto/index.ts"],"sourcesContent":["/**\n * AES-256-GCM field encryption (for PII at rest — SSN/EIN/ID numbers, secrets).\n * WebCrypto only — runs on Cloudflare Workers, Node, and the browser with no\n * Node `crypto` dependency. The 32-byte key is a PARAMETER (64-char hex); the\n * framework never reads env — the product binds its own `ENCRYPTION_KEY` (this\n * is the concrete impl behind the `KeyCrypto` seam in `../billing`).\n *\n * Wire format: base64(iv ‖ ciphertext ‖ tag) — the 12-byte IV is prepended; the\n * GCM auth tag is appended by WebCrypto inside the ciphertext.\n */\n\nconst IV_LENGTH = 12\nconst TAG_LENGTH = 16\nconst ALGORITHM = 'AES-GCM'\n\n/** Validate + decode a 64-char hex key to 32 bytes. Throws on the wrong shape so\n * a misconfigured key fails loud, never silently weakens encryption. */\nexport function decodeHexKey(keyHex: string): Uint8Array {\n if (keyHex.length !== 64) throw new Error('encryption key must be a 64-char hex string (32 bytes)')\n const bytes = new Uint8Array(32)\n for (let i = 0; i < 64; i += 2) bytes[i / 2] = parseInt(keyHex.substring(i, i + 2), 16)\n return bytes\n}\n\nasync function importKey(keyHex: string): Promise<CryptoKey> {\n const raw = decodeHexKey(keyHex)\n return crypto.subtle.importKey('raw', raw.buffer as ArrayBuffer, { name: ALGORITHM } as Algorithm, false, ['encrypt', 'decrypt'])\n}\n\nfunction toBase64(data: Uint8Array): string {\n let binary = ''\n for (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i]!)\n return btoa(binary)\n}\n\nfunction fromBase64(b64: string): Uint8Array {\n const binary = atob(b64)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)\n return bytes\n}\n\n/** Encrypt `plaintext` with AES-256-GCM under `keyHex`. Returns\n * base64(iv ‖ ciphertext ‖ tag). A fresh random IV per call. */\nexport async function encryptAesGcm(plaintext: string, keyHex: string): Promise<string> {\n const key = await importKey(keyHex)\n const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))\n const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH * 8 }, key, new TextEncoder().encode(plaintext))\n const result = new Uint8Array(IV_LENGTH + ciphertext.byteLength)\n result.set(iv, 0)\n result.set(new Uint8Array(ciphertext), IV_LENGTH)\n return toBase64(result)\n}\n\n/** Decrypt a base64(iv ‖ ciphertext ‖ tag) string under `keyHex`. Throws if the\n * tag fails (tamper/wrong key). */\nexport async function decryptAesGcm(encrypted: string, keyHex: string): Promise<string> {\n const key = await importKey(keyHex)\n const data = fromBase64(encrypted)\n const iv = data.slice(0, IV_LENGTH)\n const ciphertext = data.slice(IV_LENGTH)\n const plain = await crypto.subtle.decrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH * 8 }, key, ciphertext)\n return new TextDecoder().decode(plain)\n}\n\n/** Build a {@link import('../billing').KeyCrypto}-compatible pair bound to a key\n * (or a key-resolver, for env-backed keys resolved per call). */\nexport function createFieldCrypto(key: string | (() => string)): { encrypt(s: string): Promise<string>; decrypt(s: string): Promise<string> } {\n const resolve = typeof key === 'function' ? key : () => key\n return {\n encrypt: (s) => encryptAesGcm(s, resolve()),\n decrypt: (s) => decryptAesGcm(s, resolve()),\n }\n}\n\n/**\n * --- Passphrase-derived key path (PBKDF2 → AES-256-GCM CryptoKey) ---\n *\n * The `encryptAesGcm`/`decryptAesGcm` path takes a raw 64-char-hex key. Some\n * products instead bind a SECRET STRING (not a hex key) and derive the AES key\n * with PBKDF2 — and need a BINARY path (encrypting document bytes, not just\n * strings). Both are exposed here so a product never hand-rolls WebCrypto.\n *\n * The derivation parameters (salt, iterations) are PARAMETERS — a product pins\n * its own so the derived key bytes stay stable for data already at rest. The\n * default salt/iterations match the historical tax-agent contract, but any\n * product supplies its own via {@link DeriveKeyOptions}.\n */\n\nexport interface DeriveKeyOptions {\n /** PBKDF2 salt. A product MUST pin this — changing it changes the derived key\n * bytes and orphans every value already encrypted at rest. */\n salt: Uint8Array | string\n /** PBKDF2 iteration count. Pin it for the same reason as `salt`. */\n iterations: number\n /** PBKDF2 hash. Default `'SHA-256'`. */\n hash?: 'SHA-256' | 'SHA-384' | 'SHA-512'\n}\n\n/** Derive an AES-256-GCM `CryptoKey` from a secret string via PBKDF2. The key is\n * non-extractable and usable only for encrypt/decrypt. */\nexport async function deriveKey(secret: string, opts: DeriveKeyOptions): Promise<CryptoKey> {\n const salt = typeof opts.salt === 'string' ? new TextEncoder().encode(opts.salt) : opts.salt\n const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), 'PBKDF2', false, ['deriveKey'])\n return crypto.subtle.deriveKey(\n { name: 'PBKDF2', salt: salt as BufferSource, iterations: opts.iterations, hash: opts.hash ?? 'SHA-256' },\n keyMaterial,\n { name: ALGORITHM, length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\n/** Encrypt `plaintext` under a derived `CryptoKey`. Returns base64(iv ‖ ct ‖ tag). */\nexport async function encryptWithKey(plaintext: string, key: CryptoKey): Promise<string> {\n const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))\n const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, new TextEncoder().encode(plaintext))\n const out = new Uint8Array(IV_LENGTH + ciphertext.byteLength)\n out.set(iv, 0)\n out.set(new Uint8Array(ciphertext), IV_LENGTH)\n return toBase64(out)\n}\n\n/** Decrypt a base64(iv ‖ ct ‖ tag) string under a derived `CryptoKey`. */\nexport async function decryptWithKey(encoded: string, key: CryptoKey): Promise<string> {\n const raw = fromBase64(encoded)\n const iv = raw.slice(0, IV_LENGTH)\n const ciphertext = raw.slice(IV_LENGTH)\n const plain = await crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext)\n return new TextDecoder().decode(plain)\n}\n\n/** Encrypt binary data under a derived `CryptoKey`. Returns an ArrayBuffer:\n * 12-byte IV ‖ ciphertext ‖ 16-byte GCM tag (same wire layout as the string\n * path, raw bytes instead of base64). */\nexport async function encryptBytes(data: ArrayBuffer, key: CryptoKey): Promise<ArrayBuffer> {\n const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))\n const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv }, key, data)\n const out = new Uint8Array(IV_LENGTH + ciphertext.byteLength)\n out.set(iv, 0)\n out.set(new Uint8Array(ciphertext), IV_LENGTH)\n return out.buffer\n}\n\n/** Decrypt binary data (IV ‖ ciphertext ‖ tag) under a derived `CryptoKey`. */\nexport async function decryptBytes(data: ArrayBuffer, key: CryptoKey): Promise<ArrayBuffer> {\n const raw = new Uint8Array(data)\n const iv = raw.slice(0, IV_LENGTH)\n const ciphertext = raw.slice(IV_LENGTH)\n return crypto.subtle.decrypt({ name: ALGORITHM, iv }, key, ciphertext)\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/knowledge/index.ts
|
|
2
|
+
function clamp(value) {
|
|
3
|
+
if (!Number.isFinite(value)) return 0;
|
|
4
|
+
if (value < 0) return 0;
|
|
5
|
+
if (value > 1) return 1;
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function buildKnowledgeRequirements(specs, signals = {}) {
|
|
9
|
+
return specs.map((spec) => {
|
|
10
|
+
const signal = signals[spec.id];
|
|
11
|
+
return {
|
|
12
|
+
id: spec.id,
|
|
13
|
+
description: spec.description,
|
|
14
|
+
requiredFor: spec.requiredFor ?? [],
|
|
15
|
+
category: spec.category,
|
|
16
|
+
acquisitionMode: spec.acquisitionMode,
|
|
17
|
+
importance: spec.importance ?? "blocking",
|
|
18
|
+
freshness: spec.freshness ?? "static",
|
|
19
|
+
sensitivity: spec.sensitivity ?? "private",
|
|
20
|
+
confidenceNeeded: spec.confidenceNeeded ?? 1,
|
|
21
|
+
currentConfidence: clamp(signal?.confidence ?? 0),
|
|
22
|
+
evidenceIds: signal?.evidence ? [signal.evidence] : [],
|
|
23
|
+
fallbackPolicy: spec.acquisitionMode === "ask_user" ? "ask" : "block"
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function deriveSignals(specs, ctx) {
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const spec of specs) {
|
|
30
|
+
if (spec.derive) {
|
|
31
|
+
out[spec.id] = { confidence: clamp(await spec.derive(ctx)), evidence: spec.evidence };
|
|
32
|
+
} else if (spec.satisfiedBy) {
|
|
33
|
+
const ok = await evalRule(spec.satisfiedBy, ctx);
|
|
34
|
+
out[spec.id] = ok ? { confidence: 1, evidence: spec.evidence ?? describeRule(spec.satisfiedBy) } : { confidence: 0 };
|
|
35
|
+
} else {
|
|
36
|
+
out[spec.id] = { confidence: 0 };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
async function evalRule(rule, ctx) {
|
|
42
|
+
if ("anyOf" in rule) {
|
|
43
|
+
for (const sub of rule.anyOf) if (await evalRule(sub, ctx)) return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if ("allOf" in rule) {
|
|
47
|
+
for (const sub of rule.allOf) if (!await evalRule(sub, ctx)) return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if ("config" in rule) {
|
|
51
|
+
const value = ctx.config(rule.config);
|
|
52
|
+
if (rule.nonEmpty) return Array.isArray(value) ? value.length > 0 : value != null && value !== "";
|
|
53
|
+
return value != null && value !== "" && value !== false;
|
|
54
|
+
}
|
|
55
|
+
const rows = await ctx.count({ table: rule.table, where: rule.where, statusIn: rule.statusIn });
|
|
56
|
+
return rows >= (rule.minRows ?? 1);
|
|
57
|
+
}
|
|
58
|
+
function describeRule(rule) {
|
|
59
|
+
if ("anyOf" in rule) return `anyOf(${rule.anyOf.map(describeRule).join(",")})`;
|
|
60
|
+
if ("allOf" in rule) return `allOf(${rule.allOf.map(describeRule).join(",")})`;
|
|
61
|
+
if ("config" in rule) return `config:${rule.config}`;
|
|
62
|
+
return `${rule.table}${rule.statusIn ? `[${rule.statusIn.join("|")}]` : ""}>=${rule.minRows ?? 1}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
buildKnowledgeRequirements,
|
|
67
|
+
deriveSignals
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=chunk-ZXNXAQAH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/knowledge/index.ts"],"sourcesContent":["/**\n * Declarative knowledge-requirement gate.\n *\n * Every agent product hand-rolls the same pair: a `buildXKnowledgeRequirements`\n * declaring the requirements that gate its control loop, and a\n * `deriveXRuntimeKnowledge` that scores each one from workspace state. Across\n * the fleet those derives are uniformly \"is a config field set\" / \"are there\n * >= N rows in table T (optionally with a status filter)\" / \"any/all of the\n * above\" — data, not logic. This module makes both DATA: a spec list with\n * declarative `satisfiedBy` rules, plus a per-spec `derive` escape hatch for\n * the rare rule a declarative form can't express (e.g. an aggregate over a\n * JSON column).\n *\n * Substrate-free: the only seam is `KnowledgeStateAccessor` (a config lookup +\n * a row count), which the consumer's backend — or `agent-app/preset-cloudflare`\n * — implements. Emits `@tangle-network/agent-eval`'s `KnowledgeRequirement[]`,\n * exactly what the agent-runtime control loop consumes.\n */\n\nimport type {\n KnowledgeAcquisitionMode,\n KnowledgeFreshness,\n KnowledgeImportance,\n KnowledgeRequirement,\n KnowledgeRequirementCategory,\n KnowledgeSensitivity,\n} from '@tangle-network/agent-eval'\n\n/** A declarative rule for satisfying a requirement from workspace state. */\nexport type SatisfiedByRule =\n /** A workspace-config field (dot-path) is set. `nonEmpty` requires a\n * non-empty array/string rather than mere presence. */\n | { config: string; nonEmpty?: boolean }\n /** At least `minRows` (default 1) rows exist in `table` for the workspace,\n * optionally filtered to `statusIn`. `where` names the workspace fk column\n * the accessor scopes on (default: the accessor's convention). */\n | { table: string; where?: string; statusIn?: string[]; minRows?: number }\n | { anyOf: SatisfiedByRule[] }\n | { allOf: SatisfiedByRule[] }\n\nexport interface KnowledgeRequirementSpec {\n id: string\n description: string\n category: KnowledgeRequirementCategory\n acquisitionMode: KnowledgeAcquisitionMode\n importance?: KnowledgeImportance\n freshness?: KnowledgeFreshness\n sensitivity?: KnowledgeSensitivity\n confidenceNeeded?: number\n requiredFor?: string[]\n /** The data path — evaluated against the `KnowledgeStateAccessor`. */\n satisfiedBy?: SatisfiedByRule\n /** The escape hatch — a code derive for what a rule can't express. Wins\n * over `satisfiedBy` when both are present. Returns confidence in [0, 1]. */\n derive?: (ctx: KnowledgeStateAccessor) => number | Promise<number>\n /** Evidence id attached when satisfied (default: a description of the rule). */\n evidence?: string\n}\n\n/** The single seam a backend implements. `preset-cloudflare` provides a D1\n * implementation; a custom stack supplies its own. */\nexport interface KnowledgeStateAccessor {\n /** Resolve a workspace-config field value (dot-path), or undefined. */\n config: (path: string) => unknown\n /** Count rows in `table` for the active workspace, optionally status-filtered. */\n count: (query: { table: string; where?: string; statusIn?: string[] }) => number | Promise<number>\n}\n\nexport interface KnowledgeSignal {\n confidence: number\n evidence?: string\n}\n\nfunction clamp(value: number): number {\n if (!Number.isFinite(value)) return 0\n if (value < 0) return 0\n if (value > 1) return 1\n return value\n}\n\n/**\n * Map specs -> the runtime's `KnowledgeRequirement[]`, folding in per-spec\n * confidence from `signals` (default 0). Pure + sync: an eval harness can pass\n * hand-authored signals; production passes the output of {@link deriveSignals}.\n */\nexport function buildKnowledgeRequirements(\n specs: KnowledgeRequirementSpec[],\n signals: Record<string, KnowledgeSignal> = {},\n): KnowledgeRequirement[] {\n return specs.map((spec) => {\n const signal = signals[spec.id]\n return {\n id: spec.id,\n description: spec.description,\n requiredFor: spec.requiredFor ?? [],\n category: spec.category,\n acquisitionMode: spec.acquisitionMode,\n importance: spec.importance ?? 'blocking',\n freshness: spec.freshness ?? 'static',\n sensitivity: spec.sensitivity ?? 'private',\n confidenceNeeded: spec.confidenceNeeded ?? 1,\n currentConfidence: clamp(signal?.confidence ?? 0),\n evidenceIds: signal?.evidence ? [signal.evidence] : [],\n fallbackPolicy: spec.acquisitionMode === 'ask_user' ? 'ask' : 'block',\n }\n })\n}\n\n/**\n * Score every spec from workspace state. `derive` (code) wins; otherwise the\n * declarative `satisfiedBy` rule is evaluated through the accessor; a spec with\n * neither scores 0 (an acquisition gate, e.g. `search_web`).\n */\nexport async function deriveSignals(\n specs: KnowledgeRequirementSpec[],\n ctx: KnowledgeStateAccessor,\n): Promise<Record<string, KnowledgeSignal>> {\n const out: Record<string, KnowledgeSignal> = {}\n for (const spec of specs) {\n if (spec.derive) {\n out[spec.id] = { confidence: clamp(await spec.derive(ctx)), evidence: spec.evidence }\n } else if (spec.satisfiedBy) {\n const ok = await evalRule(spec.satisfiedBy, ctx)\n out[spec.id] = ok\n ? { confidence: 1, evidence: spec.evidence ?? describeRule(spec.satisfiedBy) }\n : { confidence: 0 }\n } else {\n out[spec.id] = { confidence: 0 }\n }\n }\n return out\n}\n\nasync function evalRule(rule: SatisfiedByRule, ctx: KnowledgeStateAccessor): Promise<boolean> {\n if ('anyOf' in rule) {\n for (const sub of rule.anyOf) if (await evalRule(sub, ctx)) return true\n return false\n }\n if ('allOf' in rule) {\n for (const sub of rule.allOf) if (!(await evalRule(sub, ctx))) return false\n return true\n }\n if ('config' in rule) {\n const value = ctx.config(rule.config)\n if (rule.nonEmpty) return Array.isArray(value) ? value.length > 0 : value != null && value !== ''\n return value != null && value !== '' && value !== false\n }\n const rows = await ctx.count({ table: rule.table, where: rule.where, statusIn: rule.statusIn })\n return rows >= (rule.minRows ?? 1)\n}\n\nfunction describeRule(rule: SatisfiedByRule): string {\n if ('anyOf' in rule) return `anyOf(${rule.anyOf.map(describeRule).join(',')})`\n if ('allOf' in rule) return `allOf(${rule.allOf.map(describeRule).join(',')})`\n if ('config' in rule) return `config:${rule.config}`\n return `${rule.table}${rule.statusIn ? `[${rule.statusIn.join('|')}]` : ''}>=${rule.minRows ?? 1}`\n}\n"],"mappings":";AAyEA,SAAS,MAAM,OAAuB;AACpC,MAAI,CAAC,OAAO,SAAS,KAAK,EAAG,QAAO;AACpC,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,QAAQ,EAAG,QAAO;AACtB,SAAO;AACT;AAOO,SAAS,2BACd,OACA,UAA2C,CAAC,GACpB;AACxB,SAAO,MAAM,IAAI,CAAC,SAAS;AACzB,UAAM,SAAS,QAAQ,KAAK,EAAE;AAC9B,WAAO;AAAA,MACL,IAAI,KAAK;AAAA,MACT,aAAa,KAAK;AAAA,MAClB,aAAa,KAAK,eAAe,CAAC;AAAA,MAClC,UAAU,KAAK;AAAA,MACf,iBAAiB,KAAK;AAAA,MACtB,YAAY,KAAK,cAAc;AAAA,MAC/B,WAAW,KAAK,aAAa;AAAA,MAC7B,aAAa,KAAK,eAAe;AAAA,MACjC,kBAAkB,KAAK,oBAAoB;AAAA,MAC3C,mBAAmB,MAAM,QAAQ,cAAc,CAAC;AAAA,MAChD,aAAa,QAAQ,WAAW,CAAC,OAAO,QAAQ,IAAI,CAAC;AAAA,MACrD,gBAAgB,KAAK,oBAAoB,aAAa,QAAQ;AAAA,IAChE;AAAA,EACF,CAAC;AACH;AAOA,eAAsB,cACpB,OACA,KAC0C;AAC1C,QAAM,MAAuC,CAAC;AAC9C,aAAW,QAAQ,OAAO;AACxB,QAAI,KAAK,QAAQ;AACf,UAAI,KAAK,EAAE,IAAI,EAAE,YAAY,MAAM,MAAM,KAAK,OAAO,GAAG,CAAC,GAAG,UAAU,KAAK,SAAS;AAAA,IACtF,WAAW,KAAK,aAAa;AAC3B,YAAM,KAAK,MAAM,SAAS,KAAK,aAAa,GAAG;AAC/C,UAAI,KAAK,EAAE,IAAI,KACX,EAAE,YAAY,GAAG,UAAU,KAAK,YAAY,aAAa,KAAK,WAAW,EAAE,IAC3E,EAAE,YAAY,EAAE;AAAA,IACtB,OAAO;AACL,UAAI,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE;AAAA,IACjC;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,SAAS,MAAuB,KAA+C;AAC5F,MAAI,WAAW,MAAM;AACnB,eAAW,OAAO,KAAK,MAAO,KAAI,MAAM,SAAS,KAAK,GAAG,EAAG,QAAO;AACnE,WAAO;AAAA,EACT;AACA,MAAI,WAAW,MAAM;AACnB,eAAW,OAAO,KAAK,MAAO,KAAI,CAAE,MAAM,SAAS,KAAK,GAAG,EAAI,QAAO;AACtE,WAAO;AAAA,EACT;AACA,MAAI,YAAY,MAAM;AACpB,UAAM,QAAQ,IAAI,OAAO,KAAK,MAAM;AACpC,QAAI,KAAK,SAAU,QAAO,MAAM,QAAQ,KAAK,IAAI,MAAM,SAAS,IAAI,SAAS,QAAQ,UAAU;AAC/F,WAAO,SAAS,QAAQ,UAAU,MAAM,UAAU;AAAA,EACpD;AACA,QAAM,OAAO,MAAM,IAAI,MAAM,EAAE,OAAO,KAAK,OAAO,OAAO,KAAK,OAAO,UAAU,KAAK,SAAS,CAAC;AAC9F,SAAO,SAAS,KAAK,WAAW;AAClC;AAEA,SAAS,aAAa,MAA+B;AACnD,MAAI,WAAW,KAAM,QAAO,SAAS,KAAK,MAAM,IAAI,YAAY,EAAE,KAAK,GAAG,CAAC;AAC3E,MAAI,WAAW,KAAM,QAAO,SAAS,KAAK,MAAM,IAAI,YAAY,EAAE,KAAK,GAAG,CAAC;AAC3E,MAAI,YAAY,KAAM,QAAO,UAAU,KAAK,MAAM;AAClD,SAAO,GAAG,KAAK,KAAK,GAAG,KAAK,WAAW,IAAI,KAAK,SAAS,KAAK,GAAG,CAAC,MAAM,EAAE,KAAK,KAAK,WAAW,CAAC;AAClG;","names":[]}
|
package/dist/crypto/index.d.ts
CHANGED
|
@@ -23,5 +23,40 @@ declare function createFieldCrypto(key: string | (() => string)): {
|
|
|
23
23
|
encrypt(s: string): Promise<string>;
|
|
24
24
|
decrypt(s: string): Promise<string>;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* --- Passphrase-derived key path (PBKDF2 → AES-256-GCM CryptoKey) ---
|
|
28
|
+
*
|
|
29
|
+
* The `encryptAesGcm`/`decryptAesGcm` path takes a raw 64-char-hex key. Some
|
|
30
|
+
* products instead bind a SECRET STRING (not a hex key) and derive the AES key
|
|
31
|
+
* with PBKDF2 — and need a BINARY path (encrypting document bytes, not just
|
|
32
|
+
* strings). Both are exposed here so a product never hand-rolls WebCrypto.
|
|
33
|
+
*
|
|
34
|
+
* The derivation parameters (salt, iterations) are PARAMETERS — a product pins
|
|
35
|
+
* its own so the derived key bytes stay stable for data already at rest. The
|
|
36
|
+
* default salt/iterations match the historical tax-agent contract, but any
|
|
37
|
+
* product supplies its own via {@link DeriveKeyOptions}.
|
|
38
|
+
*/
|
|
39
|
+
interface DeriveKeyOptions {
|
|
40
|
+
/** PBKDF2 salt. A product MUST pin this — changing it changes the derived key
|
|
41
|
+
* bytes and orphans every value already encrypted at rest. */
|
|
42
|
+
salt: Uint8Array | string;
|
|
43
|
+
/** PBKDF2 iteration count. Pin it for the same reason as `salt`. */
|
|
44
|
+
iterations: number;
|
|
45
|
+
/** PBKDF2 hash. Default `'SHA-256'`. */
|
|
46
|
+
hash?: 'SHA-256' | 'SHA-384' | 'SHA-512';
|
|
47
|
+
}
|
|
48
|
+
/** Derive an AES-256-GCM `CryptoKey` from a secret string via PBKDF2. The key is
|
|
49
|
+
* non-extractable and usable only for encrypt/decrypt. */
|
|
50
|
+
declare function deriveKey(secret: string, opts: DeriveKeyOptions): Promise<CryptoKey>;
|
|
51
|
+
/** Encrypt `plaintext` under a derived `CryptoKey`. Returns base64(iv ‖ ct ‖ tag). */
|
|
52
|
+
declare function encryptWithKey(plaintext: string, key: CryptoKey): Promise<string>;
|
|
53
|
+
/** Decrypt a base64(iv ‖ ct ‖ tag) string under a derived `CryptoKey`. */
|
|
54
|
+
declare function decryptWithKey(encoded: string, key: CryptoKey): Promise<string>;
|
|
55
|
+
/** Encrypt binary data under a derived `CryptoKey`. Returns an ArrayBuffer:
|
|
56
|
+
* 12-byte IV ‖ ciphertext ‖ 16-byte GCM tag (same wire layout as the string
|
|
57
|
+
* path, raw bytes instead of base64). */
|
|
58
|
+
declare function encryptBytes(data: ArrayBuffer, key: CryptoKey): Promise<ArrayBuffer>;
|
|
59
|
+
/** Decrypt binary data (IV ‖ ciphertext ‖ tag) under a derived `CryptoKey`. */
|
|
60
|
+
declare function decryptBytes(data: ArrayBuffer, key: CryptoKey): Promise<ArrayBuffer>;
|
|
26
61
|
|
|
27
|
-
export { createFieldCrypto, decodeHexKey, decryptAesGcm, encryptAesGcm };
|
|
62
|
+
export { type DeriveKeyOptions, createFieldCrypto, decodeHexKey, decryptAesGcm, decryptBytes, decryptWithKey, deriveKey, encryptAesGcm, encryptBytes, encryptWithKey };
|
package/dist/crypto/index.js
CHANGED
|
@@ -2,12 +2,22 @@ import {
|
|
|
2
2
|
createFieldCrypto,
|
|
3
3
|
decodeHexKey,
|
|
4
4
|
decryptAesGcm,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
decryptBytes,
|
|
6
|
+
decryptWithKey,
|
|
7
|
+
deriveKey,
|
|
8
|
+
encryptAesGcm,
|
|
9
|
+
encryptBytes,
|
|
10
|
+
encryptWithKey
|
|
11
|
+
} from "../chunk-ZJGY7OMZ.js";
|
|
7
12
|
export {
|
|
8
13
|
createFieldCrypto,
|
|
9
14
|
decodeHexKey,
|
|
10
15
|
decryptAesGcm,
|
|
11
|
-
|
|
16
|
+
decryptBytes,
|
|
17
|
+
decryptWithKey,
|
|
18
|
+
deriveKey,
|
|
19
|
+
encryptAesGcm,
|
|
20
|
+
encryptBytes,
|
|
21
|
+
encryptWithKey
|
|
12
22
|
};
|
|
13
23
|
//# sourceMappingURL=index.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ export { BuildDelegationOptions, DELEGATION_MCP_SERVER_KEY, DELEGATION_TOOLS, De
|
|
|
4
4
|
export { BrokerToken, BrokerTokenMinter, BrokerTokenProvider, BrokerTokenProviderOptions, ConsentUrlInput, buildConsentUrl, createBrokerTokenProvider } from './tangle/index.js';
|
|
5
5
|
export { AppToolLoopOptions, DEFAULT_TANGLE_ROUTER_BASE_URL, LoopEvent, LoopToolCall, OpenAICompatStreamTurnOptions, OpenAIStreamChunk, ResolveModelOptions, StreamAppToolLoopOptions, StreamLoopYield, TangleModelConfig, ToolLoopResult, createOpenAICompatStreamTurn, resolveTangleModelConfig, runAppToolLoop, streamAppToolLoop, toLoopEvents } from './runtime/index.js';
|
|
6
6
|
export { createTokenRecallChecker, producedFromToolEvents } from './eval/index.js';
|
|
7
|
-
export {
|
|
8
|
-
export {
|
|
7
|
+
export { KnowledgeRequirementSpec, KnowledgeSignal, KnowledgeStateAccessor, SatisfiedByRule, buildKnowledgeRequirements, deriveSignals } from './knowledge/index.js';
|
|
8
|
+
export { KeyCrypto, KeyProvisioner, PlanLimit, PlatformBalanceInfo, PlatformBalanceManager, PlatformBalanceManagerOptions, PlatformBillingClient, PlatformIdentity, PlatformProductUsage, SharedBillingState, WorkspaceKeyManager, WorkspaceKeyManagerOptions, WorkspaceKeyRecord, WorkspaceKeyStore, WorkspaceModelKeyUsage, createPlatformBalanceManager, createWorkspaceKeyManager } from './billing/index.js';
|
|
9
|
+
export { DeriveKeyOptions, createFieldCrypto, decodeHexKey, decryptAesGcm, decryptBytes, decryptWithKey, deriveKey, encryptAesGcm, encryptBytes, encryptWithKey } from './crypto/index.js';
|
|
9
10
|
export { JsonRecord, PersistedChatMessageForTurn, ResolvedChatTurn, StreamEvent, asRecord, asString, buildUserTextParts, encodeEvent, finalizeAssistantParts, getPartKey, mergePersistedPart, messageHasTurnId, normalizeClientTurnId, normalizePersistedPart, normalizeTime, normalizeToolEvent, resolveChatTurn, resolveToolId, resolveToolName } from './stream/index.js';
|
|
10
11
|
export { HubExecClient, HubExecClientOptions, HubExecErrorCode, HubExecResult, HubInvokeDeps, HubInvokeInput, HubInvokeOutcome, ParsedIntegrationAction, invokeIntegrationHub, resolveIntegrationAction } from './integrations/index.js';
|
|
11
12
|
export { JsonObject, KvLike, RateLimitResult, RequestContext, SecurityHeaderOptions, addSecurityHeaders, checkRateLimit, extractRequestContext, parseJsonObjectBody, requireString } from './web/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createFieldCrypto,
|
|
3
|
+
decodeHexKey,
|
|
4
|
+
decryptAesGcm,
|
|
5
|
+
decryptBytes,
|
|
6
|
+
decryptWithKey,
|
|
7
|
+
deriveKey,
|
|
8
|
+
encryptAesGcm,
|
|
9
|
+
encryptBytes,
|
|
10
|
+
encryptWithKey
|
|
11
|
+
} from "./chunk-ZJGY7OMZ.js";
|
|
1
12
|
import {
|
|
2
13
|
asRecord,
|
|
3
14
|
asString,
|
|
@@ -74,14 +85,13 @@ import {
|
|
|
74
85
|
weightedComposite
|
|
75
86
|
} from "./chunk-4NXVI7PW.js";
|
|
76
87
|
import {
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
buildKnowledgeRequirements,
|
|
89
|
+
deriveSignals
|
|
90
|
+
} from "./chunk-ZXNXAQAH.js";
|
|
79
91
|
import {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
encryptAesGcm
|
|
84
|
-
} from "./chunk-SIDR6BH3.js";
|
|
92
|
+
createPlatformBalanceManager,
|
|
93
|
+
createWorkspaceKeyManager
|
|
94
|
+
} from "./chunk-EAJSWUU5.js";
|
|
85
95
|
export {
|
|
86
96
|
APP_TOOL_NAMES,
|
|
87
97
|
DEFAULT_APP_TOOL_PATHS,
|
|
@@ -100,6 +110,7 @@ export {
|
|
|
100
110
|
buildConsentUrl,
|
|
101
111
|
buildDelegationMcpServer,
|
|
102
112
|
buildHttpMcpServer,
|
|
113
|
+
buildKnowledgeRequirements,
|
|
103
114
|
buildUserTextParts,
|
|
104
115
|
checkRateLimit,
|
|
105
116
|
createAppToolRuntimeExecutor,
|
|
@@ -108,13 +119,20 @@ export {
|
|
|
108
119
|
createFieldCrypto,
|
|
109
120
|
createLlmCorrectnessChecker,
|
|
110
121
|
createOpenAICompatStreamTurn,
|
|
122
|
+
createPlatformBalanceManager,
|
|
111
123
|
createTokenRecallChecker,
|
|
112
124
|
createWorkspaceKeyManager,
|
|
113
125
|
decodeHexKey,
|
|
114
126
|
decryptAesGcm,
|
|
127
|
+
decryptBytes,
|
|
128
|
+
decryptWithKey,
|
|
129
|
+
deriveKey,
|
|
130
|
+
deriveSignals,
|
|
115
131
|
dispatchAppTool,
|
|
116
132
|
encodeEvent,
|
|
117
133
|
encryptAesGcm,
|
|
134
|
+
encryptBytes,
|
|
135
|
+
encryptWithKey,
|
|
118
136
|
extractProducedState,
|
|
119
137
|
extractRequestContext,
|
|
120
138
|
finalizeAssistantParts,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { KnowledgeRequirementCategory, KnowledgeAcquisitionMode, KnowledgeImportance, KnowledgeFreshness, KnowledgeSensitivity, KnowledgeRequirement } from '@tangle-network/agent-eval';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Declarative knowledge-requirement gate.
|
|
5
|
+
*
|
|
6
|
+
* Every agent product hand-rolls the same pair: a `buildXKnowledgeRequirements`
|
|
7
|
+
* declaring the requirements that gate its control loop, and a
|
|
8
|
+
* `deriveXRuntimeKnowledge` that scores each one from workspace state. Across
|
|
9
|
+
* the fleet those derives are uniformly "is a config field set" / "are there
|
|
10
|
+
* >= N rows in table T (optionally with a status filter)" / "any/all of the
|
|
11
|
+
* above" — data, not logic. This module makes both DATA: a spec list with
|
|
12
|
+
* declarative `satisfiedBy` rules, plus a per-spec `derive` escape hatch for
|
|
13
|
+
* the rare rule a declarative form can't express (e.g. an aggregate over a
|
|
14
|
+
* JSON column).
|
|
15
|
+
*
|
|
16
|
+
* Substrate-free: the only seam is `KnowledgeStateAccessor` (a config lookup +
|
|
17
|
+
* a row count), which the consumer's backend — or `agent-app/preset-cloudflare`
|
|
18
|
+
* — implements. Emits `@tangle-network/agent-eval`'s `KnowledgeRequirement[]`,
|
|
19
|
+
* exactly what the agent-runtime control loop consumes.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** A declarative rule for satisfying a requirement from workspace state. */
|
|
23
|
+
type SatisfiedByRule =
|
|
24
|
+
/** A workspace-config field (dot-path) is set. `nonEmpty` requires a
|
|
25
|
+
* non-empty array/string rather than mere presence. */
|
|
26
|
+
{
|
|
27
|
+
config: string;
|
|
28
|
+
nonEmpty?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/** At least `minRows` (default 1) rows exist in `table` for the workspace,
|
|
31
|
+
* optionally filtered to `statusIn`. `where` names the workspace fk column
|
|
32
|
+
* the accessor scopes on (default: the accessor's convention). */
|
|
33
|
+
| {
|
|
34
|
+
table: string;
|
|
35
|
+
where?: string;
|
|
36
|
+
statusIn?: string[];
|
|
37
|
+
minRows?: number;
|
|
38
|
+
} | {
|
|
39
|
+
anyOf: SatisfiedByRule[];
|
|
40
|
+
} | {
|
|
41
|
+
allOf: SatisfiedByRule[];
|
|
42
|
+
};
|
|
43
|
+
interface KnowledgeRequirementSpec {
|
|
44
|
+
id: string;
|
|
45
|
+
description: string;
|
|
46
|
+
category: KnowledgeRequirementCategory;
|
|
47
|
+
acquisitionMode: KnowledgeAcquisitionMode;
|
|
48
|
+
importance?: KnowledgeImportance;
|
|
49
|
+
freshness?: KnowledgeFreshness;
|
|
50
|
+
sensitivity?: KnowledgeSensitivity;
|
|
51
|
+
confidenceNeeded?: number;
|
|
52
|
+
requiredFor?: string[];
|
|
53
|
+
/** The data path — evaluated against the `KnowledgeStateAccessor`. */
|
|
54
|
+
satisfiedBy?: SatisfiedByRule;
|
|
55
|
+
/** The escape hatch — a code derive for what a rule can't express. Wins
|
|
56
|
+
* over `satisfiedBy` when both are present. Returns confidence in [0, 1]. */
|
|
57
|
+
derive?: (ctx: KnowledgeStateAccessor) => number | Promise<number>;
|
|
58
|
+
/** Evidence id attached when satisfied (default: a description of the rule). */
|
|
59
|
+
evidence?: string;
|
|
60
|
+
}
|
|
61
|
+
/** The single seam a backend implements. `preset-cloudflare` provides a D1
|
|
62
|
+
* implementation; a custom stack supplies its own. */
|
|
63
|
+
interface KnowledgeStateAccessor {
|
|
64
|
+
/** Resolve a workspace-config field value (dot-path), or undefined. */
|
|
65
|
+
config: (path: string) => unknown;
|
|
66
|
+
/** Count rows in `table` for the active workspace, optionally status-filtered. */
|
|
67
|
+
count: (query: {
|
|
68
|
+
table: string;
|
|
69
|
+
where?: string;
|
|
70
|
+
statusIn?: string[];
|
|
71
|
+
}) => number | Promise<number>;
|
|
72
|
+
}
|
|
73
|
+
interface KnowledgeSignal {
|
|
74
|
+
confidence: number;
|
|
75
|
+
evidence?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Map specs -> the runtime's `KnowledgeRequirement[]`, folding in per-spec
|
|
79
|
+
* confidence from `signals` (default 0). Pure + sync: an eval harness can pass
|
|
80
|
+
* hand-authored signals; production passes the output of {@link deriveSignals}.
|
|
81
|
+
*/
|
|
82
|
+
declare function buildKnowledgeRequirements(specs: KnowledgeRequirementSpec[], signals?: Record<string, KnowledgeSignal>): KnowledgeRequirement[];
|
|
83
|
+
/**
|
|
84
|
+
* Score every spec from workspace state. `derive` (code) wins; otherwise the
|
|
85
|
+
* declarative `satisfiedBy` rule is evaluated through the accessor; a spec with
|
|
86
|
+
* neither scores 0 (an acquisition gate, e.g. `search_web`).
|
|
87
|
+
*/
|
|
88
|
+
declare function deriveSignals(specs: KnowledgeRequirementSpec[], ctx: KnowledgeStateAccessor): Promise<Record<string, KnowledgeSignal>>;
|
|
89
|
+
|
|
90
|
+
export { type KnowledgeRequirementSpec, type KnowledgeSignal, type KnowledgeStateAccessor, type SatisfiedByRule, buildKnowledgeRequirements, deriveSignals };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"packageManager": "pnpm@10.33.4",
|
|
5
|
-
"description": "
|
|
5
|
+
"description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"tangle",
|
|
8
|
+
"ai-agent",
|
|
9
|
+
"agent-framework",
|
|
10
|
+
"llm",
|
|
11
|
+
"tool-calling",
|
|
12
|
+
"mcp",
|
|
13
|
+
"openai",
|
|
14
|
+
"approval-workflow",
|
|
15
|
+
"cloudflare-workers",
|
|
16
|
+
"eval"
|
|
17
|
+
],
|
|
6
18
|
"homepage": "https://github.com/tangle-network/agent-app#readme",
|
|
7
19
|
"repository": {
|
|
8
20
|
"type": "git",
|
|
@@ -49,6 +61,11 @@
|
|
|
49
61
|
"import": "./dist/eval/index.js",
|
|
50
62
|
"default": "./dist/eval/index.js"
|
|
51
63
|
},
|
|
64
|
+
"./knowledge": {
|
|
65
|
+
"types": "./dist/knowledge/index.d.ts",
|
|
66
|
+
"import": "./dist/knowledge/index.js",
|
|
67
|
+
"default": "./dist/knowledge/index.js"
|
|
68
|
+
},
|
|
52
69
|
"./billing": {
|
|
53
70
|
"types": "./dist/billing/index.d.ts",
|
|
54
71
|
"import": "./dist/billing/index.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/billing/index.ts"],"sourcesContent":["/**\n * Per-workspace budget-capped model keys — app-owned billing, metered on Tangle.\n *\n * Each workspace (the paying entity) runs the agent on its OWN child API key\n * minted from the platform parent key. The child carries a hard USD budget the\n * Tangle Router enforces AT THE KEY — model spend can't exceed the allowance,\n * zero app-side accounting. The app charges its own subscription (e.g. 5× the\n * allowance) and re-provisions each period. Child budgets are IMMUTABLE on the\n * platform, so a new budget = a fresh key + revoke the prior (rotate).\n *\n * The mint / rotate / rollover / usage LOGIC is generic and lives here.\n * Persistence (which D1 table), secret encryption, and key provisioning are\n * SEAMS each product supplies — so this module imports no DB and no key-mgmt\n * SDK (structural contracts only, like `../tangle`). The `@tangle-network/tcloud`\n * SDK is the provisioner a product passes in; it is not a dependency here.\n */\n\n/** The key-provisioning operations this needs — the `@tangle-network/tcloud`\n * SDK's `TCloudClient` satisfies it structurally; pass it in. */\nexport interface KeyProvisioner {\n createKey(input: { name: string; product: string; budgetUsd: number; expiresAt: string }): Promise<{ id?: string; key?: string }>\n revokeKey(keyId: string): Promise<unknown>\n getKey(keyId: string): Promise<{ budgetUsd?: number; budgetSpent?: number; expiresAt?: string | null }>\n}\n\n/** A stored child-key record (the app's row, shape-normalized). */\nexport interface WorkspaceKeyRecord {\n /** App row id (opaque). */\n id: string\n keyId: string\n /** The encrypted secret — decrypted via {@link KeyCrypto.decrypt}. */\n keyEncrypted: string\n budgetUsd: number\n expiresAt: Date | null\n}\n\n/** Persistence seam — the product implements this against its own D1 table. */\nexport interface WorkspaceKeyStore {\n /** Most-recent active key for the workspace, or null. */\n getActive(workspaceId: string): Promise<WorkspaceKeyRecord | null>\n /** All active keys (to revoke priors on rotate). */\n listActive(workspaceId: string): Promise<Array<{ id: string; keyId: string }>>\n /** Persist a freshly minted active key. */\n insert(record: { workspaceId: string; keyId: string; keyEncrypted: string; budgetUsd: number; expiresAt: Date }): Promise<void>\n /** Mark a prior row revoked. */\n markRevoked(id: string, now: Date): Promise<void>\n}\n\n/** Secret encryption seam (the app's at-rest crypto). */\nexport interface KeyCrypto {\n encrypt(secret: string): Promise<string>\n decrypt(encrypted: string): Promise<string>\n}\n\nexport interface WorkspaceKeyManagerOptions {\n provisioner: KeyProvisioner\n store: WorkspaceKeyStore\n crypto: KeyCrypto\n /** Default monthly allowance (USD) when a call doesn't specify one. */\n defaultBudgetUsd: number\n /** Injectable clock. Default `() => new Date()`. */\n now?: () => Date\n /** tcloud product the key is scoped to. Default `'router'`. */\n product?: string\n}\n\nexport interface WorkspaceModelKeyUsage {\n keyId: string\n budgetUsd: number\n budgetSpent: number\n budgetRemaining: number\n expiresAt: string | null\n exhausted: boolean\n}\n\nexport interface WorkspaceKeyManager {\n /** The workspace's active child-key secret, provisioning one if absent/expired. */\n ensureKey(workspaceId: string, opts?: { budgetUsd?: number }): Promise<string>\n /** Mint a fresh key + revoke priors (period renewal / top-up). `rollover`\n * carries the prior key's unused budget into the new one, bounded by\n * `rolloverCapUsd`. Returns the new secret. */\n rotateKey(workspaceId: string, opts?: { budgetUsd?: number; rollover?: boolean; rolloverCapUsd?: number }): Promise<string>\n /** Live budget usage for the active key (drives the \"$X of $Y used\" panel). */\n getUsage(workspaceId: string): Promise<WorkspaceModelKeyUsage | null>\n}\n\n/** Period end = first day of next month, midnight UTC. Keys expire at the period\n * boundary so a forgotten rotation fails closed rather than running free. */\nfunction nextPeriodEnd(now: Date): Date {\n return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1, 0, 0, 0, 0))\n}\n\nexport function createWorkspaceKeyManager(opts: WorkspaceKeyManagerOptions): WorkspaceKeyManager {\n const clock = opts.now ?? (() => new Date())\n const product = opts.product ?? 'router'\n\n const getUsage: WorkspaceKeyManager['getUsage'] = async (workspaceId) => {\n const active = await opts.store.getActive(workspaceId)\n if (!active) return null\n const info = await opts.provisioner.getKey(active.keyId)\n const budgetUsd = info.budgetUsd ?? active.budgetUsd\n const budgetSpent = info.budgetSpent ?? 0\n const budgetRemaining = Math.max(0, budgetUsd - budgetSpent)\n return {\n keyId: active.keyId,\n budgetUsd,\n budgetSpent,\n budgetRemaining,\n expiresAt: info.expiresAt ?? (active.expiresAt ? active.expiresAt.toISOString() : null),\n exhausted: budgetRemaining <= 0,\n }\n }\n\n const rotateKey: WorkspaceKeyManager['rotateKey'] = async (workspaceId, ropts) => {\n const now = clock()\n const allowance = ropts?.budgetUsd ?? opts.defaultBudgetUsd\n\n let budgetUsd = allowance\n if (ropts?.rollover) {\n const prior = await getUsage(workspaceId).catch(() => null)\n budgetUsd = allowance + (prior?.budgetRemaining ?? 0)\n if (ropts.rolloverCapUsd != null) budgetUsd = Math.min(budgetUsd, ropts.rolloverCapUsd)\n }\n\n const expiresAt = nextPeriodEnd(now)\n const created = await opts.provisioner.createKey({ name: `ws:${workspaceId}`, product, budgetUsd, expiresAt: expiresAt.toISOString() })\n if (!created.key || !created.id) throw new Error('tcloud createKey returned no key')\n const keyEncrypted = await opts.crypto.encrypt(created.key)\n\n const priors = await opts.store.listActive(workspaceId)\n await opts.store.insert({ workspaceId, keyId: created.id, keyEncrypted, budgetUsd, expiresAt })\n for (const p of priors) {\n await opts.store.markRevoked(p.id, now)\n // Best-effort upstream revoke — the row is already revoked and an expired\n // key fails closed regardless, so a transient error is non-fatal.\n try {\n await opts.provisioner.revokeKey(p.keyId)\n } catch {\n /* non-fatal */\n }\n }\n return created.key\n }\n\n const ensureKey: WorkspaceKeyManager['ensureKey'] = async (workspaceId, eopts) => {\n const now = clock()\n const active = await opts.store.getActive(workspaceId)\n if (active && (!active.expiresAt || active.expiresAt.getTime() > now.getTime())) {\n return opts.crypto.decrypt(active.keyEncrypted)\n }\n return rotateKey(workspaceId, { budgetUsd: eopts?.budgetUsd })\n }\n\n return { ensureKey, rotateKey, getUsage }\n}\n"],"mappings":";AAwFA,SAAS,cAAc,KAAiB;AACtC,SAAO,IAAI,KAAK,KAAK,IAAI,IAAI,eAAe,GAAG,IAAI,YAAY,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC;AACtF;AAEO,SAAS,0BAA0B,MAAuD;AAC/F,QAAM,QAAQ,KAAK,QAAQ,MAAM,oBAAI,KAAK;AAC1C,QAAM,UAAU,KAAK,WAAW;AAEhC,QAAM,WAA4C,OAAO,gBAAgB;AACvE,UAAM,SAAS,MAAM,KAAK,MAAM,UAAU,WAAW;AACrD,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,OAAO,MAAM,KAAK,YAAY,OAAO,OAAO,KAAK;AACvD,UAAM,YAAY,KAAK,aAAa,OAAO;AAC3C,UAAM,cAAc,KAAK,eAAe;AACxC,UAAM,kBAAkB,KAAK,IAAI,GAAG,YAAY,WAAW;AAC3D,WAAO;AAAA,MACL,OAAO,OAAO;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,WAAW,KAAK,cAAc,OAAO,YAAY,OAAO,UAAU,YAAY,IAAI;AAAA,MAClF,WAAW,mBAAmB;AAAA,IAChC;AAAA,EACF;AAEA,QAAM,YAA8C,OAAO,aAAa,UAAU;AAChF,UAAM,MAAM,MAAM;AAClB,UAAM,YAAY,OAAO,aAAa,KAAK;AAE3C,QAAI,YAAY;AAChB,QAAI,OAAO,UAAU;AACnB,YAAM,QAAQ,MAAM,SAAS,WAAW,EAAE,MAAM,MAAM,IAAI;AAC1D,kBAAY,aAAa,OAAO,mBAAmB;AACnD,UAAI,MAAM,kBAAkB,KAAM,aAAY,KAAK,IAAI,WAAW,MAAM,cAAc;AAAA,IACxF;AAEA,UAAM,YAAY,cAAc,GAAG;AACnC,UAAM,UAAU,MAAM,KAAK,YAAY,UAAU,EAAE,MAAM,MAAM,WAAW,IAAI,SAAS,WAAW,WAAW,UAAU,YAAY,EAAE,CAAC;AACtI,QAAI,CAAC,QAAQ,OAAO,CAAC,QAAQ,GAAI,OAAM,IAAI,MAAM,kCAAkC;AACnF,UAAM,eAAe,MAAM,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAE1D,UAAM,SAAS,MAAM,KAAK,MAAM,WAAW,WAAW;AACtD,UAAM,KAAK,MAAM,OAAO,EAAE,aAAa,OAAO,QAAQ,IAAI,cAAc,WAAW,UAAU,CAAC;AAC9F,eAAW,KAAK,QAAQ;AACtB,YAAM,KAAK,MAAM,YAAY,EAAE,IAAI,GAAG;AAGtC,UAAI;AACF,cAAM,KAAK,YAAY,UAAU,EAAE,KAAK;AAAA,MAC1C,QAAQ;AAAA,MAER;AAAA,IACF;AACA,WAAO,QAAQ;AAAA,EACjB;AAEA,QAAM,YAA8C,OAAO,aAAa,UAAU;AAChF,UAAM,MAAM,MAAM;AAClB,UAAM,SAAS,MAAM,KAAK,MAAM,UAAU,WAAW;AACrD,QAAI,WAAW,CAAC,OAAO,aAAa,OAAO,UAAU,QAAQ,IAAI,IAAI,QAAQ,IAAI;AAC/E,aAAO,KAAK,OAAO,QAAQ,OAAO,YAAY;AAAA,IAChD;AACA,WAAO,UAAU,aAAa,EAAE,WAAW,OAAO,UAAU,CAAC;AAAA,EAC/D;AAEA,SAAO,EAAE,WAAW,WAAW,SAAS;AAC1C;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/crypto/index.ts"],"sourcesContent":["/**\n * AES-256-GCM field encryption (for PII at rest — SSN/EIN/ID numbers, secrets).\n * WebCrypto only — runs on Cloudflare Workers, Node, and the browser with no\n * Node `crypto` dependency. The 32-byte key is a PARAMETER (64-char hex); the\n * framework never reads env — the product binds its own `ENCRYPTION_KEY` (this\n * is the concrete impl behind the `KeyCrypto` seam in `../billing`).\n *\n * Wire format: base64(iv ‖ ciphertext ‖ tag) — the 12-byte IV is prepended; the\n * GCM auth tag is appended by WebCrypto inside the ciphertext.\n */\n\nconst IV_LENGTH = 12\nconst TAG_LENGTH = 16\nconst ALGORITHM = 'AES-GCM'\n\n/** Validate + decode a 64-char hex key to 32 bytes. Throws on the wrong shape so\n * a misconfigured key fails loud, never silently weakens encryption. */\nexport function decodeHexKey(keyHex: string): Uint8Array {\n if (keyHex.length !== 64) throw new Error('encryption key must be a 64-char hex string (32 bytes)')\n const bytes = new Uint8Array(32)\n for (let i = 0; i < 64; i += 2) bytes[i / 2] = parseInt(keyHex.substring(i, i + 2), 16)\n return bytes\n}\n\nasync function importKey(keyHex: string): Promise<CryptoKey> {\n const raw = decodeHexKey(keyHex)\n return crypto.subtle.importKey('raw', raw.buffer as ArrayBuffer, { name: ALGORITHM } as Algorithm, false, ['encrypt', 'decrypt'])\n}\n\nfunction toBase64(data: Uint8Array): string {\n let binary = ''\n for (let i = 0; i < data.length; i++) binary += String.fromCharCode(data[i]!)\n return btoa(binary)\n}\n\nfunction fromBase64(b64: string): Uint8Array {\n const binary = atob(b64)\n const bytes = new Uint8Array(binary.length)\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)\n return bytes\n}\n\n/** Encrypt `plaintext` with AES-256-GCM under `keyHex`. Returns\n * base64(iv ‖ ciphertext ‖ tag). A fresh random IV per call. */\nexport async function encryptAesGcm(plaintext: string, keyHex: string): Promise<string> {\n const key = await importKey(keyHex)\n const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH))\n const ciphertext = await crypto.subtle.encrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH * 8 }, key, new TextEncoder().encode(plaintext))\n const result = new Uint8Array(IV_LENGTH + ciphertext.byteLength)\n result.set(iv, 0)\n result.set(new Uint8Array(ciphertext), IV_LENGTH)\n return toBase64(result)\n}\n\n/** Decrypt a base64(iv ‖ ciphertext ‖ tag) string under `keyHex`. Throws if the\n * tag fails (tamper/wrong key). */\nexport async function decryptAesGcm(encrypted: string, keyHex: string): Promise<string> {\n const key = await importKey(keyHex)\n const data = fromBase64(encrypted)\n const iv = data.slice(0, IV_LENGTH)\n const ciphertext = data.slice(IV_LENGTH)\n const plain = await crypto.subtle.decrypt({ name: ALGORITHM, iv, tagLength: TAG_LENGTH * 8 }, key, ciphertext)\n return new TextDecoder().decode(plain)\n}\n\n/** Build a {@link import('../billing').KeyCrypto}-compatible pair bound to a key\n * (or a key-resolver, for env-backed keys resolved per call). */\nexport function createFieldCrypto(key: string | (() => string)): { encrypt(s: string): Promise<string>; decrypt(s: string): Promise<string> } {\n const resolve = typeof key === 'function' ? key : () => key\n return {\n encrypt: (s) => encryptAesGcm(s, resolve()),\n decrypt: (s) => decryptAesGcm(s, resolve()),\n }\n}\n"],"mappings":";AAWA,IAAM,YAAY;AAClB,IAAM,aAAa;AACnB,IAAM,YAAY;AAIX,SAAS,aAAa,QAA4B;AACvD,MAAI,OAAO,WAAW,GAAI,OAAM,IAAI,MAAM,wDAAwD;AAClG,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,WAAS,IAAI,GAAG,IAAI,IAAI,KAAK,EAAG,OAAM,IAAI,CAAC,IAAI,SAAS,OAAO,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE;AACtF,SAAO;AACT;AAEA,eAAe,UAAU,QAAoC;AAC3D,QAAM,MAAM,aAAa,MAAM;AAC/B,SAAO,OAAO,OAAO,UAAU,OAAO,IAAI,QAAuB,EAAE,MAAM,UAAU,GAAgB,OAAO,CAAC,WAAW,SAAS,CAAC;AAClI;AAEA,SAAS,SAAS,MAA0B;AAC1C,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,KAAK,QAAQ,IAAK,WAAU,OAAO,aAAa,KAAK,CAAC,CAAE;AAC5E,SAAO,KAAK,MAAM;AACpB;AAEA,SAAS,WAAW,KAAyB;AAC3C,QAAM,SAAS,KAAK,GAAG;AACvB,QAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;AAC1C,WAAS,IAAI,GAAG,IAAI,OAAO,QAAQ,IAAK,OAAM,CAAC,IAAI,OAAO,WAAW,CAAC;AACtE,SAAO;AACT;AAIA,eAAsB,cAAc,WAAmB,QAAiC;AACtF,QAAM,MAAM,MAAM,UAAU,MAAM;AAClC,QAAM,KAAK,OAAO,gBAAgB,IAAI,WAAW,SAAS,CAAC;AAC3D,QAAM,aAAa,MAAM,OAAO,OAAO,QAAQ,EAAE,MAAM,WAAW,IAAI,WAAW,aAAa,EAAE,GAAG,KAAK,IAAI,YAAY,EAAE,OAAO,SAAS,CAAC;AAC3I,QAAM,SAAS,IAAI,WAAW,YAAY,WAAW,UAAU;AAC/D,SAAO,IAAI,IAAI,CAAC;AAChB,SAAO,IAAI,IAAI,WAAW,UAAU,GAAG,SAAS;AAChD,SAAO,SAAS,MAAM;AACxB;AAIA,eAAsB,cAAc,WAAmB,QAAiC;AACtF,QAAM,MAAM,MAAM,UAAU,MAAM;AAClC,QAAM,OAAO,WAAW,SAAS;AACjC,QAAM,KAAK,KAAK,MAAM,GAAG,SAAS;AAClC,QAAM,aAAa,KAAK,MAAM,SAAS;AACvC,QAAM,QAAQ,MAAM,OAAO,OAAO,QAAQ,EAAE,MAAM,WAAW,IAAI,WAAW,aAAa,EAAE,GAAG,KAAK,UAAU;AAC7G,SAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AACvC;AAIO,SAAS,kBAAkB,KAA4G;AAC5I,QAAM,UAAU,OAAO,QAAQ,aAAa,MAAM,MAAM;AACxD,SAAO;AAAA,IACL,SAAS,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAAA,IAC1C,SAAS,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAC;AAAA,EAC5C;AACF;","names":[]}
|