@openparachute/vault 0.1.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/.claude/settings.local.json +31 -0
- package/.dockerignore +8 -0
- package/.env.example +9 -0
- package/.playwright-mcp/console-2026-04-14T04-17-25-395Z.log +2 -0
- package/.playwright-mcp/console-2026-04-14T04-18-11-767Z.log +1 -0
- package/.playwright-mcp/console-2026-04-14T04-19-07-733Z.log +2 -0
- package/.playwright-mcp/console-2026-04-14T04-20-45-440Z.log +2 -0
- package/.playwright-mcp/page-2026-04-14T04-17-25-536Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-14T04-18-11-816Z.yml +1 -0
- package/.playwright-mcp/page-2026-04-14T04-18-31-674Z.yml +211 -0
- package/.playwright-mcp/page-2026-04-14T04-19-07-795Z.yml +59 -0
- package/.playwright-mcp/page-2026-04-14T04-19-36-239Z.yml +232 -0
- package/.playwright-mcp/page-2026-04-14T04-19-58-327Z.yml +182 -0
- package/.playwright-mcp/page-2026-04-14T04-20-10-517Z.yml +91 -0
- package/.playwright-mcp/page-2026-04-14T04-20-14-796Z.yml +70 -0
- package/.playwright-mcp/page-2026-04-14T04-20-45-509Z.yml +59 -0
- package/CLAUDE.md +115 -0
- package/Caddyfile +3 -0
- package/Dockerfile +22 -0
- package/LICENSE +661 -0
- package/README.md +356 -0
- package/bun.lock +219 -0
- package/bunfig.toml +2 -0
- package/core/package.json +7 -0
- package/core/src/core.test.ts +940 -0
- package/core/src/hooks.test.ts +361 -0
- package/core/src/hooks.ts +234 -0
- package/core/src/links.ts +352 -0
- package/core/src/mcp.ts +672 -0
- package/core/src/notes.ts +520 -0
- package/core/src/obsidian.test.ts +380 -0
- package/core/src/obsidian.ts +322 -0
- package/core/src/paths.test.ts +197 -0
- package/core/src/paths.ts +53 -0
- package/core/src/schema.ts +331 -0
- package/core/src/store.ts +303 -0
- package/core/src/tag-schemas.ts +104 -0
- package/core/src/test-preload.ts +8 -0
- package/core/src/types.ts +140 -0
- package/core/src/wikilinks.test.ts +277 -0
- package/core/src/wikilinks.ts +402 -0
- package/deploy/parachute-vault.service +20 -0
- package/docker-compose.yml +50 -0
- package/docs/HTTP_API.md +328 -0
- package/fly.toml +24 -0
- package/package.json +32 -0
- package/railway.json +14 -0
- package/religions-abrahamic-filter.png +0 -0
- package/religions-buddhism-v2.png +0 -0
- package/religions-buddhism.png +0 -0
- package/religions-final.png +0 -0
- package/religions-v1.png +0 -0
- package/religions-v2.png +0 -0
- package/religions-zen.png +0 -0
- package/scripts/migrate-audio-to-opus.test.ts +237 -0
- package/scripts/migrate-audio-to-opus.ts +499 -0
- package/src/auth.ts +170 -0
- package/src/cli.ts +1131 -0
- package/src/config-triggers.test.ts +83 -0
- package/src/config.test.ts +125 -0
- package/src/config.ts +716 -0
- package/src/db.ts +14 -0
- package/src/launchd.ts +109 -0
- package/src/mcp-http.ts +113 -0
- package/src/mcp-tools.ts +155 -0
- package/src/oauth.test.ts +1242 -0
- package/src/oauth.ts +729 -0
- package/src/owner-auth.ts +159 -0
- package/src/prompt.ts +141 -0
- package/src/published.test.ts +214 -0
- package/src/qrcode-terminal.d.ts +9 -0
- package/src/routes.ts +822 -0
- package/src/server.ts +450 -0
- package/src/systemd.ts +84 -0
- package/src/token-store.test.ts +174 -0
- package/src/token-store.ts +241 -0
- package/src/triggers.test.ts +397 -0
- package/src/triggers.ts +412 -0
- package/src/two-factor.test.ts +246 -0
- package/src/two-factor.ts +222 -0
- package/src/vault-store.ts +47 -0
- package/src/vault.test.ts +1309 -0
- package/tsconfig.json +29 -0
- package/web/README.md +73 -0
- package/web/bun.lock +827 -0
- package/web/eslint.config.js +23 -0
- package/web/index.html +15 -0
- package/web/package.json +36 -0
- package/web/public/favicon.svg +1 -0
- package/web/public/icons.svg +24 -0
- package/web/src/App.tsx +149 -0
- package/web/src/Graph.tsx +200 -0
- package/web/src/NoteView.tsx +155 -0
- package/web/src/Sidebar.tsx +186 -0
- package/web/src/api.ts +21 -0
- package/web/src/index.css +50 -0
- package/web/src/main.tsx +10 -0
- package/web/src/types.ts +37 -0
- package/web/src/utils.ts +107 -0
- package/web/tsconfig.app.json +25 -0
- package/web/tsconfig.json +7 -0
- package/web/tsconfig.node.json +24 -0
- package/web/vite.config.ts +15 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the token store — scoped tokens with permissions.
|
|
3
|
+
* Tokens now live inside each vault's SQLite database (schema v7).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
7
|
+
import { Database } from "bun:sqlite";
|
|
8
|
+
import { initSchema } from "../core/src/schema.ts";
|
|
9
|
+
import {
|
|
10
|
+
generateToken,
|
|
11
|
+
createToken,
|
|
12
|
+
resolveToken,
|
|
13
|
+
listTokens,
|
|
14
|
+
revokeToken,
|
|
15
|
+
} from "./token-store.ts";
|
|
16
|
+
|
|
17
|
+
let db: Database;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
db = new Database(":memory:");
|
|
21
|
+
initSchema(db);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
db.close();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("token CRUD", () => {
|
|
29
|
+
test("create and resolve a full-access token", () => {
|
|
30
|
+
const { fullToken } = generateToken();
|
|
31
|
+
createToken(db, fullToken, { label: "test-token", permission: "full" });
|
|
32
|
+
|
|
33
|
+
const resolved = resolveToken(db, fullToken);
|
|
34
|
+
expect(resolved).not.toBeNull();
|
|
35
|
+
expect(resolved!.permission).toBe("full");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("token with read permission", () => {
|
|
39
|
+
const { fullToken } = generateToken();
|
|
40
|
+
createToken(db, fullToken, {
|
|
41
|
+
label: "reader",
|
|
42
|
+
permission: "read",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const resolved = resolveToken(db, fullToken);
|
|
46
|
+
expect(resolved!.permission).toBe("read");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("default permission is full", () => {
|
|
50
|
+
const { fullToken } = generateToken();
|
|
51
|
+
createToken(db, fullToken, { label: "default-perm" });
|
|
52
|
+
|
|
53
|
+
const resolved = resolveToken(db, fullToken);
|
|
54
|
+
expect(resolved!.permission).toBe("full");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("legacy admin permission normalizes to full", () => {
|
|
58
|
+
const { fullToken } = generateToken();
|
|
59
|
+
// Simulate a legacy token by writing "admin" directly to DB
|
|
60
|
+
const hash = require("./config.ts").hashKey(fullToken);
|
|
61
|
+
db.prepare("INSERT INTO tokens (token_hash, label, permission, created_at) VALUES (?, ?, ?, ?)")
|
|
62
|
+
.run(hash, "legacy-admin", "admin", new Date().toISOString());
|
|
63
|
+
|
|
64
|
+
const resolved = resolveToken(db, fullToken);
|
|
65
|
+
expect(resolved!.permission).toBe("full");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("legacy write permission normalizes to full", () => {
|
|
69
|
+
const { fullToken } = generateToken();
|
|
70
|
+
const hash = require("./config.ts").hashKey(fullToken);
|
|
71
|
+
db.prepare("INSERT INTO tokens (token_hash, label, permission, created_at) VALUES (?, ?, ?, ?)")
|
|
72
|
+
.run(hash, "legacy-write", "write", new Date().toISOString());
|
|
73
|
+
|
|
74
|
+
const resolved = resolveToken(db, fullToken);
|
|
75
|
+
expect(resolved!.permission).toBe("full");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("expired token is rejected", () => {
|
|
79
|
+
const { fullToken } = generateToken();
|
|
80
|
+
createToken(db, fullToken, {
|
|
81
|
+
label: "expired",
|
|
82
|
+
permission: "full",
|
|
83
|
+
expires_at: "2020-01-01T00:00:00.000Z", // in the past
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const resolved = resolveToken(db, fullToken);
|
|
87
|
+
expect(resolved).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("non-expired token is accepted", () => {
|
|
91
|
+
const { fullToken } = generateToken();
|
|
92
|
+
const future = new Date(Date.now() + 86400000).toISOString(); // +1 day
|
|
93
|
+
createToken(db, fullToken, {
|
|
94
|
+
label: "valid",
|
|
95
|
+
permission: "read",
|
|
96
|
+
expires_at: future,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const resolved = resolveToken(db, fullToken);
|
|
100
|
+
expect(resolved).not.toBeNull();
|
|
101
|
+
expect(resolved!.permission).toBe("read");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("invalid token returns null", () => {
|
|
105
|
+
const resolved = resolveToken(db, "pvt_does_not_exist");
|
|
106
|
+
expect(resolved).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("list tokens shows all tokens", () => {
|
|
110
|
+
const { fullToken: t1 } = generateToken();
|
|
111
|
+
const { fullToken: t2 } = generateToken();
|
|
112
|
+
createToken(db, t1, { label: "first", permission: "full" });
|
|
113
|
+
createToken(db, t2, { label: "second", permission: "read" });
|
|
114
|
+
|
|
115
|
+
const tokens = listTokens(db);
|
|
116
|
+
expect(tokens.length).toBe(2);
|
|
117
|
+
expect(tokens.some((t) => t.label === "first")).toBe(true);
|
|
118
|
+
expect(tokens.some((t) => t.label === "second")).toBe(true);
|
|
119
|
+
// Each token should have a display ID
|
|
120
|
+
expect(tokens.every((t) => t.id.startsWith("t_"))).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("revoke token by display ID", () => {
|
|
124
|
+
const { fullToken } = generateToken();
|
|
125
|
+
createToken(db, fullToken, { label: "to-revoke" });
|
|
126
|
+
|
|
127
|
+
const tokens = listTokens(db);
|
|
128
|
+
expect(tokens.length).toBe(1);
|
|
129
|
+
|
|
130
|
+
const revoked = revokeToken(db, tokens[0].id);
|
|
131
|
+
expect(revoked).toBe(true);
|
|
132
|
+
|
|
133
|
+
const after = listTokens(db);
|
|
134
|
+
expect(after.length).toBe(0);
|
|
135
|
+
|
|
136
|
+
// Token should no longer resolve
|
|
137
|
+
expect(resolveToken(db, fullToken)).toBeNull();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("revoke non-existent token returns false", () => {
|
|
141
|
+
expect(revokeToken(db, "t_doesnotexist")).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("resolve updates last_used_at", () => {
|
|
145
|
+
const { fullToken } = generateToken();
|
|
146
|
+
createToken(db, fullToken, { label: "usage-tracking" });
|
|
147
|
+
|
|
148
|
+
// Before first use
|
|
149
|
+
const before = listTokens(db);
|
|
150
|
+
expect(before[0].last_used_at).toBeNull();
|
|
151
|
+
|
|
152
|
+
// Resolve (which should update last_used_at)
|
|
153
|
+
resolveToken(db, fullToken);
|
|
154
|
+
|
|
155
|
+
const after = listTokens(db);
|
|
156
|
+
expect(after[0].last_used_at).not.toBeNull();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("token generation", () => {
|
|
161
|
+
test("generated tokens have pvt_ prefix", () => {
|
|
162
|
+
const { fullToken, tokenHash } = generateToken();
|
|
163
|
+
expect(fullToken.startsWith("pvt_")).toBe(true);
|
|
164
|
+
expect(tokenHash.startsWith("sha256:")).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("generated tokens are unique", () => {
|
|
168
|
+
const t1 = generateToken();
|
|
169
|
+
const t2 = generateToken();
|
|
170
|
+
expect(t1.fullToken).not.toBe(t2.fullToken);
|
|
171
|
+
expect(t1.tokenHash).not.toBe(t2.tokenHash);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token operations for per-vault token management.
|
|
3
|
+
*
|
|
4
|
+
* Tokens live in each vault's SQLite database (the `tokens` table is part of
|
|
5
|
+
* the vault schema as of v7). All functions take a Database parameter — the
|
|
6
|
+
* vault's own DB connection.
|
|
7
|
+
*
|
|
8
|
+
* Two permission levels:
|
|
9
|
+
* - "full" — unrestricted access (CRUD, delete, token management)
|
|
10
|
+
* - "read" — query-only (no mutations)
|
|
11
|
+
*
|
|
12
|
+
* Legacy "admin" and "write" values in the DB are normalized to "full" at
|
|
13
|
+
* read time for backward compatibility.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Database } from "bun:sqlite";
|
|
17
|
+
import crypto from "node:crypto";
|
|
18
|
+
import { hashKey } from "./config.ts";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Types
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export type TokenPermission = "full" | "read";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize legacy permission values ("admin", "write") to the current
|
|
28
|
+
* two-tier model. Existing DB rows may contain the old values.
|
|
29
|
+
*/
|
|
30
|
+
export function normalizePermission(p: string): TokenPermission {
|
|
31
|
+
if (p === "read") return "read";
|
|
32
|
+
return "full"; // "admin", "write", or anything else → full
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Token {
|
|
36
|
+
token_hash: string;
|
|
37
|
+
label: string;
|
|
38
|
+
permission: TokenPermission;
|
|
39
|
+
/** @deprecated Scope columns exist in DB but are not enforced at runtime. */
|
|
40
|
+
scope_tag: string | null;
|
|
41
|
+
/** @deprecated Scope columns exist in DB but are not enforced at runtime. */
|
|
42
|
+
scope_path_prefix: string | null;
|
|
43
|
+
expires_at: string | null;
|
|
44
|
+
created_at: string;
|
|
45
|
+
last_used_at: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface ResolvedToken {
|
|
49
|
+
permission: TokenPermission;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Token operations
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
export function generateToken(): { fullToken: string; tokenHash: string } {
|
|
57
|
+
const random = crypto.randomBytes(32).toString("base64url").slice(0, 32);
|
|
58
|
+
const fullToken = `pvt_${random}`;
|
|
59
|
+
return { fullToken, tokenHash: hashKey(fullToken) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createToken(
|
|
63
|
+
db: Database,
|
|
64
|
+
fullToken: string,
|
|
65
|
+
opts: {
|
|
66
|
+
label: string;
|
|
67
|
+
permission?: TokenPermission;
|
|
68
|
+
/** @deprecated Written to DB but not enforced at runtime. */
|
|
69
|
+
scope_tag?: string | null;
|
|
70
|
+
/** @deprecated Written to DB but not enforced at runtime. */
|
|
71
|
+
scope_path_prefix?: string | null;
|
|
72
|
+
expires_at?: string | null;
|
|
73
|
+
},
|
|
74
|
+
): Token {
|
|
75
|
+
const tokenHash = hashKey(fullToken);
|
|
76
|
+
const now = new Date().toISOString();
|
|
77
|
+
const permission = opts.permission ?? "full";
|
|
78
|
+
|
|
79
|
+
db.prepare(`
|
|
80
|
+
INSERT INTO tokens (token_hash, label, permission, scope_tag, scope_path_prefix, expires_at, created_at)
|
|
81
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
82
|
+
`).run(
|
|
83
|
+
tokenHash,
|
|
84
|
+
opts.label,
|
|
85
|
+
permission,
|
|
86
|
+
opts.scope_tag ?? null,
|
|
87
|
+
opts.scope_path_prefix ?? null,
|
|
88
|
+
opts.expires_at ?? null,
|
|
89
|
+
now,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
token_hash: tokenHash,
|
|
94
|
+
label: opts.label,
|
|
95
|
+
permission,
|
|
96
|
+
scope_tag: opts.scope_tag ?? null,
|
|
97
|
+
scope_path_prefix: opts.scope_path_prefix ?? null,
|
|
98
|
+
expires_at: opts.expires_at ?? null,
|
|
99
|
+
created_at: now,
|
|
100
|
+
last_used_at: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a bearer token. Returns the token info if valid, null if not found or expired.
|
|
106
|
+
* Updates last_used_at on successful resolution.
|
|
107
|
+
*/
|
|
108
|
+
export function resolveToken(db: Database, providedToken: string): ResolvedToken | null {
|
|
109
|
+
// Hash-then-lookup: the SQL = comparison on SHA-256 output is not timing-safe,
|
|
110
|
+
// but this is acceptable — the attacker would need to guess a valid SHA-256
|
|
111
|
+
// preimage, which is computationally infeasible regardless of timing leaks.
|
|
112
|
+
const candidateHash = hashKey(providedToken);
|
|
113
|
+
|
|
114
|
+
const row = db.prepare(`
|
|
115
|
+
SELECT token_hash, permission, expires_at
|
|
116
|
+
FROM tokens WHERE token_hash = ?
|
|
117
|
+
`).get(candidateHash) as {
|
|
118
|
+
token_hash: string;
|
|
119
|
+
permission: string;
|
|
120
|
+
expires_at: string | null;
|
|
121
|
+
} | null;
|
|
122
|
+
|
|
123
|
+
if (!row) return null;
|
|
124
|
+
|
|
125
|
+
// Check expiry
|
|
126
|
+
if (row.expires_at && new Date(row.expires_at) < new Date()) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Update last_used_at
|
|
131
|
+
db.prepare("UPDATE tokens SET last_used_at = ? WHERE token_hash = ?")
|
|
132
|
+
.run(new Date().toISOString(), row.token_hash);
|
|
133
|
+
|
|
134
|
+
return { permission: normalizePermission(row.permission) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* List all tokens (for CLI display). Never exposes the hash directly —
|
|
139
|
+
* shows a truncated prefix for identification.
|
|
140
|
+
*/
|
|
141
|
+
export function listTokens(db: Database): (Token & { id: string })[] {
|
|
142
|
+
const rows = db.prepare(`
|
|
143
|
+
SELECT token_hash, label, permission, scope_tag, scope_path_prefix,
|
|
144
|
+
expires_at, created_at, last_used_at
|
|
145
|
+
FROM tokens ORDER BY created_at DESC
|
|
146
|
+
`).all() as Token[];
|
|
147
|
+
|
|
148
|
+
return rows.map((r) => ({
|
|
149
|
+
...r,
|
|
150
|
+
permission: normalizePermission(r.permission),
|
|
151
|
+
// Derive a short display ID from the hash (first 12 chars after "sha256:")
|
|
152
|
+
id: `t_${r.token_hash.slice(7, 19)}`,
|
|
153
|
+
}));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Revoke (delete) a token by its display ID or full hash.
|
|
158
|
+
* Returns true if exactly one token was deleted.
|
|
159
|
+
* If a display ID prefix matches multiple tokens, returns false (ambiguous).
|
|
160
|
+
*/
|
|
161
|
+
export function revokeToken(db: Database, idOrHash: string): boolean {
|
|
162
|
+
// Try matching by display ID prefix
|
|
163
|
+
if (idOrHash.startsWith("t_")) {
|
|
164
|
+
const hashPrefix = idOrHash.slice(2);
|
|
165
|
+
const rows = db.prepare(
|
|
166
|
+
"SELECT token_hash FROM tokens WHERE token_hash LIKE ?"
|
|
167
|
+
).all(`sha256:${hashPrefix}%`) as { token_hash: string }[];
|
|
168
|
+
if (rows.length === 1) {
|
|
169
|
+
db.prepare("DELETE FROM tokens WHERE token_hash = ?").run(rows[0].token_hash);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
if (rows.length > 1) {
|
|
173
|
+
// Ambiguous prefix — refuse to revoke
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Try matching by full hash
|
|
179
|
+
const result = db.prepare("DELETE FROM tokens WHERE token_hash = ?").run(idOrHash);
|
|
180
|
+
return result.changes > 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Migration: import existing API keys from config.yaml into a vault's DB
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Import existing API keys for a specific vault from config.yaml into its DB.
|
|
189
|
+
* Idempotent — skips keys whose hash already exists.
|
|
190
|
+
*
|
|
191
|
+
* Imports:
|
|
192
|
+
* - Per-vault keys from vault.yaml (direct match)
|
|
193
|
+
* - Global keys from config.yaml (they become full-access tokens in every vault)
|
|
194
|
+
*/
|
|
195
|
+
export function migrateVaultKeys(
|
|
196
|
+
db: Database,
|
|
197
|
+
vaultKeys: { key_hash: string; label: string; scope?: string; created_at: string; last_used_at?: string }[],
|
|
198
|
+
globalKeys?: { key_hash: string; label: string; scope?: string; created_at: string; last_used_at?: string }[],
|
|
199
|
+
): number {
|
|
200
|
+
let migrated = 0;
|
|
201
|
+
|
|
202
|
+
// Import per-vault keys
|
|
203
|
+
for (const key of vaultKeys) {
|
|
204
|
+
const exists = db.prepare("SELECT 1 FROM tokens WHERE token_hash = ?").get(key.key_hash);
|
|
205
|
+
if (!exists) {
|
|
206
|
+
db.prepare(`
|
|
207
|
+
INSERT INTO tokens (token_hash, label, permission, created_at, last_used_at)
|
|
208
|
+
VALUES (?, ?, ?, ?, ?)
|
|
209
|
+
`).run(
|
|
210
|
+
key.key_hash,
|
|
211
|
+
key.label,
|
|
212
|
+
key.scope === "read" ? "read" : "full",
|
|
213
|
+
key.created_at,
|
|
214
|
+
key.last_used_at ?? null,
|
|
215
|
+
);
|
|
216
|
+
migrated++;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Import global keys as full-access tokens
|
|
221
|
+
if (globalKeys) {
|
|
222
|
+
for (const key of globalKeys) {
|
|
223
|
+
const exists = db.prepare("SELECT 1 FROM tokens WHERE token_hash = ?").get(key.key_hash);
|
|
224
|
+
if (!exists) {
|
|
225
|
+
db.prepare(`
|
|
226
|
+
INSERT INTO tokens (token_hash, label, permission, created_at, last_used_at)
|
|
227
|
+
VALUES (?, ?, ?, ?, ?)
|
|
228
|
+
`).run(
|
|
229
|
+
key.key_hash,
|
|
230
|
+
key.label,
|
|
231
|
+
"full",
|
|
232
|
+
key.created_at,
|
|
233
|
+
key.last_used_at ?? null,
|
|
234
|
+
);
|
|
235
|
+
migrated++;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return migrated;
|
|
241
|
+
}
|