@intentius/chant-lexicon-sql 0.117.0 → 0.118.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 (183) hide show
  1. package/dist/clickhouse/access.d.ts +132 -0
  2. package/dist/clickhouse/access.d.ts.map +1 -0
  3. package/dist/clickhouse/apply/apply.d.ts.map +1 -1
  4. package/dist/clickhouse/apply/statements.d.ts +2 -0
  5. package/dist/clickhouse/apply/statements.d.ts.map +1 -1
  6. package/dist/clickhouse/entities.d.ts +171 -2
  7. package/dist/clickhouse/entities.d.ts.map +1 -1
  8. package/dist/clickhouse/http.d.ts +3 -0
  9. package/dist/clickhouse/http.d.ts.map +1 -1
  10. package/dist/clickhouse/import/generator.d.ts +28 -0
  11. package/dist/clickhouse/import/generator.d.ts.map +1 -1
  12. package/dist/clickhouse/import/ir.d.ts.map +1 -1
  13. package/dist/clickhouse/import/parser.d.ts +3 -3
  14. package/dist/clickhouse/import/parser.d.ts.map +1 -1
  15. package/dist/clickhouse/live/bind.d.ts.map +1 -1
  16. package/dist/clickhouse/live/catalog.d.ts +41 -6
  17. package/dist/clickhouse/live/catalog.d.ts.map +1 -1
  18. package/dist/clickhouse/live/describe-resources.d.ts +3 -2
  19. package/dist/clickhouse/live/describe-resources.d.ts.map +1 -1
  20. package/dist/clickhouse/parser.d.ts +54 -3
  21. package/dist/clickhouse/parser.d.ts.map +1 -1
  22. package/dist/clickhouse/plan/commands.d.ts +3 -1
  23. package/dist/clickhouse/plan/commands.d.ts.map +1 -1
  24. package/dist/clickhouse/plan/deep.d.ts.map +1 -1
  25. package/dist/clickhouse/plan/diff.d.ts +12 -0
  26. package/dist/clickhouse/plan/diff.d.ts.map +1 -1
  27. package/dist/clickhouse/plan/normalize.d.ts +61 -1
  28. package/dist/clickhouse/plan/normalize.d.ts.map +1 -1
  29. package/dist/clickhouse/plan/report.d.ts.map +1 -1
  30. package/dist/clickhouse/plan/rules.d.ts +7 -0
  31. package/dist/clickhouse/plan/rules.d.ts.map +1 -1
  32. package/dist/clickhouse/plan/schema.d.ts +8 -4
  33. package/dist/clickhouse/plan/schema.d.ts.map +1 -1
  34. package/dist/clickhouse/rebuild/backfill.d.ts +3 -1
  35. package/dist/clickhouse/rebuild/backfill.d.ts.map +1 -1
  36. package/dist/clickhouse/rebuild/observe.d.ts +7 -3
  37. package/dist/clickhouse/rebuild/observe.d.ts.map +1 -1
  38. package/dist/clickhouse/rebuild/op.d.ts +8 -0
  39. package/dist/clickhouse/rebuild/op.d.ts.map +1 -1
  40. package/dist/clickhouse/rebuild/steps.d.ts +48 -0
  41. package/dist/clickhouse/rebuild/steps.d.ts.map +1 -1
  42. package/dist/clickhouse/testing/fake-server.d.ts.map +1 -1
  43. package/dist/clickhouse/testing/writable-server.d.ts.map +1 -1
  44. package/dist/clickhouse/topology.d.ts.map +1 -1
  45. package/dist/clickhouse.d.ts +1 -1
  46. package/dist/clickhouse.d.ts.map +1 -1
  47. package/dist/codegen/classifier-page.d.ts.map +1 -1
  48. package/dist/codegen/registry.d.ts.map +1 -1
  49. package/dist/config.d.ts +50 -7
  50. package/dist/config.d.ts.map +1 -1
  51. package/dist/core/references.d.ts +4 -2
  52. package/dist/core/references.d.ts.map +1 -1
  53. package/dist/core/statements.d.ts +11 -0
  54. package/dist/core/statements.d.ts.map +1 -1
  55. package/dist/files/build-root.d.ts +26 -0
  56. package/dist/files/build-root.d.ts.map +1 -0
  57. package/dist/files/clickhouse.d.ts +19 -0
  58. package/dist/files/clickhouse.d.ts.map +1 -0
  59. package/dist/files/common.d.ts +25 -0
  60. package/dist/files/common.d.ts.map +1 -0
  61. package/dist/files/postgres.d.ts +36 -0
  62. package/dist/files/postgres.d.ts.map +1 -0
  63. package/dist/import-parser.d.ts +25 -0
  64. package/dist/import-parser.d.ts.map +1 -0
  65. package/dist/index.d.ts +3 -2
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/integrity.json +6 -6
  68. package/dist/lint/rules/sqlch001.d.ts +1 -1
  69. package/dist/lint/rules/sqlch001.d.ts.map +1 -1
  70. package/dist/lint/rules/sqlch002.d.ts.map +1 -1
  71. package/dist/lint/rules/templates.d.ts +1 -1
  72. package/dist/lint/rules/templates.d.ts.map +1 -1
  73. package/dist/manifest.json +1 -1
  74. package/dist/meta.json +30 -0
  75. package/dist/okf/index.md +7 -1
  76. package/dist/okf/rules/SQLCH001.md +2 -2
  77. package/dist/okf/types/Dictionary.md +9 -0
  78. package/dist/okf/types/Function.md +9 -0
  79. package/dist/okf/types/Grant.md +9 -0
  80. package/dist/okf/types/Role.md +9 -0
  81. package/dist/okf/types/RowPolicy.md +9 -0
  82. package/dist/okf/types/User.md +9 -0
  83. package/dist/op/activities/clickhouse-rebuild.d.ts.map +1 -1
  84. package/dist/op/activity-contracts.d.ts +9 -0
  85. package/dist/op/activity-contracts.d.ts.map +1 -1
  86. package/dist/plugin.d.ts.map +1 -1
  87. package/dist/postgres/apply/statements.d.ts +68 -0
  88. package/dist/postgres/apply/statements.d.ts.map +1 -1
  89. package/dist/postgres/import/generator.d.ts +33 -0
  90. package/dist/postgres/import/generator.d.ts.map +1 -1
  91. package/dist/postgres/live/bind.d.ts.map +1 -1
  92. package/dist/postgres/live/client.d.ts +3 -0
  93. package/dist/postgres/live/client.d.ts.map +1 -1
  94. package/dist/rules/sqlch001.ts +26 -4
  95. package/dist/rules/sqlch002.ts +3 -2
  96. package/dist/rules/templates.ts +2 -2
  97. package/dist/serializer.d.ts +2 -1
  98. package/dist/serializer.d.ts.map +1 -1
  99. package/dist/sql-files.d.ts +79 -0
  100. package/dist/sql-files.d.ts.map +1 -0
  101. package/dist/token-source.d.ts +88 -0
  102. package/dist/token-source.d.ts.map +1 -0
  103. package/dist/validate.d.ts.map +1 -1
  104. package/package.json +2 -2
  105. package/src/clickhouse/access.test.ts +203 -0
  106. package/src/clickhouse/access.ts +595 -0
  107. package/src/clickhouse/apply/apply.ts +29 -20
  108. package/src/clickhouse/apply/statements.ts +105 -8
  109. package/src/clickhouse/dictionary.test.ts +177 -0
  110. package/src/clickhouse/entities.ts +356 -6
  111. package/src/clickhouse/function.test.ts +91 -0
  112. package/src/clickhouse/http.ts +11 -2
  113. package/src/clickhouse/import/generator.ts +81 -46
  114. package/src/clickhouse/import/import.test.ts +9 -3
  115. package/src/clickhouse/import/ir.ts +6 -2
  116. package/src/clickhouse/import/parser.ts +10 -18
  117. package/src/clickhouse/live/access.e2e.test.ts +218 -0
  118. package/src/clickhouse/live/bind.ts +17 -6
  119. package/src/clickhouse/live/catalog.ts +106 -10
  120. package/src/clickhouse/live/describe-resources.ts +23 -10
  121. package/src/clickhouse/live/dictionaries.e2e.test.ts +186 -0
  122. package/src/clickhouse/live/functions.e2e.test.ts +155 -0
  123. package/src/clickhouse/live/live.test.ts +2 -2
  124. package/src/clickhouse/parser.ts +136 -4
  125. package/src/clickhouse/plan/commands.ts +18 -7
  126. package/src/clickhouse/plan/deep.ts +71 -11
  127. package/src/clickhouse/plan/diff.ts +65 -2
  128. package/src/clickhouse/plan/normalize.ts +200 -6
  129. package/src/clickhouse/plan/report.ts +14 -3
  130. package/src/clickhouse/plan/rules.ts +44 -1
  131. package/src/clickhouse/plan/schema.ts +40 -17
  132. package/src/clickhouse/plan/server-format.ts +1 -1
  133. package/src/clickhouse/rebuild/backfill.ts +5 -25
  134. package/src/clickhouse/rebuild/observe.ts +7 -3
  135. package/src/clickhouse/rebuild/op.ts +9 -0
  136. package/src/clickhouse/rebuild/rebuild.e2e.test.ts +62 -0
  137. package/src/clickhouse/rebuild/rebuild.test.ts +54 -0
  138. package/src/clickhouse/rebuild/steps.ts +136 -1
  139. package/src/clickhouse/testing/fake-server.ts +2 -0
  140. package/src/clickhouse/testing/writable-server.ts +2 -0
  141. package/src/clickhouse/topology.ts +17 -2
  142. package/src/clickhouse.ts +19 -0
  143. package/src/codegen/classifier-page.ts +2 -0
  144. package/src/codegen/registry.ts +6 -0
  145. package/src/config.ts +12 -3
  146. package/src/core/references.ts +5 -3
  147. package/src/core/statements.ts +11 -1
  148. package/src/files/build-root.ts +62 -0
  149. package/src/files/clickhouse.ts +52 -0
  150. package/src/files/common.ts +54 -0
  151. package/src/files/postgres.ts +371 -0
  152. package/src/generated/lexicon-sql.json +30 -0
  153. package/src/import-parser.ts +61 -0
  154. package/src/index.ts +15 -2
  155. package/src/lint/rules/sqlch001.ts +26 -4
  156. package/src/lint/rules/sqlch002.ts +3 -2
  157. package/src/lint/rules/templates.ts +2 -2
  158. package/src/lsp/template.ts +3 -3
  159. package/src/mcp/index.ts +6 -6
  160. package/src/migration-statements.test.ts +25 -0
  161. package/src/migration-statements.ts +1 -0
  162. package/src/op/activities/clickhouse-rebuild.ts +1 -0
  163. package/src/op/activity-contracts.ts +1 -0
  164. package/src/plugin.ts +29 -8
  165. package/src/postgres/apply/constraint-sequences.e2e.test.ts +179 -0
  166. package/src/postgres/apply/safe-sequences.e2e.test.ts +176 -0
  167. package/src/postgres/apply/statements.test.ts +138 -6
  168. package/src/postgres/apply/statements.ts +156 -10
  169. package/src/postgres/import/generator.ts +118 -45
  170. package/src/postgres/live/bind.ts +17 -6
  171. package/src/postgres/live/client.ts +10 -2
  172. package/src/postgres/plan/plan.test.ts +2 -2
  173. package/src/postgres/plan/rules.ts +5 -5
  174. package/src/serializer.ts +14 -9
  175. package/src/skills/chant-sql-postgres-plan.md +3 -3
  176. package/src/skills/chant-sql-rebuild.md +3 -2
  177. package/src/skills/chant-sql.md +7 -1
  178. package/src/sql-files.test.ts +188 -0
  179. package/src/sql-files.ts +218 -0
  180. package/src/token-source.e2e.test.ts +142 -0
  181. package/src/token-source.test.ts +119 -0
  182. package/src/token-source.ts +189 -0
  183. package/src/validate.ts +6 -0
@@ -0,0 +1,119 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { sqlProfileSchema } from "./config";
3
+ import { credentialSource, tokenCommand, TokenSourceError, type RunCommand } from "./token-source";
4
+ import { resolvePostgresTarget, classifyPostgresFailure, isUnresolvedTarget } from "./postgres/live/bind";
5
+ import { resolveClickHouseTarget, classifyClickHouseFailure } from "./clickhouse/live/bind";
6
+
7
+ const RDS_URL = "postgres://db.abc123.eu-west-1.rds.amazonaws.com:5433/shop";
8
+
9
+ describe("tokenCommand", () => {
10
+ test("rds-iam runs aws rds generate-db-auth-token for the URL's host and port and the profile's user", () => {
11
+ expect(tokenCommand({ token: "rds-iam" }, { url: RDS_URL, user: "app", env: {} })).toEqual([
12
+ "aws", "rds", "generate-db-auth-token", "--hostname", "db.abc123.eu-west-1.rds.amazonaws.com", "--port", "5433", "--username", "app", "--region", "eu-west-1",
13
+ ]);
14
+ });
15
+
16
+ test("rds-iam takes the region from the config, then AWS_REGION, then the host name", () => {
17
+ const region = (argv: readonly string[]) => argv[argv.indexOf("--region") + 1];
18
+ expect(region(tokenCommand({ token: "rds-iam", region: "us-east-2" }, { url: RDS_URL, user: "app", env: { AWS_REGION: "ap-south-1" } }))).toBe("us-east-2");
19
+ expect(region(tokenCommand({ token: "rds-iam" }, { url: RDS_URL, user: "app", env: { AWS_REGION: "ap-south-1" } }))).toBe("ap-south-1");
20
+ const proxy = tokenCommand({ token: "rds-iam" }, { url: "postgres://db.internal/shop", user: "app", env: {} });
21
+ expect(proxy).not.toContain("--region");
22
+ expect(proxy[proxy.indexOf("--port") + 1]).toBe("5432");
23
+ });
24
+
25
+ test("rds-iam needs a user", () => {
26
+ expect(() => tokenCommand({ token: "rds-iam" }, { url: RDS_URL, env: {} })).toThrow(TokenSourceError);
27
+ });
28
+
29
+ test("cloud-sql-iam, entra and command", () => {
30
+ expect(tokenCommand({ token: "cloud-sql-iam" }, { url: "postgres://10.0.0.3/shop", env: {} })).toEqual(["gcloud", "sql", "generate-login-token"]);
31
+ expect(tokenCommand({ token: "entra" }, { url: "postgres://x.postgres.database.azure.com/shop", env: {} })).toEqual([
32
+ "az", "account", "get-access-token", "--resource-type", "oss-rdbms", "--query", "accessToken", "--output", "tsv",
33
+ ]);
34
+ expect(tokenCommand({ token: "command", command: ["./mint.sh", "prod"] }, { url: "http://ch:8123", env: {} })).toEqual(["./mint.sh", "prod"]);
35
+ });
36
+ });
37
+
38
+ describe("credentialSource", () => {
39
+ const counting = (outputs: string[]) => {
40
+ const calls: (readonly string[])[] = [];
41
+ const run: RunCommand = async (argv) => {
42
+ calls.push(argv);
43
+ return outputs[Math.min(calls.length - 1, outputs.length - 1)]!;
44
+ };
45
+ return { calls, run };
46
+ };
47
+
48
+ test("mints once, reuses the token, and mints again after most of its lifetime", async () => {
49
+ let now = 0;
50
+ const { calls, run } = counting(["one\n", "two\n"]);
51
+ const src = credentialSource({ token: "command", command: ["mint"], ttlSeconds: 100 }, { url: "http://ch:8123", env: {}, source: "sql.profiles.ci.password", run, now: () => now });
52
+ expect(src.source).toBe("sql.profiles.ci.password (command)");
53
+ expect(calls).toHaveLength(0);
54
+ expect(await src.get()).toBe("one");
55
+ now = 79_000;
56
+ expect(await src.get()).toBe("one");
57
+ now = 80_000;
58
+ expect(await src.get()).toBe("two");
59
+ expect(calls).toHaveLength(2);
60
+ });
61
+
62
+ test("invalidate forgets the token; requests at once share one mint", async () => {
63
+ const { calls, run } = counting(["a", "b"]);
64
+ const src = credentialSource({ token: "command", command: ["mint"] }, { url: "http://ch:8123", env: {}, source: "s", run });
65
+ expect(await Promise.all([src.get(), src.get(), src.get()])).toEqual(["a", "a", "a"]);
66
+ src.invalidate();
67
+ expect(await src.get()).toBe("b");
68
+ expect(calls).toHaveLength(2);
69
+ });
70
+
71
+ test("a failed or empty mint names the program, never its output", async () => {
72
+ const failing: RunCommand = async () => {
73
+ throw Object.assign(new Error("secret-ish stderr"), { code: 2 });
74
+ };
75
+ const missing: RunCommand = async () => {
76
+ throw Object.assign(new Error("spawn"), { code: "ENOENT" });
77
+ };
78
+ const opts = { url: RDS_URL, user: "app", env: {}, source: "sql.profiles.prod.password" };
79
+ const err = (await credentialSource({ token: "rds-iam" }, { ...opts, run: failing }).get().catch((e: unknown) => e)) as Error;
80
+ expect(err).toBeInstanceOf(TokenSourceError);
81
+ expect(err.message).toBe("sql.profiles.prod.password (rds-iam) could not mint a token: `aws` failed, it exited with 2");
82
+ await expect(credentialSource({ token: "entra" }, { ...opts, run: missing }).get()).rejects.toThrow("`az` failed, az is not installed");
83
+ await expect(credentialSource({ token: "command", command: ["x"] }, { ...opts, run: async () => " \n" }).get()).rejects.toThrow("printed nothing");
84
+ expect(classifyPostgresFailure(err)).toEqual({ reason: "no-credentials", detail: err.message });
85
+ expect(classifyClickHouseFailure(err).reason).toBe("no-credentials");
86
+ });
87
+ });
88
+
89
+ describe("profiles with a token source", () => {
90
+ test("the schema takes a token source or an env reference, and refuses an unknown one", () => {
91
+ expect(sqlProfileSchema.safeParse({ url: RDS_URL, password: { token: "rds-iam", region: "eu-west-1" } }).success).toBe(true);
92
+ expect(sqlProfileSchema.safeParse({ url: RDS_URL, password: { token: "command", command: ["./mint.sh"], ttlSeconds: 60 } }).success).toBe(true);
93
+ expect(sqlProfileSchema.safeParse({ url: RDS_URL, password: { env: "PW" } }).success).toBe(true);
94
+ expect(sqlProfileSchema.safeParse({ url: RDS_URL, password: { token: "vault" } }).success).toBe(false);
95
+ expect(sqlProfileSchema.safeParse({ url: RDS_URL, password: { token: "command", command: [] } }).success).toBe(false);
96
+ });
97
+
98
+ test("a Postgres profile binds a token source with the resolved user, and mints nothing yet", () => {
99
+ const config = { sql: { profiles: { prod: { url: RDS_URL, user: { env: "PG_USER" }, password: { token: "rds-iam" as const } } } } };
100
+ const target = resolvePostgresTarget({ environment: "prod", config, env: { PG_USER: "app" } });
101
+ if (isUnresolvedTarget(target)) throw new Error(target.detail);
102
+ expect(target.endpoint.user).toBe("app");
103
+ expect(target.endpoint.password).toBeUndefined();
104
+ expect(target.endpoint.token?.source).toBe("sql.profiles.prod.password (rds-iam)");
105
+ });
106
+
107
+ test("rds-iam with no user is no-credentials", () => {
108
+ const config = { sql: { profiles: { prod: { url: RDS_URL, password: { token: "rds-iam" as const } } } } };
109
+ expect(resolvePostgresTarget({ environment: "prod", config, env: {} })).toMatchObject({ reason: "no-credentials" });
110
+ });
111
+
112
+ test("ClickHouse takes a command source and refuses a cloud one", () => {
113
+ const command = { sql: { profiles: { ci: { url: "http://ch:8123", password: { token: "command" as const, command: ["mint"] } } } } };
114
+ const target = resolveClickHouseTarget({ environment: "ci", config: command, env: {} });
115
+ expect("endpoint" in target && target.endpoint.token?.source).toBe("sql.profiles.ci.password (command)");
116
+ const cloud = { sql: { profiles: { ci: { url: "http://ch:8123", password: { token: "cloud-sql-iam" as const } } } } };
117
+ expect(resolveClickHouseTarget({ environment: "ci", config: cloud, env: {} })).toMatchObject({ reason: "no-credentials" });
118
+ });
119
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Short-lived database credentials (#3685): a profile's `password` can name a
3
+ * token source instead of an environment variable, and the token is minted
4
+ * when a connection needs it.
5
+ *
6
+ * ```ts
7
+ * profiles: {
8
+ * prod: { url: "postgres://db.abc.us-east-1.rds.amazonaws.com:5432/shop", user: { env: "PG_USER" }, password: { token: "rds-iam" } },
9
+ * ci: { url: "postgres://db.internal:5432/shop", password: { token: "command", command: ["./mint-token.sh"], ttlSeconds: 600 } },
10
+ * }
11
+ * ```
12
+ *
13
+ * Each cloud source runs that cloud's own command line, which reads the job's
14
+ * ambient identity (an OIDC-federated role in CI, a workload identity, a
15
+ * logged-in developer), so chant holds no cloud SDK and no long-lived secret:
16
+ *
17
+ * - `rds-iam`: `aws rds generate-db-auth-token`, for RDS and Aurora. The host
18
+ * and port come from the profile's URL, the user from its `user`, the region
19
+ * from `region`, else `AWS_REGION`, else the RDS host name.
20
+ * - `cloud-sql-iam`: `gcloud sql generate-login-token`.
21
+ * - `entra`: `az account get-access-token` for Azure Database for PostgreSQL.
22
+ * - `command`: any command that prints a password on its standard output.
23
+ *
24
+ * A token is reused until most of its lifetime has passed and minted again
25
+ * after, so a long apply, whose ClickHouse requests each carry the password
26
+ * and whose Postgres connections each authenticate when opened, never sends
27
+ * an expired one. A token is never written to a message, an outcome or a log.
28
+ */
29
+
30
+ import { execFile } from "node:child_process";
31
+ import { z } from "zod";
32
+
33
+ const ttl = z.number().int().positive().optional();
34
+
35
+ /** The token sources a profile's `password` can name. */
36
+ export const tokenSourceSchema = z.discriminatedUnion("token", [
37
+ z.strictObject({
38
+ token: z.literal("rds-iam"),
39
+ /** The AWS region. `AWS_REGION`, `AWS_DEFAULT_REGION`, else the RDS host name's, when omitted. */
40
+ region: z.string().min(1).optional(),
41
+ /** How long a token is valid, in seconds. 900, the RDS lifetime, when omitted. */
42
+ ttlSeconds: ttl,
43
+ }),
44
+ z.strictObject({
45
+ token: z.literal("cloud-sql-iam"),
46
+ /** How long a token is valid, in seconds. 3600 when omitted. */
47
+ ttlSeconds: ttl,
48
+ }),
49
+ z.strictObject({
50
+ token: z.literal("entra"),
51
+ /** How long a token is valid, in seconds. 3600 when omitted. */
52
+ ttlSeconds: ttl,
53
+ }),
54
+ z.strictObject({
55
+ token: z.literal("command"),
56
+ /** The program and its arguments, run without a shell; its standard output, trimmed, is the password. */
57
+ command: z.array(z.string().min(1)).min(1),
58
+ /** How long a token is valid, in seconds. 300 when omitted. */
59
+ ttlSeconds: ttl,
60
+ }),
61
+ ]);
62
+
63
+ export type TokenSourceConfig = z.infer<typeof tokenSourceSchema>;
64
+
65
+ /** The cloud sources, which mint for a Postgres server only. */
66
+ export const POSTGRES_ONLY_TOKEN_SOURCES: ReadonlySet<string> = new Set(["rds-iam", "cloud-sql-iam", "entra"]);
67
+
68
+ const DEFAULT_TTL_SECONDS: Record<TokenSourceConfig["token"], number> = {
69
+ "rds-iam": 900,
70
+ "cloud-sql-iam": 3600,
71
+ entra: 3600,
72
+ command: 300,
73
+ };
74
+
75
+ /** A token reused past this share of its lifetime is minted again. */
76
+ const REUSE_FRACTION = 0.8;
77
+
78
+ /** A token source that could not mint. The message names the source and the command, never what it printed. */
79
+ export class TokenSourceError extends Error {
80
+ constructor(message: string) {
81
+ super(message);
82
+ this.name = "TokenSourceError";
83
+ }
84
+ }
85
+
86
+ /** Runs a program without a shell and returns its standard output. */
87
+ export type RunCommand = (argv: readonly string[], env: Record<string, string | undefined>) => Promise<string>;
88
+
89
+ const runCommand: RunCommand = (argv, env) =>
90
+ new Promise((resolve, reject) => {
91
+ execFile(argv[0]!, argv.slice(1), { env: env as NodeJS.ProcessEnv, timeout: 60_000, maxBuffer: 1 << 20 }, (err, stdout) => {
92
+ if (err) reject(err);
93
+ else resolve(String(stdout));
94
+ });
95
+ });
96
+
97
+ /** Where a token is minted, and the cached token. */
98
+ export interface CredentialSource {
99
+ /** What the token is minted by, for a message: `sql.profiles.prod.password (rds-iam)`. */
100
+ readonly source: string;
101
+ /** A valid token: the cached one, or a new one when it has expired. */
102
+ get(): Promise<string>;
103
+ /** Forget the cached token, so the next `get()` mints again. */
104
+ invalidate(): void;
105
+ }
106
+
107
+ /** The program a source runs. Pure, for tests. */
108
+ export function tokenCommand(
109
+ config: TokenSourceConfig,
110
+ input: { url: string; user?: string; env: Record<string, string | undefined> },
111
+ ): readonly string[] {
112
+ switch (config.token) {
113
+ case "command":
114
+ return config.command;
115
+ case "cloud-sql-iam":
116
+ return ["gcloud", "sql", "generate-login-token"];
117
+ case "entra":
118
+ return ["az", "account", "get-access-token", "--resource-type", "oss-rdbms", "--query", "accessToken", "--output", "tsv"];
119
+ case "rds-iam": {
120
+ const url = new URL(input.url);
121
+ if (input.user === undefined) throw new TokenSourceError("rds-iam mints a token for one database user; the profile names no user");
122
+ const region = config.region ?? input.env.AWS_REGION ?? input.env.AWS_DEFAULT_REGION ?? /\.([a-z]{2}(?:-[a-z]+)+-\d+)\.rds\.amazonaws\.com$/.exec(url.hostname)?.[1];
123
+ return [
124
+ "aws",
125
+ "rds",
126
+ "generate-db-auth-token",
127
+ "--hostname",
128
+ url.hostname,
129
+ "--port",
130
+ url.port || "5432",
131
+ "--username",
132
+ input.user,
133
+ ...(region !== undefined ? ["--region", region] : []),
134
+ ];
135
+ }
136
+ }
137
+ }
138
+
139
+ /**
140
+ * A source that mints with `config` when asked. Nothing runs until `get()`.
141
+ * `now` and `run` are for tests.
142
+ */
143
+ export function credentialSource(
144
+ config: TokenSourceConfig,
145
+ input: {
146
+ url: string;
147
+ user?: string;
148
+ env: Record<string, string | undefined>;
149
+ source: string;
150
+ run?: RunCommand;
151
+ now?: () => number;
152
+ },
153
+ ): CredentialSource {
154
+ const run = input.run ?? runCommand;
155
+ const now = input.now ?? Date.now;
156
+ const lifetimeMs = (config.ttlSeconds ?? DEFAULT_TTL_SECONDS[config.token]) * 1000 * REUSE_FRACTION;
157
+ const source = `${input.source} (${config.token})`;
158
+ let cached: { token: string; until: number } | undefined;
159
+ let pending: Promise<string> | undefined;
160
+ const mint = async (): Promise<string> => {
161
+ const argv = tokenCommand(config, input);
162
+ let out: string;
163
+ try {
164
+ out = await run(argv, input.env);
165
+ } catch (err) {
166
+ const e = err as { code?: unknown; stderr?: unknown };
167
+ const why = e.code === "ENOENT" ? `${argv[0]} is not installed` : `it exited with ${String(e.code ?? "an error")}`;
168
+ throw new TokenSourceError(`${source} could not mint a token: \`${argv[0]}\` failed, ${why}`);
169
+ }
170
+ const token = out.trim();
171
+ if (token === "") throw new TokenSourceError(`${source} could not mint a token: \`${argv[0]}\` printed nothing`);
172
+ cached = { token, until: now() + lifetimeMs };
173
+ return token;
174
+ };
175
+ return {
176
+ source,
177
+ async get() {
178
+ if (cached && now() < cached.until) return cached.token;
179
+ // Requests that ask at once share one mint.
180
+ pending ??= mint().finally(() => {
181
+ pending = undefined;
182
+ });
183
+ return pending;
184
+ },
185
+ invalidate() {
186
+ cached = undefined;
187
+ },
188
+ };
189
+ }
package/src/validate.ts CHANGED
@@ -25,6 +25,12 @@ const REQUIRED_ENTITIES = [
25
25
  "Table",
26
26
  "View",
27
27
  "MaterializedView",
28
+ "Dictionary",
29
+ "Function",
30
+ "User",
31
+ "Role",
32
+ "RowPolicy",
33
+ "Grant",
28
34
  "PostgresSchema",
29
35
  "PostgresTable",
30
36
  "PostgresIndex",