@minion-stack/db 0.3.1 → 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.
@@ -0,0 +1,19 @@
1
+ /** Seal plaintext → { ciphertext, iv }. ciphertext = hex(encrypted || authTag). */
2
+ export declare function sealSecret(plaintext: string): {
3
+ ciphertext: string;
4
+ iv: string;
5
+ };
6
+ /** Open hex(encrypted || authTag) + hex(iv) → plaintext. Throws on auth failure. */
7
+ export declare function openSecret(ciphertext: string, iv: string): string;
8
+ /** Alias of {@link sealSecret}. */
9
+ export declare const encrypt: typeof sealSecret;
10
+ /** Alias of {@link openSecret}. */
11
+ export declare const decrypt: typeof openSecret;
12
+ /** Seal a token → { encrypted, iv } (hub's field name for the ciphertext). */
13
+ export declare function encryptToken(token: string): {
14
+ encrypted: string;
15
+ iv: string;
16
+ };
17
+ /** Open a sealed token. */
18
+ export declare function decryptToken(encrypted: string, iv: string): string;
19
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAiCA,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAOhF;AAED,oFAAoF;AACpF,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAOjE;AAMD,mCAAmC;AACnC,eAAO,MAAM,OAAO,mBAAa,CAAC;AAElC,mCAAmC;AACnC,eAAO,MAAM,OAAO,mBAAa,CAAC;AAElC,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAG7E;AAED,2BAA2B;AAC3B,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAElE"}
package/dist/crypto.js ADDED
@@ -0,0 +1,65 @@
1
+ // Canonical app-level secret encryption for the Minion stack (R7 of
2
+ // specs/2026-05-26-auth-token-simplification.md). AES-256-GCM, dialect-agnostic
3
+ // — consumed by the PG identity path (sealSecret/openSecret) and re-exported by
4
+ // minion_hub's crypto.ts (encrypt/decrypt/encryptToken/decryptToken) so there is
5
+ // ONE implementation and one key-derivation path instead of byte-matched copies.
6
+ //
7
+ // Layout (MUST stay stable — existing ciphertext at rest depends on it):
8
+ // key = scryptSync(ENCRYPTION_KEY, 'minion-hub-salt', 32)
9
+ // ciphertext = hex(encrypted || authTag) (16-byte GCM tag LAST)
10
+ // iv = hex(12 random bytes), stored separately
11
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
12
+ const ALGORITHM = "aes-256-gcm";
13
+ const IV_BYTES = 12;
14
+ const AUTH_TAG_BYTES = 16;
15
+ let cachedKey = null;
16
+ function key() {
17
+ if (cachedKey)
18
+ return cachedKey;
19
+ const raw = process.env.ENCRYPTION_KEY;
20
+ if (!raw) {
21
+ if (process.env.NODE_ENV === "production") {
22
+ throw new Error("ENCRYPTION_KEY environment variable must be set in production");
23
+ }
24
+ // Dev-only fallback — never used in production.
25
+ cachedKey = scryptSync("minion-hub-dev-key", "minion-hub-salt", 32);
26
+ return cachedKey;
27
+ }
28
+ cachedKey = scryptSync(raw, "minion-hub-salt", 32);
29
+ return cachedKey;
30
+ }
31
+ /** Seal plaintext → { ciphertext, iv }. ciphertext = hex(encrypted || authTag). */
32
+ export function sealSecret(plaintext) {
33
+ const iv = randomBytes(IV_BYTES);
34
+ const cipher = createCipheriv(ALGORITHM, key(), iv);
35
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
36
+ const authTag = cipher.getAuthTag();
37
+ const combined = Buffer.concat([encrypted, authTag]);
38
+ return { ciphertext: combined.toString("hex"), iv: iv.toString("hex") };
39
+ }
40
+ /** Open hex(encrypted || authTag) + hex(iv) → plaintext. Throws on auth failure. */
41
+ export function openSecret(ciphertext, iv) {
42
+ const combined = Buffer.from(ciphertext, "hex");
43
+ const encrypted = combined.subarray(0, combined.length - AUTH_TAG_BYTES);
44
+ const authTag = combined.subarray(combined.length - AUTH_TAG_BYTES);
45
+ const decipher = createDecipheriv(ALGORITHM, key(), Buffer.from(iv, "hex"));
46
+ decipher.setAuthTag(authTag);
47
+ return decipher.update(encrypted) + decipher.final("utf8");
48
+ }
49
+ // --- minion_hub-compatible aliases -------------------------------------------
50
+ // Hub's crypto.ts historically exported these names; keeping them lets hub become
51
+ // a thin re-export of this module without touching its many call sites.
52
+ /** Alias of {@link sealSecret}. */
53
+ export const encrypt = sealSecret;
54
+ /** Alias of {@link openSecret}. */
55
+ export const decrypt = openSecret;
56
+ /** Seal a token → { encrypted, iv } (hub's field name for the ciphertext). */
57
+ export function encryptToken(token) {
58
+ const { ciphertext, iv } = sealSecret(token);
59
+ return { encrypted: ciphertext, iv };
60
+ }
61
+ /** Open a sealed token. */
62
+ export function decryptToken(encrypted, iv) {
63
+ return openSecret(encrypted, iv);
64
+ }
65
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=crypto.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.test.d.ts","sourceRoot":"","sources":["../src/crypto.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { sealSecret, openSecret, encrypt, decrypt, encryptToken, decryptToken, } from "./crypto.js";
3
+ describe("canonical crypto", () => {
4
+ it("seal/open roundtrips", () => {
5
+ const { ciphertext, iv } = sealSecret("hunter2");
6
+ expect(ciphertext).not.toContain("hunter2");
7
+ expect(openSecret(ciphertext, iv)).toBe("hunter2");
8
+ });
9
+ it("encrypt/decrypt are aliases of seal/open", () => {
10
+ const { ciphertext, iv } = encrypt("s3cret");
11
+ expect(decrypt(ciphertext, iv)).toBe("s3cret");
12
+ });
13
+ it("encryptToken returns { encrypted, iv } and decryptToken roundtrips", () => {
14
+ const { encrypted, iv } = encryptToken("tok-abc");
15
+ expect(decryptToken(encrypted, iv)).toBe("tok-abc");
16
+ });
17
+ it("openSecret throws on a tampered ciphertext (GCM auth)", () => {
18
+ const { ciphertext, iv } = sealSecret("x");
19
+ const tampered = (ciphertext[0] === "a" ? "b" : "a") + ciphertext.slice(1);
20
+ expect(() => openSecret(tampered, iv)).toThrow();
21
+ });
22
+ });
23
+ //# sourceMappingURL=crypto.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.test.js","sourceRoot":"","sources":["../src/crypto.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,UAAU,EACV,UAAU,EACV,OAAO,EACP,OAAO,EACP,YAAY,EACZ,YAAY,GACb,MAAM,aAAa,CAAC;AAErB,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;QACjD,MAAM,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAC5C,MAAM,CAAC,UAAU,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC7C,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oEAAoE,EAAE,GAAG,EAAE;QAC5E,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;QAClD,MAAM,CAAC,YAAY,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,EAAE,UAAU,EAAE,EAAE,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,MAAM,QAAQ,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC3E,MAAM,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;IACnD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -1,8 +1,2 @@
1
- /** Seal plaintext → { ciphertext, iv }. ciphertext = hex(encrypted || authTag). */
2
- export declare function sealSecret(plaintext: string): {
3
- ciphertext: string;
4
- iv: string;
5
- };
6
- /** Open hex(encrypted || authTag) + hex(iv) → plaintext. Throws on auth failure. */
7
- export declare function openSecret(ciphertext: string, iv: string): string;
1
+ export { sealSecret, openSecret } from "../crypto.js";
8
2
  //# sourceMappingURL=crypto.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/pg/crypto.ts"],"names":[],"mappings":"AAyBA,mFAAmF;AACnF,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAOhF;AAED,oFAAoF;AACpF,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAOjE"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/pg/crypto.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
package/dist/pg/crypto.js CHANGED
@@ -1,42 +1,5 @@
1
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
2
- const ALGORITHM = 'aes-256-gcm';
3
- const IV_BYTES = 12;
4
- const AUTH_TAG_BYTES = 16;
5
- // MUST match minion_hub/src/server/auth/crypto.ts so hub + site interoperate:
6
- // key = scryptSync(ENCRYPTION_KEY, 'minion-hub-salt', 32)
7
- // ciphertext (hex) = encrypted || authTag (16-byte tag LAST)
8
- // iv (hex) = 12 random bytes, stored separately
9
- let cachedKey = null;
10
- function key() {
11
- if (cachedKey)
12
- return cachedKey;
13
- const raw = process.env.ENCRYPTION_KEY;
14
- if (!raw) {
15
- if (process.env.NODE_ENV === 'production') {
16
- throw new Error('ENCRYPTION_KEY environment variable must be set in production');
17
- }
18
- cachedKey = scryptSync('minion-hub-dev-key', 'minion-hub-salt', 32);
19
- return cachedKey;
20
- }
21
- cachedKey = scryptSync(raw, 'minion-hub-salt', 32);
22
- return cachedKey;
23
- }
24
- /** Seal plaintext → { ciphertext, iv }. ciphertext = hex(encrypted || authTag). */
25
- export function sealSecret(plaintext) {
26
- const iv = randomBytes(IV_BYTES);
27
- const cipher = createCipheriv(ALGORITHM, key(), iv);
28
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
29
- const authTag = cipher.getAuthTag();
30
- const combined = Buffer.concat([encrypted, authTag]);
31
- return { ciphertext: combined.toString('hex'), iv: iv.toString('hex') };
32
- }
33
- /** Open hex(encrypted || authTag) + hex(iv) → plaintext. Throws on auth failure. */
34
- export function openSecret(ciphertext, iv) {
35
- const combined = Buffer.from(ciphertext, 'hex');
36
- const encrypted = combined.subarray(0, combined.length - AUTH_TAG_BYTES);
37
- const authTag = combined.subarray(combined.length - AUTH_TAG_BYTES);
38
- const decipher = createDecipheriv(ALGORITHM, key(), Buffer.from(iv, 'hex'));
39
- decipher.setAuthTag(authTag);
40
- return decipher.update(encrypted) + decipher.final('utf8');
41
- }
1
+ // Re-export of the canonical crypto module (../crypto.ts). Kept as a stable
2
+ // subpath for existing importers of the PG identity path; the implementation
3
+ // lives in one place now (R7 of specs/2026-05-26-auth-token-simplification.md).
4
+ export { sealSecret, openSecret } from "../crypto.js";
42
5
  //# sourceMappingURL=crypto.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/pg/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAExF,MAAM,SAAS,GAAG,aAAa,CAAC;AAChC,MAAM,QAAQ,GAAG,EAAE,CAAC;AACpB,MAAM,cAAc,GAAG,EAAE,CAAC;AAE1B,8EAA8E;AAC9E,4DAA4D;AAC5D,iEAAiE;AACjE,kDAAkD;AAClD,IAAI,SAAS,GAAkB,IAAI,CAAC;AACpC,SAAS,GAAG;IACV,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAChC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;IACvC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;YAC1C,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;QACnF,CAAC;QACD,SAAS,GAAG,UAAU,CAAC,oBAAoB,EAAE,iBAAiB,EAAE,EAAE,CAAC,CAAC;QACpE,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,iBAAiB,EAAE,EAAE,CAAC,CAAC;IACnD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,UAAU,CAAC,SAAiB;IAC1C,MAAM,EAAE,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACjC,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;IACpD,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACpF,MAAM,OAAO,GAAG,MAAM,CAAC,UAAU,EAAE,CAAC;IACpC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AAC1E,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,UAAU,CAAC,UAAkB,EAAE,EAAU;IACvD,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;IACzE,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,cAAc,CAAC,CAAC;IACpE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,SAAS,EAAE,GAAG,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC;IAC5E,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;IAC7B,OAAO,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AAC7D,CAAC"}
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/pg/crypto.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,6EAA6E;AAC7E,gFAAgF;AAChF,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
@@ -150,6 +150,42 @@ export declare const flows: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
150
150
  identity: undefined;
151
151
  generated: undefined;
152
152
  }, {}, {}>;
153
+ active: import("drizzle-orm/sqlite-core").SQLiteColumn<{
154
+ name: "active";
155
+ tableName: "flows";
156
+ dataType: "boolean";
157
+ columnType: "SQLiteBoolean";
158
+ data: boolean;
159
+ driverParam: number;
160
+ notNull: true;
161
+ hasDefault: true;
162
+ isPrimaryKey: false;
163
+ isAutoincrement: false;
164
+ hasRuntimeDefault: false;
165
+ enumValues: undefined;
166
+ baseColumn: never;
167
+ identity: undefined;
168
+ generated: undefined;
169
+ }, {}, {}>;
170
+ config: import("drizzle-orm/sqlite-core").SQLiteColumn<{
171
+ name: "config";
172
+ tableName: "flows";
173
+ dataType: "string";
174
+ columnType: "SQLiteText";
175
+ data: string;
176
+ driverParam: string;
177
+ notNull: true;
178
+ hasDefault: true;
179
+ isPrimaryKey: false;
180
+ isAutoincrement: false;
181
+ hasRuntimeDefault: false;
182
+ enumValues: [string, ...string[]];
183
+ baseColumn: never;
184
+ identity: undefined;
185
+ generated: undefined;
186
+ }, {}, {
187
+ length: number | undefined;
188
+ }>;
153
189
  };
154
190
  dialect: "sqlite";
155
191
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/schema/flows.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAShB,CAAC"}
1
+ {"version":3,"file":"flows.d.ts","sourceRoot":"","sources":["../../src/schema/flows.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWhB,CAAC"}
@@ -8,5 +8,7 @@ export const flows = sqliteTable('flows', {
8
8
  tenantId: text('tenant_id'), // tenant scope — null for pre-migration rows
9
9
  createdAt: integer('created_at').notNull(),
10
10
  updatedAt: integer('updated_at').notNull(),
11
+ active: integer('active', { mode: 'boolean' }).notNull().default(false),
12
+ config: text('config').notNull().default('{}'),
11
13
  });
12
14
  //# sourceMappingURL=flows.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"flows.js","sourceRoot":"","sources":["../../src/schema/flows.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAErE,MAAM,CAAC,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,EAAE;IACxC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE;IAC3B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,4BAA4B;IAC1E,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,4BAA4B;IAC1E,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,0DAA0D;IACnF,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,6CAA6C;IAC1E,SAAS,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE;IAC1C,SAAS,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE;CAC3C,CAAC,CAAC"}
1
+ {"version":3,"file":"flows.js","sourceRoot":"","sources":["../../src/schema/flows.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAErE,MAAM,CAAC,MAAM,KAAK,GAAG,WAAW,CAAC,OAAO,EAAE;IACxC,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE;IAC3B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,4BAA4B;IAC1E,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,4BAA4B;IAC1E,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,0DAA0D;IACnF,QAAQ,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,6CAA6C;IAC1E,SAAS,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE;IAC1C,SAAS,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE;IAC1C,MAAM,EAAE,OAAO,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC;IACvE,MAAM,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC;CAC/C,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minion-stack/db",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Drizzle ORM schema for the Minion shared database (LibSQL/Turso).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/pg/schema/index.d.ts",
29
29
  "import": "./dist/pg/schema/index.js"
30
30
  },
31
+ "./crypto": {
32
+ "types": "./dist/crypto.d.ts",
33
+ "import": "./dist/crypto.js"
34
+ },
31
35
  "./relations": {
32
36
  "types": "./dist/relations.d.ts",
33
37
  "import": "./dist/relations.js"
@@ -45,24 +49,23 @@
45
49
  "src",
46
50
  "README.md"
47
51
  ],
48
- "scripts": {
49
- "build": "tsc",
50
- "prepublishOnly": "tsc",
51
- "typecheck": "tsc --noEmit",
52
- "lint": "oxlint src",
53
- "db:pg:generate": "drizzle-kit generate --config=drizzle.pg.config.ts",
54
- "test": "vitest run"
55
- },
56
52
  "peerDependencies": {
57
53
  "drizzle-orm": ">=0.45.0"
58
54
  },
59
55
  "devDependencies": {
60
- "@minion-stack/tsconfig": "workspace:*",
61
56
  "@paralleldrive/cuid2": "^3.3.0",
62
57
  "drizzle-kit": "^0.31.9",
63
58
  "drizzle-orm": "^0.45.1",
64
59
  "oxlint": "^1.66.0",
65
60
  "typescript": "^5.0.0",
66
- "vitest": "^2.1.9"
61
+ "vitest": "^2.1.9",
62
+ "@minion-stack/tsconfig": "0.1.0"
63
+ },
64
+ "scripts": {
65
+ "build": "tsc",
66
+ "typecheck": "tsc --noEmit",
67
+ "lint": "oxlint src",
68
+ "db:pg:generate": "drizzle-kit generate --config=drizzle.pg.config.ts",
69
+ "test": "vitest run"
67
70
  }
68
- }
71
+ }
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ sealSecret,
4
+ openSecret,
5
+ encrypt,
6
+ decrypt,
7
+ encryptToken,
8
+ decryptToken,
9
+ } from "./crypto.js";
10
+
11
+ describe("canonical crypto", () => {
12
+ it("seal/open roundtrips", () => {
13
+ const { ciphertext, iv } = sealSecret("hunter2");
14
+ expect(ciphertext).not.toContain("hunter2");
15
+ expect(openSecret(ciphertext, iv)).toBe("hunter2");
16
+ });
17
+
18
+ it("encrypt/decrypt are aliases of seal/open", () => {
19
+ const { ciphertext, iv } = encrypt("s3cret");
20
+ expect(decrypt(ciphertext, iv)).toBe("s3cret");
21
+ });
22
+
23
+ it("encryptToken returns { encrypted, iv } and decryptToken roundtrips", () => {
24
+ const { encrypted, iv } = encryptToken("tok-abc");
25
+ expect(decryptToken(encrypted, iv)).toBe("tok-abc");
26
+ });
27
+
28
+ it("openSecret throws on a tampered ciphertext (GCM auth)", () => {
29
+ const { ciphertext, iv } = sealSecret("x");
30
+ const tampered = (ciphertext[0] === "a" ? "b" : "a") + ciphertext.slice(1);
31
+ expect(() => openSecret(tampered, iv)).toThrow();
32
+ });
33
+ });
package/src/crypto.ts ADDED
@@ -0,0 +1,73 @@
1
+ // Canonical app-level secret encryption for the Minion stack (R7 of
2
+ // specs/2026-05-26-auth-token-simplification.md). AES-256-GCM, dialect-agnostic
3
+ // — consumed by the PG identity path (sealSecret/openSecret) and re-exported by
4
+ // minion_hub's crypto.ts (encrypt/decrypt/encryptToken/decryptToken) so there is
5
+ // ONE implementation and one key-derivation path instead of byte-matched copies.
6
+ //
7
+ // Layout (MUST stay stable — existing ciphertext at rest depends on it):
8
+ // key = scryptSync(ENCRYPTION_KEY, 'minion-hub-salt', 32)
9
+ // ciphertext = hex(encrypted || authTag) (16-byte GCM tag LAST)
10
+ // iv = hex(12 random bytes), stored separately
11
+
12
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
13
+
14
+ const ALGORITHM = "aes-256-gcm";
15
+ const IV_BYTES = 12;
16
+ const AUTH_TAG_BYTES = 16;
17
+
18
+ let cachedKey: Buffer | null = null;
19
+ function key(): Buffer {
20
+ if (cachedKey) return cachedKey;
21
+ const raw = process.env.ENCRYPTION_KEY;
22
+ if (!raw) {
23
+ if (process.env.NODE_ENV === "production") {
24
+ throw new Error("ENCRYPTION_KEY environment variable must be set in production");
25
+ }
26
+ // Dev-only fallback — never used in production.
27
+ cachedKey = scryptSync("minion-hub-dev-key", "minion-hub-salt", 32);
28
+ return cachedKey;
29
+ }
30
+ cachedKey = scryptSync(raw, "minion-hub-salt", 32);
31
+ return cachedKey;
32
+ }
33
+
34
+ /** Seal plaintext → { ciphertext, iv }. ciphertext = hex(encrypted || authTag). */
35
+ export function sealSecret(plaintext: string): { ciphertext: string; iv: string } {
36
+ const iv = randomBytes(IV_BYTES);
37
+ const cipher = createCipheriv(ALGORITHM, key(), iv);
38
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
39
+ const authTag = cipher.getAuthTag();
40
+ const combined = Buffer.concat([encrypted, authTag]);
41
+ return { ciphertext: combined.toString("hex"), iv: iv.toString("hex") };
42
+ }
43
+
44
+ /** Open hex(encrypted || authTag) + hex(iv) → plaintext. Throws on auth failure. */
45
+ export function openSecret(ciphertext: string, iv: string): string {
46
+ const combined = Buffer.from(ciphertext, "hex");
47
+ const encrypted = combined.subarray(0, combined.length - AUTH_TAG_BYTES);
48
+ const authTag = combined.subarray(combined.length - AUTH_TAG_BYTES);
49
+ const decipher = createDecipheriv(ALGORITHM, key(), Buffer.from(iv, "hex"));
50
+ decipher.setAuthTag(authTag);
51
+ return decipher.update(encrypted) + decipher.final("utf8");
52
+ }
53
+
54
+ // --- minion_hub-compatible aliases -------------------------------------------
55
+ // Hub's crypto.ts historically exported these names; keeping them lets hub become
56
+ // a thin re-export of this module without touching its many call sites.
57
+
58
+ /** Alias of {@link sealSecret}. */
59
+ export const encrypt = sealSecret;
60
+
61
+ /** Alias of {@link openSecret}. */
62
+ export const decrypt = openSecret;
63
+
64
+ /** Seal a token → { encrypted, iv } (hub's field name for the ciphertext). */
65
+ export function encryptToken(token: string): { encrypted: string; iv: string } {
66
+ const { ciphertext, iv } = sealSecret(token);
67
+ return { encrypted: ciphertext, iv };
68
+ }
69
+
70
+ /** Open a sealed token. */
71
+ export function decryptToken(encrypted: string, iv: string): string {
72
+ return openSecret(encrypted, iv);
73
+ }
package/src/pg/crypto.ts CHANGED
@@ -1,44 +1,4 @@
1
- import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
2
-
3
- const ALGORITHM = 'aes-256-gcm';
4
- const IV_BYTES = 12;
5
- const AUTH_TAG_BYTES = 16;
6
-
7
- // MUST match minion_hub/src/server/auth/crypto.ts so hub + site interoperate:
8
- // key = scryptSync(ENCRYPTION_KEY, 'minion-hub-salt', 32)
9
- // ciphertext (hex) = encrypted || authTag (16-byte tag LAST)
10
- // iv (hex) = 12 random bytes, stored separately
11
- let cachedKey: Buffer | null = null;
12
- function key(): Buffer {
13
- if (cachedKey) return cachedKey;
14
- const raw = process.env.ENCRYPTION_KEY;
15
- if (!raw) {
16
- if (process.env.NODE_ENV === 'production') {
17
- throw new Error('ENCRYPTION_KEY environment variable must be set in production');
18
- }
19
- cachedKey = scryptSync('minion-hub-dev-key', 'minion-hub-salt', 32);
20
- return cachedKey;
21
- }
22
- cachedKey = scryptSync(raw, 'minion-hub-salt', 32);
23
- return cachedKey;
24
- }
25
-
26
- /** Seal plaintext → { ciphertext, iv }. ciphertext = hex(encrypted || authTag). */
27
- export function sealSecret(plaintext: string): { ciphertext: string; iv: string } {
28
- const iv = randomBytes(IV_BYTES);
29
- const cipher = createCipheriv(ALGORITHM, key(), iv);
30
- const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
31
- const authTag = cipher.getAuthTag();
32
- const combined = Buffer.concat([encrypted, authTag]);
33
- return { ciphertext: combined.toString('hex'), iv: iv.toString('hex') };
34
- }
35
-
36
- /** Open hex(encrypted || authTag) + hex(iv) → plaintext. Throws on auth failure. */
37
- export function openSecret(ciphertext: string, iv: string): string {
38
- const combined = Buffer.from(ciphertext, 'hex');
39
- const encrypted = combined.subarray(0, combined.length - AUTH_TAG_BYTES);
40
- const authTag = combined.subarray(combined.length - AUTH_TAG_BYTES);
41
- const decipher = createDecipheriv(ALGORITHM, key(), Buffer.from(iv, 'hex'));
42
- decipher.setAuthTag(authTag);
43
- return decipher.update(encrypted) + decipher.final('utf8');
44
- }
1
+ // Re-export of the canonical crypto module (../crypto.ts). Kept as a stable
2
+ // subpath for existing importers of the PG identity path; the implementation
3
+ // lives in one place now (R7 of specs/2026-05-26-auth-token-simplification.md).
4
+ export { sealSecret, openSecret } from "../crypto.js";
@@ -9,4 +9,6 @@ export const flows = sqliteTable('flows', {
9
9
  tenantId: text('tenant_id'), // tenant scope — null for pre-migration rows
10
10
  createdAt: integer('created_at').notNull(),
11
11
  updatedAt: integer('updated_at').notNull(),
12
+ active: integer('active', { mode: 'boolean' }).notNull().default(false),
13
+ config: text('config').notNull().default('{}'),
12
14
  });