@agentic-kit/db-tools 0.2.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.
Files changed (131) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +138 -0
  3. package/app-workspace.d.ts +16 -0
  4. package/app-workspace.js +145 -0
  5. package/context.d.ts +62 -0
  6. package/context.js +282 -0
  7. package/db-probe.d.ts +30 -0
  8. package/db-probe.js +50 -0
  9. package/esm/app-workspace.d.ts +16 -0
  10. package/esm/app-workspace.js +134 -0
  11. package/esm/context.d.ts +62 -0
  12. package/esm/context.js +270 -0
  13. package/esm/db-probe.d.ts +30 -0
  14. package/esm/db-probe.js +47 -0
  15. package/esm/host.d.ts +115 -0
  16. package/esm/host.js +23 -0
  17. package/esm/index.d.ts +20 -0
  18. package/esm/index.js +52 -0
  19. package/esm/policy/add-policies-to-table.d.ts +26 -0
  20. package/esm/policy/add-policies-to-table.js +66 -0
  21. package/esm/policy/provision-helpers.d.ts +38 -0
  22. package/esm/policy/provision-helpers.js +50 -0
  23. package/esm/provision-database/credential.d.ts +14 -0
  24. package/esm/provision-database/credential.js +19 -0
  25. package/esm/provision-database/env-file.d.ts +37 -0
  26. package/esm/provision-database/env-file.js +85 -0
  27. package/esm/provision-database/manifest.d.ts +10 -0
  28. package/esm/provision-database/manifest.js +48 -0
  29. package/esm/provision-database/pg-fixups.d.ts +16 -0
  30. package/esm/provision-database/pg-fixups.js +132 -0
  31. package/esm/provision-database/preset-match.d.ts +13 -0
  32. package/esm/provision-database/preset-match.js +66 -0
  33. package/esm/provision-database/presets.d.ts +4 -0
  34. package/esm/provision-database/presets.js +13 -0
  35. package/esm/provision-database/request-database.d.ts +61 -0
  36. package/esm/provision-database/request-database.js +130 -0
  37. package/esm/provision-database/resolve.d.ts +27 -0
  38. package/esm/provision-database/resolve.js +49 -0
  39. package/esm/records/meta.d.ts +41 -0
  40. package/esm/records/meta.js +167 -0
  41. package/esm/run-codegen/barrels.d.ts +3 -0
  42. package/esm/run-codegen/barrels.js +48 -0
  43. package/esm/run-codegen/endpoints.d.ts +8 -0
  44. package/esm/run-codegen/endpoints.js +46 -0
  45. package/esm/schema-resolve.d.ts +23 -0
  46. package/esm/schema-resolve.js +69 -0
  47. package/esm/tool-schema.d.ts +3 -0
  48. package/esm/tool-schema.js +10 -0
  49. package/esm/tools/add-policies.d.ts +24 -0
  50. package/esm/tools/add-policies.js +94 -0
  51. package/esm/tools/add-records.d.ts +18 -0
  52. package/esm/tools/add-records.js +122 -0
  53. package/esm/tools/add-relation-schema.d.ts +29 -0
  54. package/esm/tools/add-relation-schema.js +56 -0
  55. package/esm/tools/add-relation.d.ts +10 -0
  56. package/esm/tools/add-relation.js +91 -0
  57. package/esm/tools/create-api-key.d.ts +25 -0
  58. package/esm/tools/create-api-key.js +214 -0
  59. package/esm/tools/describe-schema.d.ts +19 -0
  60. package/esm/tools/describe-schema.js +130 -0
  61. package/esm/tools/manage-entity-types.d.ts +40 -0
  62. package/esm/tools/manage-entity-types.js +201 -0
  63. package/esm/tools/mutations.d.ts +35 -0
  64. package/esm/tools/mutations.js +230 -0
  65. package/esm/tools/provision-blueprint.d.ts +21 -0
  66. package/esm/tools/provision-blueprint.js +109 -0
  67. package/esm/tools/provision-database.d.ts +18 -0
  68. package/esm/tools/provision-database.js +260 -0
  69. package/esm/tools/run-codegen.d.ts +12 -0
  70. package/esm/tools/run-codegen.js +137 -0
  71. package/esm/tools/templates.d.ts +71 -0
  72. package/esm/tools/templates.js +331 -0
  73. package/host.d.ts +115 -0
  74. package/host.js +28 -0
  75. package/index.d.ts +20 -0
  76. package/index.js +76 -0
  77. package/package.json +49 -0
  78. package/policy/add-policies-to-table.d.ts +26 -0
  79. package/policy/add-policies-to-table.js +71 -0
  80. package/policy/provision-helpers.d.ts +38 -0
  81. package/policy/provision-helpers.js +58 -0
  82. package/provision-database/credential.d.ts +14 -0
  83. package/provision-database/credential.js +22 -0
  84. package/provision-database/env-file.d.ts +37 -0
  85. package/provision-database/env-file.js +91 -0
  86. package/provision-database/manifest.d.ts +10 -0
  87. package/provision-database/manifest.js +56 -0
  88. package/provision-database/pg-fixups.d.ts +16 -0
  89. package/provision-database/pg-fixups.js +170 -0
  90. package/provision-database/preset-match.d.ts +13 -0
  91. package/provision-database/preset-match.js +70 -0
  92. package/provision-database/presets.d.ts +4 -0
  93. package/provision-database/presets.js +17 -0
  94. package/provision-database/request-database.d.ts +61 -0
  95. package/provision-database/request-database.js +134 -0
  96. package/provision-database/resolve.d.ts +27 -0
  97. package/provision-database/resolve.js +53 -0
  98. package/records/meta.d.ts +41 -0
  99. package/records/meta.js +172 -0
  100. package/run-codegen/barrels.d.ts +3 -0
  101. package/run-codegen/barrels.js +54 -0
  102. package/run-codegen/endpoints.d.ts +8 -0
  103. package/run-codegen/endpoints.js +51 -0
  104. package/schema-resolve.d.ts +23 -0
  105. package/schema-resolve.js +75 -0
  106. package/tool-schema.d.ts +3 -0
  107. package/tool-schema.js +13 -0
  108. package/tools/add-policies.d.ts +24 -0
  109. package/tools/add-policies.js +97 -0
  110. package/tools/add-records.d.ts +18 -0
  111. package/tools/add-records.js +125 -0
  112. package/tools/add-relation-schema.d.ts +29 -0
  113. package/tools/add-relation-schema.js +60 -0
  114. package/tools/add-relation.d.ts +10 -0
  115. package/tools/add-relation.js +94 -0
  116. package/tools/create-api-key.d.ts +25 -0
  117. package/tools/create-api-key.js +220 -0
  118. package/tools/describe-schema.d.ts +19 -0
  119. package/tools/describe-schema.js +133 -0
  120. package/tools/manage-entity-types.d.ts +40 -0
  121. package/tools/manage-entity-types.js +206 -0
  122. package/tools/mutations.d.ts +35 -0
  123. package/tools/mutations.js +233 -0
  124. package/tools/provision-blueprint.d.ts +21 -0
  125. package/tools/provision-blueprint.js +112 -0
  126. package/tools/provision-database.d.ts +18 -0
  127. package/tools/provision-database.js +266 -0
  128. package/tools/run-codegen.d.ts +12 -0
  129. package/tools/run-codegen.js +141 -0
  130. package/tools/templates.d.ts +71 -0
  131. package/tools/templates.js +335 -0
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnsupportedPolicyCategoryError = void 0;
4
+ exports.addPoliciesToExistingTable = addPoliciesToExistingTable;
5
+ const harness_1 = require("@agentic-kit/harness");
6
+ const provision_helpers_1 = require("./provision-helpers");
7
+ const SUPPORTED_CATEGORIES = new Set([
8
+ 'has-module',
9
+ 'no-fields',
10
+ 'needs-table',
11
+ ]);
12
+ class UnsupportedPolicyCategoryError extends Error {
13
+ policyType;
14
+ category;
15
+ constructor(policyType, category) {
16
+ super(`Policy type "${policyType}" has category "${category}" which requires column creation or composite handling — use the policies UI instead.`);
17
+ this.policyType = policyType;
18
+ this.category = category;
19
+ this.name = 'UnsupportedPolicyCategoryError';
20
+ }
21
+ }
22
+ exports.UnsupportedPolicyCategoryError = UnsupportedPolicyCategoryError;
23
+ async function addPoliciesToExistingTable(input) {
24
+ const { client } = input;
25
+ for (const entry of input.policies) {
26
+ const category = (0, harness_1.getPolicyCategory)(entry.policyType);
27
+ if (!SUPPORTED_CATEGORIES.has(category)) {
28
+ throw new UnsupportedPolicyCategoryError(entry.policyType, category);
29
+ }
30
+ }
31
+ const policiesArray = [];
32
+ const nodesByType = new Map();
33
+ for (const entry of input.policies) {
34
+ const groups = (0, provision_helpers_1.groupOperationsByConfig)(entry.enabledOperations ?? provision_helpers_1.CRUD_OPERATIONS, entry.operations);
35
+ for (const group of groups) {
36
+ const ops = group.privileges.join('_');
37
+ const rand = Math.random().toString(36).slice(2, 8);
38
+ policiesArray.push((0, provision_helpers_1.buildPolicyEntry)(entry.policyType, {
39
+ privileges: group.privileges,
40
+ policy_role: group.roleName,
41
+ permissive: group.isPermissive,
42
+ data: entry.sharedPolicyData,
43
+ policy_name: `${ops}_${rand}`,
44
+ }));
45
+ }
46
+ if (entry.dataNodeType && !nodesByType.has(entry.dataNodeType)) {
47
+ const nodeData = entry.nodeData ?? {};
48
+ nodesByType.set(entry.dataNodeType, {
49
+ $type: entry.dataNodeType,
50
+ ...(Object.keys(nodeData).length > 0 ? { data: nodeData } : {}),
51
+ });
52
+ }
53
+ }
54
+ const provisionInput = {
55
+ databaseId: input.databaseId,
56
+ schemaId: input.schemaId,
57
+ tableId: input.tableId,
58
+ grants: (0, provision_helpers_1.buildGrants)(),
59
+ policies: policiesArray,
60
+ };
61
+ if (nodesByType.size > 0) {
62
+ provisionInput.nodes = [...nodesByType.values()];
63
+ }
64
+ await client.secureTableProvision
65
+ .create({
66
+ data: provisionInput,
67
+ select: { id: true, tableId: true, tableName: true },
68
+ })
69
+ .unwrap();
70
+ return { success: true };
71
+ }
@@ -0,0 +1,38 @@
1
+ export type CrudOperation = 'create' | 'read' | 'update' | 'delete';
2
+ export interface OperationPolicyConfig {
3
+ roleName: string;
4
+ isPermissive: boolean;
5
+ policyData: Record<string, unknown>;
6
+ isCustomized: boolean;
7
+ }
8
+ export interface CrudPolicyConfigs {
9
+ create: OperationPolicyConfig;
10
+ read: OperationPolicyConfig;
11
+ update: OperationPolicyConfig;
12
+ delete: OperationPolicyConfig;
13
+ }
14
+ export declare const CRUD_OPERATIONS: CrudOperation[];
15
+ export type GrantPrivilege = [string, string | string[]];
16
+ export interface GrantEntry {
17
+ roles: string[];
18
+ privileges: GrantPrivilege[];
19
+ }
20
+ export interface PolicyEntry {
21
+ $type: string;
22
+ data?: Record<string, unknown>;
23
+ privileges?: string[];
24
+ policy_role?: string;
25
+ permissive?: boolean;
26
+ policy_name?: string;
27
+ }
28
+ export declare const ALL_CRUD_PRIVILEGES: GrantPrivilege[];
29
+ export declare function crudOpToPrivilege(op: CrudOperation): string;
30
+ export declare function crudOpsToPrivileges(ops: readonly CrudOperation[]): string[];
31
+ export declare function buildGrants(roles?: string[], privileges?: GrantPrivilege[]): GrantEntry[];
32
+ export declare function buildPolicyEntry(type: string, opts?: Partial<Omit<PolicyEntry, '$type'>>): PolicyEntry;
33
+ export interface OperationGroup {
34
+ roleName: string;
35
+ isPermissive: boolean;
36
+ privileges: string[];
37
+ }
38
+ export declare function groupOperationsByConfig(enabledOperations: readonly CrudOperation[], operations: CrudPolicyConfigs): OperationGroup[];
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ALL_CRUD_PRIVILEGES = exports.CRUD_OPERATIONS = void 0;
4
+ exports.crudOpToPrivilege = crudOpToPrivilege;
5
+ exports.crudOpsToPrivileges = crudOpsToPrivileges;
6
+ exports.buildGrants = buildGrants;
7
+ exports.buildPolicyEntry = buildPolicyEntry;
8
+ exports.groupOperationsByConfig = groupOperationsByConfig;
9
+ exports.CRUD_OPERATIONS = ['create', 'read', 'update', 'delete'];
10
+ exports.ALL_CRUD_PRIVILEGES = [
11
+ ['select', '*'],
12
+ ['insert', '*'],
13
+ ['update', '*'],
14
+ ['delete', '*'],
15
+ ];
16
+ const CRUD_OP_TO_LOWER_PRIVILEGE = {
17
+ create: 'insert',
18
+ read: 'select',
19
+ update: 'update',
20
+ delete: 'delete',
21
+ };
22
+ function crudOpToPrivilege(op) {
23
+ return CRUD_OP_TO_LOWER_PRIVILEGE[op];
24
+ }
25
+ function crudOpsToPrivileges(ops) {
26
+ return ops.map(crudOpToPrivilege);
27
+ }
28
+ function buildGrants(roles = ['authenticated'], privileges = exports.ALL_CRUD_PRIVILEGES) {
29
+ return [{ roles, privileges }];
30
+ }
31
+ function buildPolicyEntry(type, opts = {}) {
32
+ const entry = { $type: type };
33
+ if (opts.data && Object.keys(opts.data).length > 0)
34
+ entry.data = opts.data;
35
+ if (opts.privileges && opts.privileges.length > 0)
36
+ entry.privileges = opts.privileges;
37
+ if (opts.policy_role)
38
+ entry.policy_role = opts.policy_role;
39
+ if (opts.permissive !== undefined)
40
+ entry.permissive = opts.permissive;
41
+ if (opts.policy_name)
42
+ entry.policy_name = opts.policy_name;
43
+ return entry;
44
+ }
45
+ function groupOperationsByConfig(enabledOperations, operations) {
46
+ const groupMap = new Map();
47
+ for (const op of enabledOperations) {
48
+ const config = operations[op];
49
+ const key = `${config.roleName}:${config.isPermissive}`;
50
+ let group = groupMap.get(key);
51
+ if (!group) {
52
+ group = { roleName: config.roleName, isPermissive: config.isPermissive, privileges: [] };
53
+ groupMap.set(key, group);
54
+ }
55
+ group.privileges.push(...crudOpsToPrivileges([op]));
56
+ }
57
+ return [...groupMap.values()];
58
+ }
@@ -0,0 +1,14 @@
1
+ export type AccountCredential = {
2
+ userId: string;
3
+ accessToken: string;
4
+ apiKey?: string;
5
+ };
6
+ export type ProvisionCredential = {
7
+ mode: 'account';
8
+ ownerId: string;
9
+ bearer: string;
10
+ } | {
11
+ mode: 'error';
12
+ reason: string;
13
+ };
14
+ export declare function selectProvisionCredential(account: AccountCredential | null | undefined, signInHint?: string): ProvisionCredential;
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ // Pure: decide which credential authenticates a project's provisioning. Under the
3
+ // account gate a signed-in session is guaranteed, so every new project provisions
4
+ // UNDER the account (account-owned + enumerable): the database's owner_id is the
5
+ // account's own userId, and the account's API key (falling back to its session
6
+ // token) is the bearer that authenticates the provision mutation and lands in the
7
+ // project .env as ACCESS_TOKEN (refreshed on every provision run, including the
8
+ // skip path). A signed-in session with no usable bearer (keychain locked) is an
9
+ // explicit error, never a silent throwaway owner.
10
+ // Side-effect-free so the branch is unit-testable without runtime/network.
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.selectProvisionCredential = selectProvisionCredential;
13
+ function selectProvisionCredential(account, signInHint) {
14
+ const bearer = account?.apiKey ?? account?.accessToken;
15
+ if (account?.userId && bearer) {
16
+ return { mode: 'account', ownerId: account.userId, bearer };
17
+ }
18
+ return {
19
+ mode: 'error',
20
+ reason: `No usable account credential. ${signInHint ?? 'Sign in (or unlock your session) before provisioning a database.'}`,
21
+ };
22
+ }
@@ -0,0 +1,37 @@
1
+ export type EnvVars = Record<string, string>;
2
+ /**
3
+ * Upsert `vars` into the existing `.env` `content`. Existing keys are replaced
4
+ * in place (preserving surrounding lines/comments); new keys are appended.
5
+ * Returns the new file body, trailing-newline-terminated.
6
+ */
7
+ export declare function mergeEnv(content: string, vars: EnvVars): string;
8
+ /** The credential keys provision_database owns in a project's .env. */
9
+ export declare const PROVISION_ENV_KEYS: readonly ["DATABASE_ID", "DATABASE_NAME", "OWNER_ID", "ACCESS_TOKEN", "PGDATABASE", "API_ENDPOINT", "MODULES_ENDPOINT"];
10
+ /**
11
+ * Keys archived as comments on reprovision — the binding identity: which
12
+ * database, under whom, on which backend. ACCESS_TOKEN is the account login
13
+ * token, identical across bindings, so it is overwritten in place rather than
14
+ * archived.
15
+ */
16
+ export declare const ARCHIVED_BINDING_KEYS: readonly ["DATABASE_ID", "DATABASE_NAME", "OWNER_ID", "PGDATABASE", "API_ENDPOINT", "MODULES_ENDPOINT"];
17
+ /**
18
+ * Build the provisioning credential block written to a project's .env. The
19
+ * endpoint pins record which backend owns this binding — outside-app consumers
20
+ * (verify-feature.sh, skill scripts, dev-server shells) resolve them instead of
21
+ * falling back to localhost defaults when the app targets a hosted backend.
22
+ */
23
+ export declare function provisionEnvVars(args: {
24
+ databaseId: string;
25
+ databaseName: string;
26
+ ownerId: string;
27
+ accessToken: string;
28
+ apiEndpoint: string;
29
+ modulesEndpoint: string;
30
+ }): EnvVars;
31
+ /**
32
+ * Comment out the given keys in place (`# archived <date>: KEY=value`) so a
33
+ * reprovision keeps the previous binding readable in the file instead of
34
+ * silently overwriting it. Every other line — including existing comments and
35
+ * already-archived entries — passes through untouched.
36
+ */
37
+ export declare function archiveBindingKeys(content: string, keys: readonly string[], archivedOn: string): string;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ // Pure .env merge — used by provision_database to persist provisioning
3
+ // credentials without clobbering existing keys: replace a key in place if
4
+ // present, else append.
5
+ // Kept side-effect-free so it is unit-testable (the tool does the fs write).
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ARCHIVED_BINDING_KEYS = exports.PROVISION_ENV_KEYS = void 0;
8
+ exports.mergeEnv = mergeEnv;
9
+ exports.provisionEnvVars = provisionEnvVars;
10
+ exports.archiveBindingKeys = archiveBindingKeys;
11
+ /**
12
+ * Upsert `vars` into the existing `.env` `content`. Existing keys are replaced
13
+ * in place (preserving surrounding lines/comments); new keys are appended.
14
+ * Returns the new file body, trailing-newline-terminated.
15
+ */
16
+ function mergeEnv(content, vars) {
17
+ let out = content;
18
+ for (const [key, val] of Object.entries(vars)) {
19
+ // Escape the key for the regex, and use a replacement function so `$`
20
+ // sequences in the value are never treated as replacement specials.
21
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
22
+ const regex = new RegExp(`^${escapedKey}=.*`, 'm');
23
+ if (regex.test(out))
24
+ out = out.replace(regex, () => `${key}=${val}`);
25
+ else
26
+ out += `\n${key}=${val}`;
27
+ }
28
+ return out.trim() + '\n';
29
+ }
30
+ /** The credential keys provision_database owns in a project's .env. */
31
+ exports.PROVISION_ENV_KEYS = [
32
+ 'DATABASE_ID',
33
+ 'DATABASE_NAME',
34
+ 'OWNER_ID',
35
+ 'ACCESS_TOKEN',
36
+ 'PGDATABASE',
37
+ 'API_ENDPOINT',
38
+ 'MODULES_ENDPOINT',
39
+ ];
40
+ /**
41
+ * Keys archived as comments on reprovision — the binding identity: which
42
+ * database, under whom, on which backend. ACCESS_TOKEN is the account login
43
+ * token, identical across bindings, so it is overwritten in place rather than
44
+ * archived.
45
+ */
46
+ exports.ARCHIVED_BINDING_KEYS = [
47
+ 'DATABASE_ID',
48
+ 'DATABASE_NAME',
49
+ 'OWNER_ID',
50
+ 'PGDATABASE',
51
+ 'API_ENDPOINT',
52
+ 'MODULES_ENDPOINT',
53
+ ];
54
+ /**
55
+ * Build the provisioning credential block written to a project's .env. The
56
+ * endpoint pins record which backend owns this binding — outside-app consumers
57
+ * (verify-feature.sh, skill scripts, dev-server shells) resolve them instead of
58
+ * falling back to localhost defaults when the app targets a hosted backend.
59
+ */
60
+ function provisionEnvVars(args) {
61
+ return {
62
+ DATABASE_ID: args.databaseId,
63
+ DATABASE_NAME: args.databaseName,
64
+ OWNER_ID: args.ownerId,
65
+ ACCESS_TOKEN: args.accessToken,
66
+ PGDATABASE: args.databaseName,
67
+ API_ENDPOINT: args.apiEndpoint,
68
+ MODULES_ENDPOINT: args.modulesEndpoint,
69
+ };
70
+ }
71
+ /**
72
+ * Comment out the given keys in place (`# archived <date>: KEY=value`) so a
73
+ * reprovision keeps the previous binding readable in the file instead of
74
+ * silently overwriting it. Every other line — including existing comments and
75
+ * already-archived entries — passes through untouched.
76
+ */
77
+ function archiveBindingKeys(content, keys, archivedOn) {
78
+ const keySet = new Set(keys);
79
+ return content
80
+ .split('\n')
81
+ .map((line) => {
82
+ if (line.trimStart().startsWith('#'))
83
+ return line;
84
+ const eq = line.indexOf('=');
85
+ if (eq === -1)
86
+ return line;
87
+ const key = line.slice(0, eq).trim();
88
+ return keySet.has(key) ? `# archived ${archivedOn}: ${line.trim()}` : line;
89
+ })
90
+ .join('\n');
91
+ }
@@ -0,0 +1,10 @@
1
+ import type { ProvisionOverlay } from './resolve';
2
+ export declare const PROVISION_MANIFEST_FILE = "provision.json";
3
+ export type ProvisionManifest = ProvisionOverlay;
4
+ /** Validate an untrusted value as a provision manifest, throwing on bad shape. */
5
+ export declare function parseProvisionManifest(value: unknown): ProvisionManifest;
6
+ /**
7
+ * Load `<dir>/provision.json` as an overlay, or `null` when absent. A malformed
8
+ * file throws so a broken overlay is surfaced rather than silently ignored.
9
+ */
10
+ export declare function loadProvisionManifest(dir: string): Promise<ProvisionManifest | null>;
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.PROVISION_MANIFEST_FILE = void 0;
7
+ exports.parseProvisionManifest = parseProvisionManifest;
8
+ exports.loadProvisionManifest = loadProvisionManifest;
9
+ // `provision.json` overlay manifest.
10
+ //
11
+ // A pure-data overlay, distributed the same way skills are: written into a
12
+ // config/app directory (locally, or materialized there from appstash / a pinned
13
+ // git ref) and read at provision time. It NEVER installs or executes a package
14
+ // — the base preset is a pinned build-time dependency; this file only selects a
15
+ // preset and layers add/remove on top.
16
+ const promises_1 = require("node:fs/promises");
17
+ const node_path_1 = __importDefault(require("node:path"));
18
+ const zod_1 = require("zod");
19
+ exports.PROVISION_MANIFEST_FILE = 'provision.json';
20
+ const ModuleSchema = zod_1.z.union([
21
+ zod_1.z.string(),
22
+ zod_1.z.tuple([zod_1.z.string(), zod_1.z.record(zod_1.z.string(), zod_1.z.unknown())]),
23
+ ]);
24
+ const ProvisionManifestSchema = zod_1.z
25
+ .object({
26
+ preset: zod_1.z.string().optional(),
27
+ add: zod_1.z.array(ModuleSchema).optional(),
28
+ remove: zod_1.z.array(zod_1.z.string()).optional(),
29
+ })
30
+ .strict();
31
+ /** Validate an untrusted value as a provision manifest, throwing on bad shape. */
32
+ function parseProvisionManifest(value) {
33
+ return ProvisionManifestSchema.parse(value);
34
+ }
35
+ /**
36
+ * Load `<dir>/provision.json` as an overlay, or `null` when absent. A malformed
37
+ * file throws so a broken overlay is surfaced rather than silently ignored.
38
+ */
39
+ async function loadProvisionManifest(dir) {
40
+ const file = node_path_1.default.join(dir, exports.PROVISION_MANIFEST_FILE);
41
+ let raw;
42
+ try {
43
+ raw = await (0, promises_1.readFile)(file, 'utf8');
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(raw);
51
+ }
52
+ catch (err) {
53
+ throw new Error(`Invalid ${exports.PROVISION_MANIFEST_FILE} at ${file}: ${err instanceof Error ? err.message : String(err)}`);
54
+ }
55
+ return parseProvisionManifest(parsed);
56
+ }
@@ -0,0 +1,16 @@
1
+ export type FixupResult = {
2
+ applied: boolean;
3
+ note: string;
4
+ };
5
+ export type PgEnv = {
6
+ host?: string;
7
+ port?: string;
8
+ user?: string;
9
+ password?: string;
10
+ };
11
+ export declare function parsePgpmEnv(stdout: string): PgEnv;
12
+ export declare function resolvePgEnv(): Promise<PgEnv | null>;
13
+ export declare function applySqlFixups(args: {
14
+ databaseName: string;
15
+ physicalDb: string;
16
+ }): Promise<FixupResult>;
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ // Post-provision SQL fixups for a freshly provisioned database.
3
+ //
4
+ // Two things must happen at the Postgres level that the provisioning GraphQL API
5
+ // does not do for us:
6
+ //
7
+ // 1. Naming settings — `constructive.simple_schema_names` and
8
+ // `schema_use_underscores` make later `provision_blueprint` calls create an
9
+ // `app_public` schema (which resolveSchemaId looks for) instead of a
10
+ // hyphenated `<db>-<hex>-app-public` one. These ALTER DATABASE settings
11
+ // PERSIST, so we set them once here, before any blueprint construction.
12
+ //
13
+ // 2. Membership defaults — new sign-ups otherwise land with
14
+ // is_approved=false/is_verified=false, the AuthzEntityMembership policy then
15
+ // denies every insert/select, and CRUD rows silently never persist
16
+ // (BLUEPRINT-PENDING-001). We flip the defaults (future sign-ups) AND any
17
+ // memberships already created during provisioning.
18
+ //
19
+ // All SQL targets the PHYSICAL control-plane database (`constructive`), NOT the
20
+ // app's database name — that name is only a schema prefix (MEMBERSHIP-DB-001).
21
+ //
22
+ // Connection creds come from the ambient PG* env vars, falling back to sourcing
23
+ // `pgpm env` (the same mechanism the old `eval "$(pgpm env)"` shell step used).
24
+ // If neither yields a connection, provisioning still succeeded — we return a
25
+ // note so the tool can warn the user that sign-in/CRUD may need a manual fixup.
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.parsePgpmEnv = parsePgpmEnv;
61
+ exports.resolvePgEnv = resolvePgEnv;
62
+ exports.applySqlFixups = applySqlFixups;
63
+ const node_child_process_1 = require("node:child_process");
64
+ const node_util_1 = require("node:util");
65
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
66
+ // Parse `pgpm env` output. It prints shell assignments (optionally `export`-
67
+ // prefixed), one per line: `export PGHOST=localhost`. Quotes are stripped.
68
+ function parsePgpmEnv(stdout) {
69
+ const env = {};
70
+ for (const rawLine of stdout.split('\n')) {
71
+ const line = rawLine.trim().replace(/^export\s+/, '');
72
+ const eq = line.indexOf('=');
73
+ if (eq === -1)
74
+ continue;
75
+ const key = line.slice(0, eq).trim();
76
+ let value = line.slice(eq + 1).trim();
77
+ if ((value.startsWith('"') && value.endsWith('"')) ||
78
+ (value.startsWith("'") && value.endsWith("'"))) {
79
+ value = value.slice(1, -1);
80
+ }
81
+ env[key] = value;
82
+ }
83
+ return {
84
+ host: env.PGHOST,
85
+ port: env.PGPORT,
86
+ user: env.PGUSER,
87
+ password: env.PGPASSWORD,
88
+ };
89
+ }
90
+ async function resolvePgEnv() {
91
+ if (process.env.PGHOST) {
92
+ return {
93
+ host: process.env.PGHOST,
94
+ port: process.env.PGPORT,
95
+ user: process.env.PGUSER,
96
+ password: process.env.PGPASSWORD,
97
+ };
98
+ }
99
+ try {
100
+ const { stdout } = await execFileAsync('pgpm', ['env'], { timeout: 15_000 });
101
+ const parsed = parsePgpmEnv(stdout);
102
+ return parsed.host ? parsed : null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ // Find the memberships schema for this app. Schema names use hyphens locally
109
+ // (`<db>-<hex>-memberships-public`) but may use underscores when the naming
110
+ // settings are active — match both, scoped to this app's name prefix.
111
+ const MEMBERSHIPS_SCHEMA_QUERY = `
112
+ SELECT schema_name FROM information_schema.schemata
113
+ WHERE (schema_name LIKE '%memberships-public' OR schema_name LIKE '%memberships_public')
114
+ AND (schema_name LIKE $1 OR schema_name LIKE $2)
115
+ ORDER BY schema_name DESC LIMIT 1`;
116
+ async function applySqlFixups(args) {
117
+ const pgEnv = await resolvePgEnv();
118
+ if (!pgEnv) {
119
+ return {
120
+ applied: false,
121
+ note: 'Could not reach Postgres (no PGHOST and `pgpm env` unavailable). Database provisioned, but membership defaults were not enabled — sign-in/CRUD may fail until the constructive DB is fixed up manually.',
122
+ };
123
+ }
124
+ // pg is dynamically imported so the main bundle never loads it unless a
125
+ // provision actually runs with Postgres access. The import lives inside the
126
+ // try: fixups are best-effort, so even a broken pg module must degrade to a
127
+ // note instead of failing the tool after the database is already provisioned.
128
+ let pool;
129
+ try {
130
+ const { Pool } = await Promise.resolve().then(() => __importStar(require('pg')));
131
+ pool = new Pool({
132
+ host: pgEnv.host,
133
+ port: pgEnv.port ? Number(pgEnv.port) : undefined,
134
+ user: pgEnv.user,
135
+ password: pgEnv.password,
136
+ database: args.physicalDb,
137
+ });
138
+ const db = args.physicalDb;
139
+ // Persistent naming settings — so future provision_blueprint calls land an
140
+ // `app_public` schema resolveSchemaId can find.
141
+ await pool.query(`ALTER DATABASE "${db}" SET constructive.simple_schema_names = 'true'`);
142
+ await pool.query(`ALTER DATABASE "${db}" SET constructive.schema_use_underscores = 'true'`);
143
+ const schemaResult = await pool.query(MEMBERSHIPS_SCHEMA_QUERY, [
144
+ `${args.databaseName}-%`,
145
+ `${args.databaseName}\\_%`,
146
+ ]);
147
+ if (schemaResult.rows.length === 0) {
148
+ return {
149
+ applied: true,
150
+ note: `Naming settings applied, but no memberships schema was found for "${args.databaseName}" — sign-in may need a manual membership fixup.`,
151
+ };
152
+ }
153
+ const membershipsSchema = schemaResult.rows[0].schema_name;
154
+ await pool.query(`UPDATE "${membershipsSchema}".app_membership_defaults SET is_approved = TRUE, is_verified = TRUE`);
155
+ await pool.query(`UPDATE "${membershipsSchema}".app_memberships
156
+ SET is_approved = TRUE, is_verified = TRUE
157
+ WHERE is_approved = FALSE OR is_verified = FALSE`);
158
+ return { applied: true, note: `Membership defaults enabled (schema: ${membershipsSchema}).` };
159
+ }
160
+ catch (err) {
161
+ const message = err instanceof Error ? err.message : String(err);
162
+ return {
163
+ applied: false,
164
+ note: `Database provisioned, but SQL fixups failed (${message}). Sign-in/CRUD may fail until membership defaults are enabled manually.`,
165
+ };
166
+ }
167
+ finally {
168
+ await pool?.end().catch(() => { });
169
+ }
170
+ }
@@ -0,0 +1,13 @@
1
+ import { type ProvisionModule } from './presets';
2
+ import type { ProvisionRequest } from './request-database';
3
+ /**
4
+ * The slug of the registry preset whose module set equals `resolved` under
5
+ * the backend's hash semantics, or undefined when no preset matches.
6
+ */
7
+ export declare function matchPresetSlug(resolved: ProvisionModule[]): string | undefined;
8
+ /**
9
+ * Build the requestDatabase input branch: presetSlug when the resolved set
10
+ * equals a registry preset (warm-pool eligible), else the explicit modules
11
+ * array — never both.
12
+ */
13
+ export declare function selectProvisionRequest(resolved: ProvisionModule[]): ProvisionRequest;