@intx/db 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/approval-store.d.ts +7 -0
- package/dist/approval-store.js +13 -0
- package/dist/backfill-principal-keys.d.ts +9 -0
- package/dist/backfill-principal-keys.js +51 -0
- package/dist/client.d.ts +27 -2
- package/dist/client.js +6 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -0
- package/dist/connection.js +1 -0
- package/dist/credential-resolution.d.ts +54 -5
- package/dist/credential-resolution.js +109 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +10 -4
- package/dist/model-source-resolution.d.ts +6 -5
- package/dist/model-source-resolution.js +42 -13
- package/dist/parse-row.d.ts +39 -8
- package/dist/parse-row.js +8 -0
- package/dist/principal-key-store.d.ts +51 -0
- package/dist/principal-key-store.js +82 -0
- package/dist/principal-store.d.ts +44 -0
- package/dist/principal-store.js +67 -0
- package/dist/schema/approvals.js +5 -0
- package/dist/schema/git-tokens.js +1 -1
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/principal-keys.d.ts +126 -0
- package/dist/schema/principal-keys.js +28 -0
- package/dist/schema/sidecar-allocation.d.ts +19 -40
- package/dist/schema/sidecar-allocation.js +4 -8
- package/dist/schema/sidecar.d.ts +0 -17
- package/dist/schema/sidecar.js +4 -12
- package/dist/schema/tenants.js +13 -3
- package/dist/schema/workflow-probe.d.ts +358 -0
- package/dist/schema/workflow-probe.js +51 -0
- package/dist/schema/workflow-run-dispatch.d.ts +17 -0
- package/dist/schema/workflow-run-dispatch.js +7 -0
- package/dist/schema/workflow-run.d.ts +27 -0
- package/dist/schema/workflow-run.js +8 -0
- package/dist/sender-key-resolver.d.ts +91 -0
- package/dist/sender-key-resolver.js +184 -0
- package/dist/sidecar-allocation-store.d.ts +38 -3
- package/dist/sidecar-allocation-store.js +224 -22
- package/dist/signer-identity.d.ts +12 -0
- package/dist/signer-identity.js +15 -0
- package/dist/tenant-hierarchy.d.ts +2 -0
- package/dist/tenant-hierarchy.js +27 -0
- package/dist/workflow-probe-store.d.ts +47 -0
- package/dist/workflow-probe-store.js +124 -0
- package/dist/workflow-run-dispatch-store.d.ts +3 -0
- package/dist/workflow-run-dispatch-store.js +12 -2
- package/migrations/0085_add_approval_run_idx.sql +1 -0
- package/migrations/0086_cool_human_cannonball.sql +1 -0
- package/migrations/0087_drop_sidecar_placement.sql +3 -0
- package/migrations/0088_thick_sprite.sql +32 -0
- package/migrations/0089_tense_selene.sql +13 -0
- package/migrations/0090_tenant_domain_lower_unique.sql +2 -0
- package/migrations/0091_workflow_run_dispatch_sender_address.sql +21 -0
- package/migrations/0092_sidecar_destroy_failed.sql +4 -0
- package/migrations/0093_sidecar_initialization.sql +2 -0
- package/migrations/meta/0085_snapshot.json +4111 -0
- package/migrations/meta/0086_snapshot.json +4117 -0
- package/migrations/meta/0087_snapshot.json +4100 -0
- package/migrations/meta/0088_snapshot.json +4286 -0
- package/migrations/meta/0089_snapshot.json +4376 -0
- package/migrations/meta/0090_snapshot.json +4387 -0
- package/migrations/meta/0091_snapshot.json +4397 -0
- package/migrations/meta/0092_snapshot.json +4397 -0
- package/migrations/meta/0093_snapshot.json +4407 -0
- package/migrations/meta/_journal.json +63 -0
- package/package.json +6 -5
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { and, eq } from "drizzle-orm";
|
|
2
|
-
import { createNoopCredentialCipher } from "@intx/crypto";
|
|
3
2
|
import { InvokerModelPreferences, ModelRequirements, credentialAad, } from "@intx/types";
|
|
4
3
|
import { listVisibleOfferings, } from "./catalog-resolution.js";
|
|
5
4
|
import { resolveCredentialById } from "./credential-resolution.js";
|
|
@@ -93,6 +92,13 @@ async function buildSource(db, tenantId, resolved, credentialCipher) {
|
|
|
93
92
|
// filter in resolveModelSources still reads the raw row's `capabilities`
|
|
94
93
|
// for its `.includes` check; that read-only comparison cannot be corrupted
|
|
95
94
|
// into a wrong routing decision, so it is left as-is.
|
|
95
|
+
// Decrypt the stored secret at the one point it is used. Strict: a
|
|
96
|
+
// non-ciphertext value (a row not yet re-keyed, or a write that failed to
|
|
97
|
+
// encrypt) throws rather than delivering a bad key -- fail closed. The secret
|
|
98
|
+
// rides the credential-material cell (keyed by `credentialId`), NOT the source
|
|
99
|
+
// config -- the source only references the credential by id, so no secret is
|
|
100
|
+
// inline in the pinned/persisted source.
|
|
101
|
+
const secret = await credentialCipher.decrypt(credential.secret, credentialAad(credential.id, "secret"));
|
|
96
102
|
const parsed = parseModelOfferingRow(offering);
|
|
97
103
|
return {
|
|
98
104
|
ok: true,
|
|
@@ -100,14 +106,17 @@ async function buildSource(db, tenantId, resolved, credentialCipher) {
|
|
|
100
106
|
id: offering.id,
|
|
101
107
|
provider: provider.plugin,
|
|
102
108
|
baseURL: provider.baseURL,
|
|
103
|
-
|
|
104
|
-
// non-ciphertext value (a row not yet re-keyed, or a write that failed to
|
|
105
|
-
// encrypt) throws rather than delivering a bad key -- fail closed.
|
|
106
|
-
apiKey: await credentialCipher.decrypt(credential.secret, credentialAad(credential.id, "secret")),
|
|
109
|
+
credentialId: provider.credentialId,
|
|
107
110
|
model: model.canonicalName,
|
|
108
111
|
capabilities: parsed.capabilities,
|
|
109
112
|
...(parsed.quirks !== null ? { quirks: parsed.quirks } : {}),
|
|
110
113
|
},
|
|
114
|
+
material: {
|
|
115
|
+
credentialId: provider.credentialId,
|
|
116
|
+
providerKey: provider.plugin,
|
|
117
|
+
origin: provider.baseURL,
|
|
118
|
+
secret,
|
|
119
|
+
},
|
|
111
120
|
};
|
|
112
121
|
}
|
|
113
122
|
/**
|
|
@@ -116,7 +125,7 @@ async function buildSource(db, tenantId, resolved, credentialCipher) {
|
|
|
116
125
|
* secrets out of persisted launch specs and rechecks tenant visibility and
|
|
117
126
|
* credential ownership on every launch.
|
|
118
127
|
*/
|
|
119
|
-
export async function resolveSourcesByOfferingIds(db, tenantId, offeringIds, credentialCipher
|
|
128
|
+
export async function resolveSourcesByOfferingIds(db, tenantId, offeringIds, credentialCipher) {
|
|
120
129
|
const visible = await listVisibleOfferings(db, tenantId);
|
|
121
130
|
const byId = new Map(visible.map((entry) => [entry.offering.id, entry]));
|
|
122
131
|
const sources = [];
|
|
@@ -155,13 +164,21 @@ export async function resolveSourcesByOfferingIds(db, tenantId, offeringIds, cre
|
|
|
155
164
|
* otherwise the offering is skipped (`credential_unauthorized`) and its secret
|
|
156
165
|
* is withheld.
|
|
157
166
|
*/
|
|
158
|
-
export async function resolveModelSources(db, tenantId, requirements,
|
|
167
|
+
export async function resolveModelSources(db, tenantId, requirements,
|
|
168
|
+
// Decrypts each resolved credential secret at its point of use in
|
|
169
|
+
// `buildSource`. Required: the edge (the launch route / rotation push /
|
|
170
|
+
// allocation service) owns the cipher and always supplies a real one; the
|
|
171
|
+
// noop fallback for a keyless composition is resolved once at that edge
|
|
172
|
+
// (`resolveCredentialCipher` in the hub app), never defaulted here.
|
|
173
|
+
credentialCipher, opts) {
|
|
159
174
|
if (requirements.length === 0) {
|
|
160
175
|
return { ok: false, reason: "no_requirements" };
|
|
161
176
|
}
|
|
162
|
-
const credentialCipher = opts?.credentialCipher ?? createNoopCredentialCipher();
|
|
163
177
|
const visible = await listVisibleOfferings(db, tenantId);
|
|
164
178
|
const sources = [];
|
|
179
|
+
// Dedupe material by credentialId across every requirement's chain: one
|
|
180
|
+
// credential backing several offerings is delivered once.
|
|
181
|
+
const materials = new Map();
|
|
165
182
|
for (const requirement of requirements) {
|
|
166
183
|
let candidates = visible
|
|
167
184
|
.filter((o) => o.model.canonicalName === requirement.model)
|
|
@@ -174,10 +191,12 @@ export async function resolveModelSources(db, tenantId, requirements, opts) {
|
|
|
174
191
|
candidates = applyPreference(candidates, opts?.invokerPreferences?.[requirement.model]);
|
|
175
192
|
const skips = [];
|
|
176
193
|
const modelSources = [];
|
|
194
|
+
const modelMaterials = [];
|
|
177
195
|
for (const candidate of candidates) {
|
|
178
196
|
const built = await buildSource(db, tenantId, candidate, credentialCipher);
|
|
179
197
|
if (built.ok) {
|
|
180
198
|
modelSources.push(built.source);
|
|
199
|
+
modelMaterials.push(built.material);
|
|
181
200
|
}
|
|
182
201
|
else {
|
|
183
202
|
skips.push(built.skip);
|
|
@@ -192,8 +211,13 @@ export async function resolveModelSources(db, tenantId, requirements, opts) {
|
|
|
192
211
|
};
|
|
193
212
|
}
|
|
194
213
|
sources.push(...modelSources);
|
|
214
|
+
for (const material of modelMaterials) {
|
|
215
|
+
if (!materials.has(material.credentialId)) {
|
|
216
|
+
materials.set(material.credentialId, material);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
195
219
|
}
|
|
196
|
-
return { ok: true, sources };
|
|
220
|
+
return { ok: true, sources, materials: [...materials.values()] };
|
|
197
221
|
}
|
|
198
222
|
/**
|
|
199
223
|
* Resolve an agent's model requirements to the credential-free
|
|
@@ -212,8 +236,14 @@ export async function resolveModelSources(db, tenantId, requirements, opts) {
|
|
|
212
236
|
* an ambiguous preference list whose lost `offering.id` distinctions cannot be
|
|
213
237
|
* recovered after the agent's columns are dropped.
|
|
214
238
|
*/
|
|
215
|
-
export async function resolveInferencePreferences(db, tenantId, requirements
|
|
216
|
-
|
|
239
|
+
export async function resolveInferencePreferences(db, tenantId, requirements,
|
|
240
|
+
// Required even though the result discards the credential: this reuses the
|
|
241
|
+
// full `resolveModelSources` path, which decrypts each secret at its point of
|
|
242
|
+
// use in `buildSource` before the `{provider, model}` projection drops it. So
|
|
243
|
+
// the caller must still supply the edge's real cipher; a noop here would throw
|
|
244
|
+
// the moment a tenant's secret is stored encrypted.
|
|
245
|
+
credentialCipher) {
|
|
246
|
+
const resolution = await resolveModelSources(db, tenantId, requirements, credentialCipher);
|
|
217
247
|
if (!resolution.ok) {
|
|
218
248
|
throw new Error(`cannot resolve inference preferences for the folded definition: ${resolution.reason}`);
|
|
219
249
|
}
|
|
@@ -284,8 +314,7 @@ export async function resolveInstanceModelSources(db, tenantId, instance, creden
|
|
|
284
314
|
for (const preference of preferences) {
|
|
285
315
|
invokerPreferences[preference.model] = preference.providers;
|
|
286
316
|
}
|
|
287
|
-
return resolveModelSources(db, tenantId, requirements, {
|
|
317
|
+
return resolveModelSources(db, tenantId, requirements, credentialCipher, {
|
|
288
318
|
invokerPreferences,
|
|
289
|
-
...(credentialCipher !== undefined ? { credentialCipher } : {}),
|
|
290
319
|
});
|
|
291
320
|
}
|
package/dist/parse-row.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { approval, credential, gitToken, grant, modelOffering, modelProvider, oauthClient, offering, principal, provider, signalCorrelation, tenant, transaction, turnPart, wallet, workflowDefinition, workflowDefinitionVersion, workflowRun, workflowRunDispatch, workflowRunLaunchSpec } from "./schema/index.js";
|
|
1
|
+
import type { approval, credential, gitToken, grant, modelOffering, modelProvider, oauthClient, offering, principal, principalKey, provider, signalCorrelation, tenant, transaction, turnPart, wallet, workflowDefinition, workflowDefinitionVersion, workflowRun, workflowRunDispatch, workflowRunLaunchSpec } from "./schema/index.js";
|
|
2
2
|
export declare const GitTokenKindValidator: import("arktype/internal/variants/string.ts").StringType<"pat" | "svc", {}>;
|
|
3
3
|
export declare function parseWorkflowDefinitionRow(row: typeof workflowDefinition.$inferSelect): {
|
|
4
4
|
status: "deployed" | "stopped";
|
|
@@ -53,8 +53,8 @@ export declare function parseGrantRow(row: typeof grant.$inferSelect): {
|
|
|
53
53
|
updatedAt: Date;
|
|
54
54
|
expiresAt: Date | null;
|
|
55
55
|
tenantId: string;
|
|
56
|
-
roleId: string | null;
|
|
57
56
|
principalId: string | null;
|
|
57
|
+
roleId: string | null;
|
|
58
58
|
resource: string;
|
|
59
59
|
action: string;
|
|
60
60
|
};
|
|
@@ -83,6 +83,15 @@ export declare function parsePrincipalRow(row: typeof principal.$inferSelect): {
|
|
|
83
83
|
tenantId: string;
|
|
84
84
|
refId: string;
|
|
85
85
|
};
|
|
86
|
+
export declare function parsePrincipalKeyRow(row: typeof principalKey.$inferSelect): {
|
|
87
|
+
status: "active" | "retired";
|
|
88
|
+
id: string;
|
|
89
|
+
createdAt: Date;
|
|
90
|
+
updatedAt: Date;
|
|
91
|
+
principalId: string;
|
|
92
|
+
publicKey: string;
|
|
93
|
+
privateKey: string;
|
|
94
|
+
};
|
|
86
95
|
export declare function parseSignalCorrelationRow(row: typeof signalCorrelation.$inferSelect): {
|
|
87
96
|
kind: "approval";
|
|
88
97
|
createdAt: Date;
|
|
@@ -107,13 +116,14 @@ export declare function parseWorkflowRunRow(row: typeof workflowRun.$inferSelect
|
|
|
107
116
|
id: string;
|
|
108
117
|
createdAt: Date;
|
|
109
118
|
tenantId: string;
|
|
110
|
-
definitionId: string;
|
|
111
119
|
principalId: string | null;
|
|
120
|
+
publicKey: string | null;
|
|
121
|
+
definitionId: string;
|
|
112
122
|
anchorRunId: string | null;
|
|
113
123
|
address: string | null;
|
|
114
|
-
publicKey: string | null;
|
|
115
124
|
sidecarId: string | null;
|
|
116
125
|
kernelId: string | null;
|
|
126
|
+
credentialRefs: import("./schema/index.js").WorkflowRunCredentialRefs | null;
|
|
117
127
|
endedAt: Date | null;
|
|
118
128
|
};
|
|
119
129
|
export declare function parseWorkflowRunLaunchSpecRow(row: typeof workflowRunLaunchSpec.$inferSelect): {
|
|
@@ -148,6 +158,18 @@ export declare function parseWorkflowRunLaunchSpecRow(row: typeof workflowRunLau
|
|
|
148
158
|
locator: "tenant";
|
|
149
159
|
name?: string;
|
|
150
160
|
}[];
|
|
161
|
+
sidecarPlacement?: {
|
|
162
|
+
capabilities?: {
|
|
163
|
+
capability: string;
|
|
164
|
+
effect: "require" | "block";
|
|
165
|
+
}[];
|
|
166
|
+
};
|
|
167
|
+
inboundMailPolicy?: {
|
|
168
|
+
untrustedFrom?: "reject" | "admit";
|
|
169
|
+
invalid?: "reject" | "admit";
|
|
170
|
+
missing?: "reject" | "admit";
|
|
171
|
+
unknown?: "reject" | "admit";
|
|
172
|
+
};
|
|
151
173
|
};
|
|
152
174
|
closure: {
|
|
153
175
|
schemaVersion: "1";
|
|
@@ -186,7 +208,13 @@ export declare function parseWorkflowRunLaunchSpecRow(row: typeof workflowRunLau
|
|
|
186
208
|
}[];
|
|
187
209
|
};
|
|
188
210
|
approvedWireHash: string;
|
|
189
|
-
approvedGrants: string
|
|
211
|
+
approvedGrants: (string | {
|
|
212
|
+
resource: string;
|
|
213
|
+
action: string;
|
|
214
|
+
source: "creator" | "invoker";
|
|
215
|
+
effect?: "allow" | "deny" | "ask";
|
|
216
|
+
conditions?: Record<string, unknown> | null;
|
|
217
|
+
})[];
|
|
190
218
|
};
|
|
191
219
|
sourceOfferingIds: string[];
|
|
192
220
|
deployContent: Record<string, unknown>;
|
|
@@ -226,6 +254,7 @@ export declare function parseWorkflowRunDispatchRow(row: typeof workflowRunDispa
|
|
|
226
254
|
nextAttemptAt: Date | null;
|
|
227
255
|
failureCode: string | null;
|
|
228
256
|
failureMessage: string | null;
|
|
257
|
+
senderAddress: string | null;
|
|
229
258
|
rawMessage: Uint8Array<ArrayBufferLike>;
|
|
230
259
|
acknowledgedGeneration: number | null;
|
|
231
260
|
attemptCount: number;
|
|
@@ -256,8 +285,8 @@ export declare function parseCredentialRow(row: typeof credential.$inferSelect):
|
|
|
256
285
|
expiresAt: Date | null;
|
|
257
286
|
providerId: string;
|
|
258
287
|
tenantId: string;
|
|
259
|
-
description: string | null;
|
|
260
288
|
principalId: string | null;
|
|
289
|
+
description: string | null;
|
|
261
290
|
scopes: string[] | null;
|
|
262
291
|
oauthClientId: string | null;
|
|
263
292
|
secret: string;
|
|
@@ -306,8 +335,10 @@ export declare function parseTenantRow(row: typeof tenant.$inferSelect): {
|
|
|
306
335
|
config: {
|
|
307
336
|
[x: string]: unknown;
|
|
308
337
|
sidecarPlacement?: {
|
|
309
|
-
|
|
310
|
-
|
|
338
|
+
capabilities?: {
|
|
339
|
+
capability: string;
|
|
340
|
+
effect: "require" | "block";
|
|
341
|
+
}[];
|
|
311
342
|
};
|
|
312
343
|
} | null;
|
|
313
344
|
domain: string;
|
package/dist/parse-row.js
CHANGED
|
@@ -33,6 +33,8 @@ const WorkflowDefinitionStatusValidator = type.enumerated(...workflowDefinitionS
|
|
|
33
33
|
const WorkflowDefinitionVersionStatusValidator = type.enumerated(...workflowDefinitionVersionStatuses);
|
|
34
34
|
const PrincipalKindValidator = type.enumerated(...principalKinds);
|
|
35
35
|
const PrincipalStatusValidator = type.enumerated(...principalStatuses);
|
|
36
|
+
const principalKeyStatuses = ["active", "retired"];
|
|
37
|
+
const PrincipalKeyStatusValidator = type.enumerated(...principalKeyStatuses);
|
|
36
38
|
const credentialTypes = [
|
|
37
39
|
"api_key",
|
|
38
40
|
"oauth_token",
|
|
@@ -113,6 +115,12 @@ export function parsePrincipalRow(row) {
|
|
|
113
115
|
status: PrincipalStatusValidator.assert(row.status),
|
|
114
116
|
};
|
|
115
117
|
}
|
|
118
|
+
export function parsePrincipalKeyRow(row) {
|
|
119
|
+
return {
|
|
120
|
+
...row,
|
|
121
|
+
status: PrincipalKeyStatusValidator.assert(row.status),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
116
124
|
export function parseSignalCorrelationRow(row) {
|
|
117
125
|
return {
|
|
118
126
|
...row,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { CredentialCipher } from "@intx/types";
|
|
2
|
+
import type { DB, DBExecutor } from "./client.js";
|
|
3
|
+
type DBHandle = DB["db"];
|
|
4
|
+
export type CreatePrincipalKeyStoreDeps = {
|
|
5
|
+
db: DBHandle;
|
|
6
|
+
/**
|
|
7
|
+
* Seals the private key seed at rest. Production supplies a real env-key
|
|
8
|
+
* cipher under `PRINCIPAL_KEY_ENCRYPTION_KEY`; tests and local dev may supply
|
|
9
|
+
* the noop cipher, which stores the seed as plaintext.
|
|
10
|
+
*/
|
|
11
|
+
cipher: CredentialCipher;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Store for the `principal_key` table -- a principal's Ed25519 signing key.
|
|
15
|
+
*
|
|
16
|
+
* The hub custodies the private key: it mints, seals, and signs with it on the
|
|
17
|
+
* principal's behalf. A signature therefore ATTRIBUTES an action to a principal
|
|
18
|
+
* but is NOT non-repudiable against the hub operator, who holds the key. See
|
|
19
|
+
* docs/AUTH.md.
|
|
20
|
+
*
|
|
21
|
+
* The store owns the one-active-key-per-principal invariant end to end: `sign`
|
|
22
|
+
* and `getPublicKey` resolve the single active row, and `generate` inserts a new
|
|
23
|
+
* active key that the table's partial unique index rejects if the principal
|
|
24
|
+
* already has one. The private seed never leaves this module -- `sign` decrypts,
|
|
25
|
+
* signs, and discards it; no method returns private material.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createPrincipalKeyStore({ db, cipher, }: CreatePrincipalKeyStoreDeps): {
|
|
28
|
+
/**
|
|
29
|
+
* Mint a fresh active signing key for a principal. Generates an Ed25519 key
|
|
30
|
+
* pair, seals the hex-encoded 32-byte seed with the cipher bound to the
|
|
31
|
+
* key's row and column, and inserts it. A principal that already holds an
|
|
32
|
+
* active key trips the partial unique index and this throws. Returns the
|
|
33
|
+
* hex-encoded public key; never returns private material.
|
|
34
|
+
*/
|
|
35
|
+
generate(principalId: string, tx?: DBExecutor): Promise<string>;
|
|
36
|
+
/**
|
|
37
|
+
* Sign a message with the principal's active key. Decrypts the sealed seed,
|
|
38
|
+
* produces the raw 64-byte Ed25519 signature, and discards the seed. Throws
|
|
39
|
+
* when the principal has no active key. The seed stays a local that goes out
|
|
40
|
+
* of scope; it is never returned or logged.
|
|
41
|
+
*/
|
|
42
|
+
sign(principalId: string, message: Uint8Array, tx?: DBExecutor): Promise<Uint8Array>;
|
|
43
|
+
/**
|
|
44
|
+
* The hex-encoded public key of a principal's active signing key. Throws
|
|
45
|
+
* when the principal has no active key rather than defaulting, so a missing
|
|
46
|
+
* key surfaces at the call site instead of downstream.
|
|
47
|
+
*/
|
|
48
|
+
getPublicKey(principalId: string, tx?: DBExecutor): Promise<string>;
|
|
49
|
+
};
|
|
50
|
+
export type PrincipalKeyStore = ReturnType<typeof createPrincipalKeyStore>;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { generateKeyPair, signEd25519 } from "@intx/crypto";
|
|
3
|
+
import { hexDecode, hexEncode, principalKeyAad } from "@intx/types";
|
|
4
|
+
import { generateId } from "@intx/hub-common";
|
|
5
|
+
import { principalKey } from "./schema/principal-keys.js";
|
|
6
|
+
/**
|
|
7
|
+
* Store for the `principal_key` table -- a principal's Ed25519 signing key.
|
|
8
|
+
*
|
|
9
|
+
* The hub custodies the private key: it mints, seals, and signs with it on the
|
|
10
|
+
* principal's behalf. A signature therefore ATTRIBUTES an action to a principal
|
|
11
|
+
* but is NOT non-repudiable against the hub operator, who holds the key. See
|
|
12
|
+
* docs/AUTH.md.
|
|
13
|
+
*
|
|
14
|
+
* The store owns the one-active-key-per-principal invariant end to end: `sign`
|
|
15
|
+
* and `getPublicKey` resolve the single active row, and `generate` inserts a new
|
|
16
|
+
* active key that the table's partial unique index rejects if the principal
|
|
17
|
+
* already has one. The private seed never leaves this module -- `sign` decrypts,
|
|
18
|
+
* signs, and discards it; no method returns private material.
|
|
19
|
+
*/
|
|
20
|
+
export function createPrincipalKeyStore({ db, cipher, }) {
|
|
21
|
+
async function loadActive(principalId, tx) {
|
|
22
|
+
const [row] = await (tx ?? db)
|
|
23
|
+
.select()
|
|
24
|
+
.from(principalKey)
|
|
25
|
+
.where(and(eq(principalKey.principalId, principalId), eq(principalKey.status, "active")))
|
|
26
|
+
.limit(1);
|
|
27
|
+
return row;
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
/**
|
|
31
|
+
* Mint a fresh active signing key for a principal. Generates an Ed25519 key
|
|
32
|
+
* pair, seals the hex-encoded 32-byte seed with the cipher bound to the
|
|
33
|
+
* key's row and column, and inserts it. A principal that already holds an
|
|
34
|
+
* active key trips the partial unique index and this throws. Returns the
|
|
35
|
+
* hex-encoded public key; never returns private material.
|
|
36
|
+
*/
|
|
37
|
+
async generate(principalId, tx) {
|
|
38
|
+
const id = generateId("principalKey");
|
|
39
|
+
const keyPair = await generateKeyPair();
|
|
40
|
+
const sealedPrivateKey = await cipher.encrypt(hexEncode(keyPair.privateKey), principalKeyAad(id, "private_key"));
|
|
41
|
+
const publicKey = hexEncode(keyPair.publicKey);
|
|
42
|
+
const now = new Date();
|
|
43
|
+
await (tx ?? db).insert(principalKey).values({
|
|
44
|
+
id,
|
|
45
|
+
principalId,
|
|
46
|
+
publicKey,
|
|
47
|
+
privateKey: sealedPrivateKey,
|
|
48
|
+
status: "active",
|
|
49
|
+
createdAt: now,
|
|
50
|
+
updatedAt: now,
|
|
51
|
+
});
|
|
52
|
+
return publicKey;
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Sign a message with the principal's active key. Decrypts the sealed seed,
|
|
56
|
+
* produces the raw 64-byte Ed25519 signature, and discards the seed. Throws
|
|
57
|
+
* when the principal has no active key. The seed stays a local that goes out
|
|
58
|
+
* of scope; it is never returned or logged.
|
|
59
|
+
*/
|
|
60
|
+
async sign(principalId, message, tx) {
|
|
61
|
+
const row = await loadActive(principalId, tx);
|
|
62
|
+
if (row === undefined) {
|
|
63
|
+
throw new Error(`principalKeyStore.sign: principal ${principalId} has no active key`);
|
|
64
|
+
}
|
|
65
|
+
const seedHex = await cipher.decrypt(row.privateKey, principalKeyAad(row.id, "private_key"));
|
|
66
|
+
const seed = hexDecode(seedHex);
|
|
67
|
+
return signEd25519(seed, message);
|
|
68
|
+
},
|
|
69
|
+
/**
|
|
70
|
+
* The hex-encoded public key of a principal's active signing key. Throws
|
|
71
|
+
* when the principal has no active key rather than defaulting, so a missing
|
|
72
|
+
* key surfaces at the call site instead of downstream.
|
|
73
|
+
*/
|
|
74
|
+
async getPublicKey(principalId, tx) {
|
|
75
|
+
const row = await loadActive(principalId, tx);
|
|
76
|
+
if (row === undefined) {
|
|
77
|
+
throw new Error(`principalKeyStore.getPublicKey: principal ${principalId} has no active key`);
|
|
78
|
+
}
|
|
79
|
+
return row.publicKey;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { DB, DBExecutor } from "./client.js";
|
|
2
|
+
import { principal } from "./schema/principals.js";
|
|
3
|
+
import { parsePrincipalRow } from "./parse-row.js";
|
|
4
|
+
import type { PrincipalKeyStore } from "./principal-key-store.js";
|
|
5
|
+
type DBHandle = DB["db"];
|
|
6
|
+
type PrincipalInsert = typeof principal.$inferInsert;
|
|
7
|
+
type ParsedPrincipal = ReturnType<typeof parsePrincipalRow>;
|
|
8
|
+
/**
|
|
9
|
+
* Store for the `principal` table -- the single owner of principal creation.
|
|
10
|
+
* Every principal insert in the system routes through this one factory, and
|
|
11
|
+
* every principal it creates is minted an active signing key in the same
|
|
12
|
+
* transaction, so a principal never exists without a key.
|
|
13
|
+
*
|
|
14
|
+
* Each method accepts an optional transaction handle so the principal row and
|
|
15
|
+
* its key join the transaction that co-writes the principal's roles and grants.
|
|
16
|
+
* When no handle is passed the store opens its own transaction, because the
|
|
17
|
+
* principal insert and the key mint are two writes that must commit together.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createPrincipalStore(db: DBHandle, principalKeyStore: PrincipalKeyStore): {
|
|
20
|
+
/**
|
|
21
|
+
* Insert a new principal and mint its signing key, failing loudly on a
|
|
22
|
+
* natural-key conflict. Callers that have already established the principal
|
|
23
|
+
* is new (a fresh tenant owner, an invite past its existence pre-check) use
|
|
24
|
+
* this so a concurrent duplicate surfaces as a unique violation rather than
|
|
25
|
+
* a silent success.
|
|
26
|
+
*/
|
|
27
|
+
create(row: PrincipalInsert, tx?: DBExecutor): Promise<ParsedPrincipal>;
|
|
28
|
+
/**
|
|
29
|
+
* Idempotent variant of `create` for the run path, where concurrent first
|
|
30
|
+
* deliveries race to reserve the same principal. On a win it inserts the
|
|
31
|
+
* principal, mints its key, and returns the parsed row; on a loss it returns
|
|
32
|
+
* `null` WITHOUT minting -- the winning transaction already minted the one
|
|
33
|
+
* active key -- so the caller can fall back to the winner's state.
|
|
34
|
+
*
|
|
35
|
+
* The conflict target is the natural key `(tenantId, kind, refId)`, NOT the
|
|
36
|
+
* surrogate `id`: user principals carry a random `generateId("principal")`,
|
|
37
|
+
* so their only real conflict key is the natural triple. The run path
|
|
38
|
+
* derives a deterministic id that agrees with the triple, so one target
|
|
39
|
+
* unifies both callers. Do not switch this to `principal.id`.
|
|
40
|
+
*/
|
|
41
|
+
createIfAbsent(row: PrincipalInsert, tx?: DBExecutor): Promise<ParsedPrincipal | null>;
|
|
42
|
+
};
|
|
43
|
+
export type PrincipalStore = ReturnType<typeof createPrincipalStore>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { principal } from "./schema/principals.js";
|
|
2
|
+
import { parsePrincipalRow } from "./parse-row.js";
|
|
3
|
+
/**
|
|
4
|
+
* Store for the `principal` table -- the single owner of principal creation.
|
|
5
|
+
* Every principal insert in the system routes through this one factory, and
|
|
6
|
+
* every principal it creates is minted an active signing key in the same
|
|
7
|
+
* transaction, so a principal never exists without a key.
|
|
8
|
+
*
|
|
9
|
+
* Each method accepts an optional transaction handle so the principal row and
|
|
10
|
+
* its key join the transaction that co-writes the principal's roles and grants.
|
|
11
|
+
* When no handle is passed the store opens its own transaction, because the
|
|
12
|
+
* principal insert and the key mint are two writes that must commit together.
|
|
13
|
+
*/
|
|
14
|
+
export function createPrincipalStore(db, principalKeyStore) {
|
|
15
|
+
return {
|
|
16
|
+
/**
|
|
17
|
+
* Insert a new principal and mint its signing key, failing loudly on a
|
|
18
|
+
* natural-key conflict. Callers that have already established the principal
|
|
19
|
+
* is new (a fresh tenant owner, an invite past its existence pre-check) use
|
|
20
|
+
* this so a concurrent duplicate surfaces as a unique violation rather than
|
|
21
|
+
* a silent success.
|
|
22
|
+
*/
|
|
23
|
+
async create(row, tx) {
|
|
24
|
+
const run = async (executor) => {
|
|
25
|
+
const [inserted] = await executor
|
|
26
|
+
.insert(principal)
|
|
27
|
+
.values(row)
|
|
28
|
+
.returning();
|
|
29
|
+
if (inserted === undefined) {
|
|
30
|
+
throw new Error(`principalStore.create: insert returned no row for ${row.id}`);
|
|
31
|
+
}
|
|
32
|
+
await principalKeyStore.generate(inserted.id, executor);
|
|
33
|
+
return parsePrincipalRow(inserted);
|
|
34
|
+
};
|
|
35
|
+
return tx === undefined ? db.transaction(run) : run(tx);
|
|
36
|
+
},
|
|
37
|
+
/**
|
|
38
|
+
* Idempotent variant of `create` for the run path, where concurrent first
|
|
39
|
+
* deliveries race to reserve the same principal. On a win it inserts the
|
|
40
|
+
* principal, mints its key, and returns the parsed row; on a loss it returns
|
|
41
|
+
* `null` WITHOUT minting -- the winning transaction already minted the one
|
|
42
|
+
* active key -- so the caller can fall back to the winner's state.
|
|
43
|
+
*
|
|
44
|
+
* The conflict target is the natural key `(tenantId, kind, refId)`, NOT the
|
|
45
|
+
* surrogate `id`: user principals carry a random `generateId("principal")`,
|
|
46
|
+
* so their only real conflict key is the natural triple. The run path
|
|
47
|
+
* derives a deterministic id that agrees with the triple, so one target
|
|
48
|
+
* unifies both callers. Do not switch this to `principal.id`.
|
|
49
|
+
*/
|
|
50
|
+
async createIfAbsent(row, tx) {
|
|
51
|
+
const run = async (executor) => {
|
|
52
|
+
const [inserted] = await executor
|
|
53
|
+
.insert(principal)
|
|
54
|
+
.values(row)
|
|
55
|
+
.onConflictDoNothing({
|
|
56
|
+
target: [principal.tenantId, principal.kind, principal.refId],
|
|
57
|
+
})
|
|
58
|
+
.returning();
|
|
59
|
+
if (inserted === undefined)
|
|
60
|
+
return null;
|
|
61
|
+
await principalKeyStore.generate(inserted.id, executor);
|
|
62
|
+
return parsePrincipalRow(inserted);
|
|
63
|
+
};
|
|
64
|
+
return tx === undefined ? db.transaction(run) : run(tx);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/schema/approvals.js
CHANGED
|
@@ -49,4 +49,9 @@ export const approval = pgTable("approval", {
|
|
|
49
49
|
}, (t) => [
|
|
50
50
|
index("approval_tenant_status_idx").on(t.tenantId, t.status, t.createdAt),
|
|
51
51
|
index("approval_anchor_run_idx").on(t.anchorRunId),
|
|
52
|
+
// Reads that list a single run's approvals (the run-scoped approval and
|
|
53
|
+
// authorization views) filter on runId; the anchor_run index above does
|
|
54
|
+
// not serve them because a child run's approvals carry a runId distinct
|
|
55
|
+
// from the deployment's anchor.
|
|
56
|
+
index("approval_run_idx").on(t.runId),
|
|
52
57
|
]);
|
|
@@ -24,7 +24,7 @@ import { tenant } from "./tenants.js";
|
|
|
24
24
|
// (`receivePack`, `createPack`, `resolveRef`, ...). The mint API
|
|
25
25
|
// translates user-facing aliases to canonical names before insert.
|
|
26
26
|
// `resource` is the single substrate authz resource string
|
|
27
|
-
// (e.g. `agent-state:run_xxx`, `asset:
|
|
27
|
+
// (e.g. `agent-state:run_xxx`, `asset:ast_yyy`) that the token grants
|
|
28
28
|
// access to. `refPattern` is a glob restricting which refs within the
|
|
29
29
|
// resource the token may read or write.
|
|
30
30
|
//
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./auth.js";
|
|
2
2
|
export * from "./tenants.js";
|
|
3
3
|
export * from "./principals.js";
|
|
4
|
+
export * from "./principal-keys.js";
|
|
4
5
|
export * from "./roles.js";
|
|
5
6
|
export * from "./grants.js";
|
|
6
7
|
export * from "./approvals.js";
|
|
@@ -23,3 +24,4 @@ export * from "./workflow-run.js";
|
|
|
23
24
|
export * from "./workflow-run-dispatch.js";
|
|
24
25
|
export * from "./workflow-run-launch-spec.js";
|
|
25
26
|
export * from "./workflow-run-execution.js";
|
|
27
|
+
export * from "./workflow-probe.js";
|
package/dist/schema/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./auth.js";
|
|
2
2
|
export * from "./tenants.js";
|
|
3
3
|
export * from "./principals.js";
|
|
4
|
+
export * from "./principal-keys.js";
|
|
4
5
|
export * from "./roles.js";
|
|
5
6
|
export * from "./grants.js";
|
|
6
7
|
export * from "./approvals.js";
|
|
@@ -23,3 +24,4 @@ export * from "./workflow-run.js";
|
|
|
23
24
|
export * from "./workflow-run-dispatch.js";
|
|
24
25
|
export * from "./workflow-run-launch-spec.js";
|
|
25
26
|
export * from "./workflow-run-execution.js";
|
|
27
|
+
export * from "./workflow-probe.js";
|