@intx/db 0.1.2
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 +37 -0
- package/drizzle.config.ts +23 -0
- package/migrations/.gitkeep +0 -0
- package/migrations/0000_brown_wither.sql +50 -0
- package/migrations/0001_white_aqueduct.sql +105 -0
- package/migrations/0002_clever_falcon.sql +56 -0
- package/migrations/0003_stiff_tyrannus.sql +44 -0
- package/migrations/0004_rename_capability_to_offering.sql +1 -0
- package/migrations/0005_gigantic_cardiac.sql +1 -0
- package/migrations/0006_sidecar.sql +8 -0
- package/migrations/0007_agent_running_session.sql +1 -0
- package/migrations/0008_session.sql +10 -0
- package/migrations/0009_session_messages.sql +18 -0
- package/migrations/0010_agent_sidecar_pubkey.sql +2 -0
- package/migrations/0011_session_message_from.sql +2 -0
- package/migrations/0012_agent_instance.sql +22 -0
- package/migrations/0013_instance_session_id.sql +2 -0
- package/migrations/0014_drop_agent_runtime_columns.sql +12 -0
- package/migrations/0015_add_instance_id_to_session_message.sql +2 -0
- package/migrations/0016_jazzy_gamma_corps.sql +3 -0
- package/migrations/0017_hesitant_marvex.sql +1 -0
- package/migrations/0018_natural_sinister_six.sql +5 -0
- package/migrations/0019_rename_grant_source_to_origin.sql +1 -0
- package/migrations/0020_add_agent_role.sql +9 -0
- package/migrations/0021_acoustic_ozymandias.sql +15 -0
- package/migrations/0022_material_sleepwalker.sql +27 -0
- package/migrations/0023_flawless_scarlet_witch.sql +2 -0
- package/migrations/0024_bumpy_sharon_ventura.sql +1 -0
- package/migrations/0025_curvy_firestar.sql +26 -0
- package/migrations/0026_keen_ultimo.sql +13 -0
- package/migrations/0027_git_tokens.sql +21 -0
- package/migrations/0028_wet_sugar_man.sql +1 -0
- package/migrations/meta/0000_snapshot.json +316 -0
- package/migrations/meta/0001_snapshot.json +968 -0
- package/migrations/meta/0002_snapshot.json +1315 -0
- package/migrations/meta/0003_snapshot.json +1594 -0
- package/migrations/meta/0004_snapshot.json +1594 -0
- package/migrations/meta/0005_snapshot.json +1600 -0
- package/migrations/meta/0011_snapshot.json +1921 -0
- package/migrations/meta/0012_snapshot.json +2067 -0
- package/migrations/meta/0013_snapshot.json +2082 -0
- package/migrations/meta/0014_snapshot.json +2049 -0
- package/migrations/meta/0015_snapshot.json +2064 -0
- package/migrations/meta/0016_snapshot.json +2085 -0
- package/migrations/meta/0017_snapshot.json +2085 -0
- package/migrations/meta/0018_snapshot.json +2070 -0
- package/migrations/meta/0019_snapshot.json +2070 -0
- package/migrations/meta/0020_snapshot.json +2126 -0
- package/migrations/meta/0021_snapshot.json +2239 -0
- package/migrations/meta/0022_snapshot.json +2425 -0
- package/migrations/meta/0023_snapshot.json +2260 -0
- package/migrations/meta/0024_snapshot.json +2254 -0
- package/migrations/meta/0025_snapshot.json +2418 -0
- package/migrations/meta/0026_snapshot.json +2508 -0
- package/migrations/meta/0027_snapshot.json +2657 -0
- package/migrations/meta/0028_snapshot.json +2657 -0
- package/migrations/meta/_journal.json +209 -0
- package/package.json +27 -0
- package/src/client.ts +24 -0
- package/src/config.ts +19 -0
- package/src/connection.ts +27 -0
- package/src/credential-resolution.ts +378 -0
- package/src/grant-store.ts +51 -0
- package/src/index.ts +32 -0
- package/src/migrate.test.ts +35 -0
- package/src/migrate.ts +168 -0
- package/src/parse-row.test.ts +113 -0
- package/src/parse-row.ts +185 -0
- package/src/schema/agent-assets.ts +21 -0
- package/src/schema/agents.ts +49 -0
- package/src/schema/assets.ts +24 -0
- package/src/schema/auth.ts +51 -0
- package/src/schema/credentials.ts +43 -0
- package/src/schema/git-tokens.ts +83 -0
- package/src/schema/grants.ts +26 -0
- package/src/schema/index.ts +19 -0
- package/src/schema/instances.ts +36 -0
- package/src/schema/messages.ts +108 -0
- package/src/schema/oauth-clients.ts +26 -0
- package/src/schema/offerings.ts +20 -0
- package/src/schema/principals.ts +28 -0
- package/src/schema/providers.ts +23 -0
- package/src/schema/roles.ts +51 -0
- package/src/schema/session-assets.ts +49 -0
- package/src/schema/sessions.ts +26 -0
- package/src/schema/sidecar.ts +14 -0
- package/src/schema/tenants.ts +41 -0
- package/src/schema/wallets.ts +44 -0
- package/src/tenant-hierarchy.ts +34 -0
- package/tsconfig.json +4 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { eq, and, or, isNull, gt, inArray } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import type { GrantRule, GrantStore } from "@intx/types/authz";
|
|
4
|
+
|
|
5
|
+
import type { DB } from "./client";
|
|
6
|
+
import { grant } from "./schema/grants";
|
|
7
|
+
import { principalRole } from "./schema/roles";
|
|
8
|
+
import { parseGrantRow } from "./parse-row";
|
|
9
|
+
|
|
10
|
+
function toGrantRule(row: typeof grant.$inferSelect): GrantRule {
|
|
11
|
+
const parsed = parseGrantRow(row);
|
|
12
|
+
return {
|
|
13
|
+
id: parsed.id,
|
|
14
|
+
resource: parsed.resource,
|
|
15
|
+
action: parsed.action,
|
|
16
|
+
effect: parsed.effect,
|
|
17
|
+
origin: parsed.origin,
|
|
18
|
+
conditions: parsed.conditions,
|
|
19
|
+
expiresAt: parsed.expiresAt,
|
|
20
|
+
roleId: parsed.roleId,
|
|
21
|
+
principalId: parsed.principalId,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createGrantStore(db: DB["db"]): GrantStore {
|
|
26
|
+
return {
|
|
27
|
+
async collectGrants(principalId, tenantId) {
|
|
28
|
+
const roleAssignments = await db.query.principalRole.findMany({
|
|
29
|
+
where: eq(principalRole.principalId, principalId),
|
|
30
|
+
});
|
|
31
|
+
const roleIds = roleAssignments.map((a) => a.roleId);
|
|
32
|
+
|
|
33
|
+
const now = new Date();
|
|
34
|
+
|
|
35
|
+
const ownership = [eq(grant.principalId, principalId)];
|
|
36
|
+
if (roleIds.length > 0) {
|
|
37
|
+
ownership.push(inArray(grant.roleId, roleIds));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rows = await db.query.grant.findMany({
|
|
41
|
+
where: and(
|
|
42
|
+
eq(grant.tenantId, tenantId),
|
|
43
|
+
or(...ownership),
|
|
44
|
+
or(isNull(grant.expiresAt), gt(grant.expiresAt, now)),
|
|
45
|
+
),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return rows.map(toGrantRule);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export { createDB, type DB } from "./client";
|
|
2
|
+
export type { DBConfig } from "./config";
|
|
3
|
+
export { runMigrations, dropSchema } from "./migrate";
|
|
4
|
+
export { createGrantStore } from "./grant-store";
|
|
5
|
+
export { getAncestorChain } from "./tenant-hierarchy";
|
|
6
|
+
export {
|
|
7
|
+
resolveProviderByName,
|
|
8
|
+
resolveOAuthClient,
|
|
9
|
+
resolveCredentialByName,
|
|
10
|
+
resolveCredentialById,
|
|
11
|
+
resolveCredentialRequirement,
|
|
12
|
+
resolveOneCredential,
|
|
13
|
+
resolveInstanceSources,
|
|
14
|
+
type CredentialOutcome,
|
|
15
|
+
ProviderMetadata,
|
|
16
|
+
} from "./credential-resolution";
|
|
17
|
+
export {
|
|
18
|
+
parseAgentRow,
|
|
19
|
+
parseAgentVersionRow,
|
|
20
|
+
parseGrantRow,
|
|
21
|
+
parseOfferingRow,
|
|
22
|
+
parseCredentialRow,
|
|
23
|
+
parseProviderRow,
|
|
24
|
+
parseTenantRow,
|
|
25
|
+
parseWalletRow,
|
|
26
|
+
parseTransactionRow,
|
|
27
|
+
parseOAuthClientRow,
|
|
28
|
+
parseGitTokenRow,
|
|
29
|
+
parseSidecarStatus,
|
|
30
|
+
parseTurnPartType,
|
|
31
|
+
} from "./parse-row";
|
|
32
|
+
export * as schema from "./schema";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { rewriteSchemaQualifiedReferences } from "./migrate";
|
|
4
|
+
|
|
5
|
+
describe('migration "public" substitution', () => {
|
|
6
|
+
test("rewrites schema-qualified references", () => {
|
|
7
|
+
const sql =
|
|
8
|
+
'ALTER TABLE "x" ADD CONSTRAINT "fk" FOREIGN KEY ("u") REFERENCES "public"."user"("id");';
|
|
9
|
+
expect(rewriteSchemaQualifiedReferences(sql, '"test_schema"')).toBe(
|
|
10
|
+
'ALTER TABLE "x" ADD CONSTRAINT "fk" FOREIGN KEY ("u") REFERENCES "test_schema"."user"("id");',
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("leaves bare quoted public in string literals alone", () => {
|
|
15
|
+
const sql = `INSERT INTO "x" VALUES ('"public"');`;
|
|
16
|
+
expect(rewriteSchemaQualifiedReferences(sql, '"t"')).toBe(sql);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("substitutes multiple occurrences on a single line", () => {
|
|
20
|
+
const sql = 'REFERENCES "public"."a"("id"), "public"."b"("id")';
|
|
21
|
+
expect(rewriteSchemaQualifiedReferences(sql, '"s"')).toBe(
|
|
22
|
+
'REFERENCES "s"."a"("id"), "s"."b"("id")',
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("no-op when target schema is public", () => {
|
|
27
|
+
const sql = 'REFERENCES "public"."user"("id")';
|
|
28
|
+
expect(rewriteSchemaQualifiedReferences(sql, '"public"')).toBe(sql);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("does not match a trailing apostrophe (bare reference)", () => {
|
|
32
|
+
const sql = `SELECT '"public"' AS literal;`;
|
|
33
|
+
expect(rewriteSchemaQualifiedReferences(sql, '"s"')).toBe(sql);
|
|
34
|
+
});
|
|
35
|
+
});
|
package/src/migrate.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { type } from "arktype";
|
|
5
|
+
import postgres from "postgres";
|
|
6
|
+
|
|
7
|
+
import { DBConfig } from "./config";
|
|
8
|
+
|
|
9
|
+
// Resolution of the migrations directory: the package layout pins
|
|
10
|
+
// the drizzle-generated SQL at `<pkgRoot>/migrations`. This file
|
|
11
|
+
// lives at `<pkgRoot>/src/migrate.ts`, so the directory is one level
|
|
12
|
+
// up from `__dirname`. Resolving via `import.meta` keeps the runtime
|
|
13
|
+
// honest about where it is reading SQL from instead of relying on a
|
|
14
|
+
// cwd-relative path that breaks under callers that change `cwd`.
|
|
15
|
+
const MIGRATIONS_DIR = path.resolve(
|
|
16
|
+
path.dirname(new URL(import.meta.url).pathname),
|
|
17
|
+
"..",
|
|
18
|
+
"migrations",
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
function quoteIdentifier(name: string): string {
|
|
22
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Rewrite drizzle-generated schema-qualified references like
|
|
27
|
+
* `REFERENCES "public"."<table>"` so they point at the target
|
|
28
|
+
* schema. The pattern matches `"public".` only when followed by
|
|
29
|
+
* another quoted identifier, leaving bare `"public"` inside string
|
|
30
|
+
* literals alone.
|
|
31
|
+
*/
|
|
32
|
+
export function rewriteSchemaQualifiedReferences(
|
|
33
|
+
sql: string,
|
|
34
|
+
schemaIdent: string,
|
|
35
|
+
): string {
|
|
36
|
+
return sql.replace(/"public"\.(?=")/g, `${schemaIdent}.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Construct the single-connection postgres client used by the
|
|
41
|
+
* migration and teardown entry points. Both paths share the same SSL
|
|
42
|
+
* passthrough and suppress NOTICE-level diagnostics (cascade reports,
|
|
43
|
+
* "schema already exists") that postgres.js logs by default but are
|
|
44
|
+
* informational only in this harness context. The optional `searchPath`
|
|
45
|
+
* pins `search_path` on connection-open so unqualified `CREATE TABLE`
|
|
46
|
+
* statements land in the caller's schema.
|
|
47
|
+
*/
|
|
48
|
+
function createMigrationClient(
|
|
49
|
+
config: DBConfig,
|
|
50
|
+
searchPath?: string,
|
|
51
|
+
): ReturnType<typeof postgres> {
|
|
52
|
+
return postgres({
|
|
53
|
+
host: config.host,
|
|
54
|
+
port: config.port,
|
|
55
|
+
user: config.user,
|
|
56
|
+
password: config.password,
|
|
57
|
+
database: config.database,
|
|
58
|
+
max: 1,
|
|
59
|
+
onnotice: () => undefined,
|
|
60
|
+
...(config.ssl !== undefined && { ssl: config.ssl }),
|
|
61
|
+
...(searchPath !== undefined && {
|
|
62
|
+
connection: { search_path: searchPath },
|
|
63
|
+
}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Apply the drizzle-generated migration SQL into the given postgres
|
|
69
|
+
* schema. The schema is created if absent; nothing else is dropped.
|
|
70
|
+
*
|
|
71
|
+
* The migration source files reference the destination schema
|
|
72
|
+
* literally (e.g. `"public"."user"` in FK constraints). To make the
|
|
73
|
+
* same source apply cleanly into an arbitrary schema, we substitute
|
|
74
|
+
* the literal token `"public"` with the quoted target schema before
|
|
75
|
+
* executing each file. This is a textual substitution rather than a
|
|
76
|
+
* postgres-side `search_path` trick because the FK references are
|
|
77
|
+
* fully qualified; `search_path` would not redirect them.
|
|
78
|
+
*
|
|
79
|
+
* The substitution is bounded: the migration source is
|
|
80
|
+
* machine-generated and never contains the string `"public"` other
|
|
81
|
+
* than in schema-qualified identifiers, so there is no ambiguity to
|
|
82
|
+
* worry about. The substitution is a no-op when the target schema is
|
|
83
|
+
* literally `public`, which is the common case.
|
|
84
|
+
*/
|
|
85
|
+
export async function runMigrations(
|
|
86
|
+
configRaw: unknown,
|
|
87
|
+
options: { schema: string },
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
const config = DBConfig(configRaw);
|
|
90
|
+
if (config instanceof type.errors) {
|
|
91
|
+
throw new Error(`Invalid database config: ${config.summary}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const { schema } = options;
|
|
95
|
+
if (schema.length === 0) {
|
|
96
|
+
throw new Error("runMigrations: schema name must not be empty");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const schemaIdent = quoteIdentifier(schema);
|
|
100
|
+
|
|
101
|
+
// Pin search_path on the migration connection so unqualified
|
|
102
|
+
// `CREATE TABLE "name"` statements land in the target schema.
|
|
103
|
+
// The FK references in the source SQL are already schema-qualified
|
|
104
|
+
// (`"public"."user"`); we rewrite those to the target schema
|
|
105
|
+
// below. Together these two mechanisms route every object the
|
|
106
|
+
// migration touches into the caller's schema.
|
|
107
|
+
const sql = createMigrationClient(config, schemaIdent);
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// The CREATE SCHEMA must run on a connection that does not
|
|
111
|
+
// require the schema to already exist for search_path to be
|
|
112
|
+
// applied. postgres.js sets the GUC after connection-open, so
|
|
113
|
+
// CREATE SCHEMA IF NOT EXISTS here is the first statement and
|
|
114
|
+
// creates the schema before search_path matters.
|
|
115
|
+
await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS ${schemaIdent}`);
|
|
116
|
+
|
|
117
|
+
const files = (await readdir(MIGRATIONS_DIR))
|
|
118
|
+
.filter((f) => f.endsWith(".sql"))
|
|
119
|
+
.sort();
|
|
120
|
+
|
|
121
|
+
if (files.length === 0) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`runMigrations: no .sql files found in ${MIGRATIONS_DIR}`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const raw = await readFile(path.join(MIGRATIONS_DIR, file), "utf-8");
|
|
129
|
+
const rendered = rewriteSchemaQualifiedReferences(raw, schemaIdent);
|
|
130
|
+
// drizzle emits multi-statement files separated by its own
|
|
131
|
+
// statement-breakpoint marker. Split on it and execute each
|
|
132
|
+
// statement individually so a syntax error in one statement
|
|
133
|
+
// surfaces with the right context.
|
|
134
|
+
const statements = rendered
|
|
135
|
+
.split("--> statement-breakpoint")
|
|
136
|
+
.map((s) => s.trim())
|
|
137
|
+
.filter((s) => s.length > 0);
|
|
138
|
+
for (const stmt of statements) {
|
|
139
|
+
await sql.unsafe(stmt);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
await sql.end();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Drop the given schema along with everything in it. Used by the
|
|
149
|
+
* integration-test harness to tear down per-test schemas.
|
|
150
|
+
*/
|
|
151
|
+
export async function dropSchema(
|
|
152
|
+
configRaw: unknown,
|
|
153
|
+
options: { schema: string },
|
|
154
|
+
): Promise<void> {
|
|
155
|
+
const config = DBConfig(configRaw);
|
|
156
|
+
if (config instanceof type.errors) {
|
|
157
|
+
throw new Error(`Invalid database config: ${config.summary}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const sql = createMigrationClient(config);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const schemaIdent = quoteIdentifier(options.schema);
|
|
164
|
+
await sql.unsafe(`DROP SCHEMA IF EXISTS ${schemaIdent} CASCADE`);
|
|
165
|
+
} finally {
|
|
166
|
+
await sql.end();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { RepoAction } from "@intx/types/sidecar";
|
|
4
|
+
|
|
5
|
+
import { GitTokenKindValidator, parseGitTokenRow } from "./parse-row";
|
|
6
|
+
import type { gitToken } from "./schema";
|
|
7
|
+
|
|
8
|
+
type GitTokenRow = typeof gitToken.$inferSelect;
|
|
9
|
+
|
|
10
|
+
function makeRow(overrides: Partial<GitTokenRow> = {}): GitTokenRow {
|
|
11
|
+
const now = new Date();
|
|
12
|
+
return {
|
|
13
|
+
id: "gtk_0123456789abcdef0123456789abcdef",
|
|
14
|
+
tenantId: null,
|
|
15
|
+
userId: "user_alice",
|
|
16
|
+
principalId: null,
|
|
17
|
+
name: "laptop",
|
|
18
|
+
kind: "pat",
|
|
19
|
+
tokenHashSha256: new Uint8Array(32),
|
|
20
|
+
resource: "agent-state:ins_test",
|
|
21
|
+
refPattern: "refs/heads/*",
|
|
22
|
+
actions: ["receivePack", "createPack", "resolveRef"],
|
|
23
|
+
expiresAt: new Date("2027-01-01T00:00:00Z"),
|
|
24
|
+
revokedAt: null,
|
|
25
|
+
createdAt: now,
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("parseGitTokenRow", () => {
|
|
31
|
+
test("round-trips a personal pat with concrete repo scope", () => {
|
|
32
|
+
const row = makeRow();
|
|
33
|
+
const parsed = parseGitTokenRow(row);
|
|
34
|
+
|
|
35
|
+
expect(parsed.id).toBe(row.id);
|
|
36
|
+
expect(parsed.tenantId).toBeNull();
|
|
37
|
+
expect(parsed.userId).toBe(row.userId);
|
|
38
|
+
expect(parsed.principalId).toBeNull();
|
|
39
|
+
expect(parsed.name).toBe(row.name);
|
|
40
|
+
expect(parsed.kind).toBe("pat");
|
|
41
|
+
expect(parsed.tokenHashSha256).toBe(row.tokenHashSha256);
|
|
42
|
+
expect(parsed.actions).toEqual(["receivePack", "createPack", "resolveRef"]);
|
|
43
|
+
expect(parsed.resource).toBe("agent-state:ins_test");
|
|
44
|
+
expect(parsed.refPattern).toBe("refs/heads/*");
|
|
45
|
+
expect(parsed.expiresAt).toBe(row.expiresAt);
|
|
46
|
+
expect(parsed.revokedAt).toBeNull();
|
|
47
|
+
expect(parsed.createdAt).toBe(row.createdAt);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("round-trips a tenant-restricted pat", () => {
|
|
51
|
+
const row = makeRow({
|
|
52
|
+
tenantId: "tnt_acme",
|
|
53
|
+
name: "acme-only",
|
|
54
|
+
});
|
|
55
|
+
const parsed = parseGitTokenRow(row);
|
|
56
|
+
|
|
57
|
+
expect(parsed.kind).toBe("pat");
|
|
58
|
+
expect(parsed.tenantId).toBe("tnt_acme");
|
|
59
|
+
expect(parsed.principalId).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("round-trips a tenant-bound svc token", () => {
|
|
63
|
+
const row = makeRow({
|
|
64
|
+
kind: "svc",
|
|
65
|
+
tenantId: "tnt_acme",
|
|
66
|
+
principalId: "prn_tenant_user",
|
|
67
|
+
name: "ci-runner",
|
|
68
|
+
actions: ["createPack", "resolveRef"],
|
|
69
|
+
resource: "asset:def_skill_xyz",
|
|
70
|
+
refPattern: "refs/tags/v*",
|
|
71
|
+
});
|
|
72
|
+
const parsed = parseGitTokenRow(row);
|
|
73
|
+
|
|
74
|
+
expect(parsed.kind).toBe("svc");
|
|
75
|
+
expect(parsed.tenantId).toBe("tnt_acme");
|
|
76
|
+
expect(parsed.principalId).toBe("prn_tenant_user");
|
|
77
|
+
expect(parsed.actions).toEqual(["createPack", "resolveRef"]);
|
|
78
|
+
expect(parsed.resource).toBe("asset:def_skill_xyz");
|
|
79
|
+
expect(parsed.refPattern).toBe("refs/tags/v*");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("preserves revokedAt for soft-revoked rows", () => {
|
|
83
|
+
const revoked = new Date("2026-01-15T00:00:00Z");
|
|
84
|
+
const row = makeRow({ revokedAt: revoked });
|
|
85
|
+
const parsed = parseGitTokenRow(row);
|
|
86
|
+
|
|
87
|
+
expect(parsed.revokedAt).toBe(revoked);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("preserves expiresAt", () => {
|
|
91
|
+
const expires = new Date("2027-01-01T00:00:00Z");
|
|
92
|
+
const row = makeRow({ expiresAt: expires });
|
|
93
|
+
const parsed = parseGitTokenRow(row);
|
|
94
|
+
|
|
95
|
+
expect(parsed.expiresAt).toBe(expires);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("rejects an unknown kind", () => {
|
|
99
|
+
expect(() => GitTokenKindValidator.assert("rogue")).toThrow();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("rejects an unknown action in the actions array", () => {
|
|
103
|
+
expect(() =>
|
|
104
|
+
RepoAction.array().assert(["receivePack", "fly-the-helicopter"]),
|
|
105
|
+
).toThrow();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("accepts an empty actions array", () => {
|
|
109
|
+
const row = makeRow({ actions: [] });
|
|
110
|
+
const parsed = parseGitTokenRow(row);
|
|
111
|
+
expect(parsed.actions).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
});
|
package/src/parse-row.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { type } from "arktype";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CredentialRequirement,
|
|
5
|
+
GrantRequirement,
|
|
6
|
+
grantEffects,
|
|
7
|
+
grantOrigins,
|
|
8
|
+
sidecarStatuses,
|
|
9
|
+
} from "@intx/types";
|
|
10
|
+
import { RepoAction } from "@intx/types/sidecar";
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
agent,
|
|
14
|
+
agentVersion,
|
|
15
|
+
credential,
|
|
16
|
+
gitToken,
|
|
17
|
+
grant,
|
|
18
|
+
oauthClient,
|
|
19
|
+
offering,
|
|
20
|
+
provider,
|
|
21
|
+
tenant,
|
|
22
|
+
transaction,
|
|
23
|
+
turnPart,
|
|
24
|
+
wallet,
|
|
25
|
+
} from "./schema";
|
|
26
|
+
|
|
27
|
+
const JSONObject = type("Record<string, unknown>");
|
|
28
|
+
|
|
29
|
+
const GrantEffectValidator = type.enumerated(...grantEffects);
|
|
30
|
+
const GrantOriginValidator = type.enumerated(...grantOrigins);
|
|
31
|
+
|
|
32
|
+
const agentVersionStatuses = ["active", "inactive", "failed"] as const;
|
|
33
|
+
const AgentVersionStatusValidator = type.enumerated(...agentVersionStatuses);
|
|
34
|
+
|
|
35
|
+
const credentialTypes = [
|
|
36
|
+
"api_key",
|
|
37
|
+
"oauth_token",
|
|
38
|
+
"certificate",
|
|
39
|
+
"other",
|
|
40
|
+
] as const;
|
|
41
|
+
const CredentialTypeValidator = type.enumerated(...credentialTypes);
|
|
42
|
+
|
|
43
|
+
const credentialStatuses = ["active", "expired", "revoked", "error"] as const;
|
|
44
|
+
const CredentialStatusValidator = type.enumerated(...credentialStatuses);
|
|
45
|
+
|
|
46
|
+
const walletBackendTypes = ["crypto", "fiat", "credits"] as const;
|
|
47
|
+
const WalletBackendTypeValidator = type.enumerated(...walletBackendTypes);
|
|
48
|
+
|
|
49
|
+
const transactionDirections = ["inbound", "outbound"] as const;
|
|
50
|
+
const TransactionDirectionValidator = type.enumerated(...transactionDirections);
|
|
51
|
+
|
|
52
|
+
const transactionStatuses = ["pending", "completed", "failed"] as const;
|
|
53
|
+
const TransactionStatusValidator = type.enumerated(...transactionStatuses);
|
|
54
|
+
|
|
55
|
+
const SidecarStatusValidator = type.enumerated(...sidecarStatuses);
|
|
56
|
+
|
|
57
|
+
const gitTokenKinds = ["pat", "svc"] as const;
|
|
58
|
+
export const GitTokenKindValidator = type.enumerated(...gitTokenKinds);
|
|
59
|
+
|
|
60
|
+
const turnPartTypes = [
|
|
61
|
+
"text",
|
|
62
|
+
"reasoning",
|
|
63
|
+
"tool",
|
|
64
|
+
"file",
|
|
65
|
+
"error",
|
|
66
|
+
"refusal",
|
|
67
|
+
"step-start",
|
|
68
|
+
"step-finish",
|
|
69
|
+
"snapshot",
|
|
70
|
+
"patch",
|
|
71
|
+
] as const;
|
|
72
|
+
const TurnPartTypeValidator = type.enumerated(...turnPartTypes);
|
|
73
|
+
|
|
74
|
+
export function parseAgentRow(row: typeof agent.$inferSelect) {
|
|
75
|
+
return {
|
|
76
|
+
...row,
|
|
77
|
+
contextConfig:
|
|
78
|
+
row.contextConfig !== null ? JSONObject.assert(row.contextConfig) : null,
|
|
79
|
+
initialState:
|
|
80
|
+
row.initialState !== null ? JSONObject.assert(row.initialState) : null,
|
|
81
|
+
modelConfig:
|
|
82
|
+
row.modelConfig !== null ? JSONObject.assert(row.modelConfig) : null,
|
|
83
|
+
capabilities:
|
|
84
|
+
row.capabilities !== null ? JSONObject.assert(row.capabilities) : null,
|
|
85
|
+
credentialRequirements:
|
|
86
|
+
row.credentialRequirements !== null
|
|
87
|
+
? CredentialRequirement.array().assert(row.credentialRequirements)
|
|
88
|
+
: null,
|
|
89
|
+
grantRequirements:
|
|
90
|
+
row.grantRequirements !== null
|
|
91
|
+
? GrantRequirement.array().assert(row.grantRequirements)
|
|
92
|
+
: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function parseAgentVersionRow(row: typeof agentVersion.$inferSelect) {
|
|
97
|
+
return {
|
|
98
|
+
...row,
|
|
99
|
+
status: AgentVersionStatusValidator.assert(row.status),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function parseGrantRow(row: typeof grant.$inferSelect) {
|
|
104
|
+
return {
|
|
105
|
+
...row,
|
|
106
|
+
effect: GrantEffectValidator.assert(row.effect),
|
|
107
|
+
origin: GrantOriginValidator.assert(row.origin),
|
|
108
|
+
conditions:
|
|
109
|
+
row.conditions !== null ? JSONObject.assert(row.conditions) : null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function parseOfferingRow(row: typeof offering.$inferSelect) {
|
|
114
|
+
return {
|
|
115
|
+
...row,
|
|
116
|
+
pricing: row.pricing !== null ? JSONObject.assert(row.pricing) : null,
|
|
117
|
+
schema: row.schema !== null ? JSONObject.assert(row.schema) : null,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function parseCredentialRow(row: typeof credential.$inferSelect) {
|
|
122
|
+
return {
|
|
123
|
+
...row,
|
|
124
|
+
type: CredentialTypeValidator.assert(row.type),
|
|
125
|
+
status: CredentialStatusValidator.assert(row.status),
|
|
126
|
+
metadata: row.metadata !== null ? JSONObject.assert(row.metadata) : null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function parseProviderRow(row: typeof provider.$inferSelect) {
|
|
131
|
+
return {
|
|
132
|
+
...row,
|
|
133
|
+
metadata: row.metadata !== null ? JSONObject.assert(row.metadata) : null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function parseTenantRow(row: typeof tenant.$inferSelect) {
|
|
138
|
+
return {
|
|
139
|
+
...row,
|
|
140
|
+
config: row.config !== null ? JSONObject.assert(row.config) : null,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function parseWalletRow(row: typeof wallet.$inferSelect) {
|
|
145
|
+
return {
|
|
146
|
+
...row,
|
|
147
|
+
backendType: WalletBackendTypeValidator.assert(row.backendType),
|
|
148
|
+
config: row.config !== null ? JSONObject.assert(row.config) : null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function parseTransactionRow(row: typeof transaction.$inferSelect) {
|
|
153
|
+
return {
|
|
154
|
+
...row,
|
|
155
|
+
direction: TransactionDirectionValidator.assert(row.direction),
|
|
156
|
+
status: TransactionStatusValidator.assert(row.status),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function parseOAuthClientRow(row: typeof oauthClient.$inferSelect) {
|
|
161
|
+
return {
|
|
162
|
+
...row,
|
|
163
|
+
metadata: row.metadata !== null ? JSONObject.assert(row.metadata) : null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function parseGitTokenRow(row: typeof gitToken.$inferSelect) {
|
|
168
|
+
return {
|
|
169
|
+
...row,
|
|
170
|
+
kind: GitTokenKindValidator.assert(row.kind),
|
|
171
|
+
actions: RepoAction.array().assert(row.actions),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function parseSidecarStatus(
|
|
176
|
+
status: string,
|
|
177
|
+
): "online" | "offline" | "error" {
|
|
178
|
+
return SidecarStatusValidator.assert(status);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function parseTurnPartType(
|
|
182
|
+
partType: string,
|
|
183
|
+
): (typeof turnPart.$inferInsert)["type"] {
|
|
184
|
+
return TurnPartTypeValidator.assert(partType);
|
|
185
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
import { agent } from "./agents";
|
|
4
|
+
import { asset } from "./assets";
|
|
5
|
+
|
|
6
|
+
export const agentAsset = pgTable(
|
|
7
|
+
"agent_asset",
|
|
8
|
+
{
|
|
9
|
+
id: text("id").primaryKey(),
|
|
10
|
+
agentId: text("agent_id")
|
|
11
|
+
.notNull()
|
|
12
|
+
.references(() => agent.id, { onDelete: "cascade" }),
|
|
13
|
+
assetId: text("asset_id")
|
|
14
|
+
.notNull()
|
|
15
|
+
.references(() => asset.id, { onDelete: "cascade" }),
|
|
16
|
+
ref: text("ref").notNull(),
|
|
17
|
+
accessMode: text("access_mode").notNull().default("read-only"),
|
|
18
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
19
|
+
},
|
|
20
|
+
(t) => [unique("agent_asset_agent_asset").on(t.agentId, t.assetId)],
|
|
21
|
+
);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { jsonb, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
import { principal } from "./principals";
|
|
4
|
+
import { tenant } from "./tenants";
|
|
5
|
+
|
|
6
|
+
export const agent = pgTable("agent", {
|
|
7
|
+
id: text("id").primaryKey(),
|
|
8
|
+
tenantId: text("tenant_id")
|
|
9
|
+
.notNull()
|
|
10
|
+
.references(() => tenant.id, { onDelete: "cascade" }),
|
|
11
|
+
// Tracks the definition author's principal for resolving source:"creator"
|
|
12
|
+
// grant requirements at launch. See AUTH.md § Grant Requirements on Definitions.
|
|
13
|
+
creatorPrincipalId: text("creator_principal_id")
|
|
14
|
+
.notNull()
|
|
15
|
+
.references(() => principal.id),
|
|
16
|
+
name: text("name").notNull(),
|
|
17
|
+
description: text("description"),
|
|
18
|
+
systemPrompt: text("system_prompt"),
|
|
19
|
+
contextConfig: jsonb("context_config"),
|
|
20
|
+
initialState: jsonb("initial_state"),
|
|
21
|
+
modelConfig: jsonb("model_config"),
|
|
22
|
+
capabilities: jsonb("capabilities"),
|
|
23
|
+
credentialRequirements: jsonb("credential_requirements"),
|
|
24
|
+
// Grant requirements manifest — resolved at launch into materialized grants
|
|
25
|
+
// on the instance principal. See AUTH.md § Grant Requirements on Definitions.
|
|
26
|
+
grantRequirements: jsonb("grant_requirements"),
|
|
27
|
+
currentVersion: text("current_version").notNull().default("1"),
|
|
28
|
+
status: text("status", {
|
|
29
|
+
enum: ["deployed", "stopped"],
|
|
30
|
+
})
|
|
31
|
+
.notNull()
|
|
32
|
+
.default("deployed"),
|
|
33
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
34
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const agentVersion = pgTable("agent_version", {
|
|
38
|
+
id: text("id").primaryKey(),
|
|
39
|
+
agentId: text("agent_id")
|
|
40
|
+
.notNull()
|
|
41
|
+
.references(() => agent.id, { onDelete: "cascade" }),
|
|
42
|
+
version: text("version").notNull(),
|
|
43
|
+
status: text("status", {
|
|
44
|
+
enum: ["active", "inactive", "failed"],
|
|
45
|
+
})
|
|
46
|
+
.notNull()
|
|
47
|
+
.default("active"),
|
|
48
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
49
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, unique } from "drizzle-orm/pg-core";
|
|
2
|
+
|
|
3
|
+
import { principal } from "./principals";
|
|
4
|
+
import { tenant } from "./tenants";
|
|
5
|
+
|
|
6
|
+
export const asset = pgTable(
|
|
7
|
+
"asset",
|
|
8
|
+
{
|
|
9
|
+
id: text("id").primaryKey(),
|
|
10
|
+
tenantId: text("tenant_id")
|
|
11
|
+
.notNull()
|
|
12
|
+
.references(() => tenant.id, { onDelete: "cascade" }),
|
|
13
|
+
kind: text("kind").notNull(),
|
|
14
|
+
name: text("name").notNull(),
|
|
15
|
+
displayName: text("display_name"),
|
|
16
|
+
creatorPrincipalId: text("creator_principal_id").references(
|
|
17
|
+
() => principal.id,
|
|
18
|
+
{ onDelete: "set null" },
|
|
19
|
+
),
|
|
20
|
+
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
21
|
+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
|
|
22
|
+
},
|
|
23
|
+
(t) => [unique("asset_tenant_kind_name").on(t.tenantId, t.kind, t.name)],
|
|
24
|
+
);
|