@alienplatform/sdk 1.14.1 → 2.0.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 (160) hide show
  1. package/dist/dist.js +371 -275
  2. package/dist/index.d.ts +22 -28
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -14356
  5. package/dist/native.d.ts +26 -0
  6. package/dist/native.d.ts.map +1 -0
  7. package/dist/native.js +2 -0
  8. package/dist/worker-runtime/channel.d.ts +25 -0
  9. package/dist/worker-runtime/channel.d.ts.map +1 -0
  10. package/dist/worker-runtime/errors.d.ts +79 -0
  11. package/dist/worker-runtime/errors.d.ts.map +1 -0
  12. package/dist/worker-runtime/event-loop.d.ts +43 -0
  13. package/dist/worker-runtime/event-loop.d.ts.map +1 -0
  14. package/dist/{generated → worker-runtime/generated}/control.d.ts +15 -15
  15. package/dist/worker-runtime/generated/control.d.ts.map +1 -0
  16. package/dist/worker-runtime/generated/google/protobuf/duration.d.ts.map +1 -0
  17. package/dist/worker-runtime/generated/google/protobuf/timestamp.d.ts.map +1 -0
  18. package/dist/{generated → worker-runtime/generated}/wait_until.d.ts +29 -29
  19. package/dist/worker-runtime/generated/wait_until.d.ts.map +1 -0
  20. package/dist/worker-runtime/grpc-utils.d.ts +23 -0
  21. package/dist/worker-runtime/grpc-utils.d.ts.map +1 -0
  22. package/dist/worker-runtime/index.d.ts +33 -0
  23. package/dist/worker-runtime/index.d.ts.map +1 -0
  24. package/dist/{src.js → worker-runtime/index.js} +9370 -4035
  25. package/dist/worker-runtime/registry.d.ts +169 -0
  26. package/dist/worker-runtime/registry.d.ts.map +1 -0
  27. package/dist/worker-runtime/service-definitions.d.ts +12 -0
  28. package/dist/worker-runtime/service-definitions.d.ts.map +1 -0
  29. package/dist/worker-runtime/wait-until-manager.d.ts +34 -0
  30. package/dist/worker-runtime/wait-until-manager.d.ts.map +1 -0
  31. package/package.json +18 -38
  32. package/.turbo/turbo-build.log +0 -30
  33. package/AGENTS.md +0 -70
  34. package/dist/bearerTokenAuthenticationPolicy.js +0 -3028
  35. package/dist/bindings/artifact-registry.d.ts +0 -88
  36. package/dist/bindings/artifact-registry.d.ts.map +0 -1
  37. package/dist/bindings/build.d.ts +0 -67
  38. package/dist/bindings/build.d.ts.map +0 -1
  39. package/dist/bindings/index.d.ts +0 -13
  40. package/dist/bindings/index.d.ts.map +0 -1
  41. package/dist/bindings/kv.d.ts +0 -113
  42. package/dist/bindings/kv.d.ts.map +0 -1
  43. package/dist/bindings/postgres.d.ts +0 -90
  44. package/dist/bindings/postgres.d.ts.map +0 -1
  45. package/dist/bindings/postgres.test.d.ts +0 -2
  46. package/dist/bindings/postgres.test.d.ts.map +0 -1
  47. package/dist/bindings/queue.d.ts +0 -81
  48. package/dist/bindings/queue.d.ts.map +0 -1
  49. package/dist/bindings/service-account.d.ts +0 -46
  50. package/dist/bindings/service-account.d.ts.map +0 -1
  51. package/dist/bindings/storage.d.ts +0 -164
  52. package/dist/bindings/storage.d.ts.map +0 -1
  53. package/dist/bindings/vault.d.ts +0 -65
  54. package/dist/bindings/vault.d.ts.map +0 -1
  55. package/dist/bindings/worker.d.ts +0 -72
  56. package/dist/bindings/worker.d.ts.map +0 -1
  57. package/dist/channel.d.ts +0 -39
  58. package/dist/channel.d.ts.map +0 -1
  59. package/dist/checksum.js +0 -6176
  60. package/dist/commands/client.d.ts +0 -47
  61. package/dist/commands/client.d.ts.map +0 -1
  62. package/dist/commands/errors.d.ts +0 -198
  63. package/dist/commands/errors.d.ts.map +0 -1
  64. package/dist/commands/index.d.ts +0 -23
  65. package/dist/commands/index.d.ts.map +0 -1
  66. package/dist/commands/index.js +0 -387
  67. package/dist/commands/types.d.ts +0 -40
  68. package/dist/commands/types.d.ts.map +0 -1
  69. package/dist/commands.d.ts +0 -55
  70. package/dist/commands.d.ts.map +0 -1
  71. package/dist/context.d.ts +0 -182
  72. package/dist/context.d.ts.map +0 -1
  73. package/dist/dist-cjs.js +0 -15258
  74. package/dist/dist2.js +0 -1239
  75. package/dist/errors.d.ts +0 -359
  76. package/dist/errors.d.ts.map +0 -1
  77. package/dist/es5.js +0 -1720
  78. package/dist/esm.js +0 -18068
  79. package/dist/esm2.js +0 -3166
  80. package/dist/event-streams.js +0 -770
  81. package/dist/events.d.ts +0 -163
  82. package/dist/events.d.ts.map +0 -1
  83. package/dist/generated/artifact_registry.d.ts +0 -302
  84. package/dist/generated/artifact_registry.d.ts.map +0 -1
  85. package/dist/generated/build.d.ts +0 -184
  86. package/dist/generated/build.d.ts.map +0 -1
  87. package/dist/generated/container.d.ts +0 -101
  88. package/dist/generated/container.d.ts.map +0 -1
  89. package/dist/generated/control.d.ts.map +0 -1
  90. package/dist/generated/google/protobuf/duration.d.ts.map +0 -1
  91. package/dist/generated/google/protobuf/timestamp.d.ts.map +0 -1
  92. package/dist/generated/kv.d.ts +0 -182
  93. package/dist/generated/kv.d.ts.map +0 -1
  94. package/dist/generated/queue.d.ts +0 -127
  95. package/dist/generated/queue.d.ts.map +0 -1
  96. package/dist/generated/service_account.d.ts +0 -112
  97. package/dist/generated/service_account.d.ts.map +0 -1
  98. package/dist/generated/storage.d.ts +0 -784
  99. package/dist/generated/storage.d.ts.map +0 -1
  100. package/dist/generated/vault.d.ts +0 -107
  101. package/dist/generated/vault.d.ts.map +0 -1
  102. package/dist/generated/wait_until.d.ts.map +0 -1
  103. package/dist/generated/worker.d.ts +0 -107
  104. package/dist/generated/worker.d.ts.map +0 -1
  105. package/dist/global.d.ts +0 -208
  106. package/dist/global.d.ts.map +0 -1
  107. package/dist/grpc-utils.d.ts +0 -25
  108. package/dist/grpc-utils.d.ts.map +0 -1
  109. package/dist/jws.js +0 -611
  110. package/dist/open.js +0 -461
  111. package/dist/rolldown-runtime.js +0 -44
  112. package/dist/src2.js +0 -114032
  113. package/dist/types.d.ts +0 -337
  114. package/dist/types.d.ts.map +0 -1
  115. package/dist/wait-until.d.ts +0 -115
  116. package/dist/wait-until.d.ts.map +0 -1
  117. package/scripts/generate-proto.sh +0 -46
  118. package/src/bindings/AGENTS.md +0 -105
  119. package/src/bindings/artifact-registry.ts +0 -317
  120. package/src/bindings/build.ts +0 -195
  121. package/src/bindings/index.ts +0 -13
  122. package/src/bindings/kv.ts +0 -240
  123. package/src/bindings/postgres.test.ts +0 -17
  124. package/src/bindings/postgres.ts +0 -446
  125. package/src/bindings/queue.ts +0 -191
  126. package/src/bindings/service-account.ts +0 -113
  127. package/src/bindings/storage.ts +0 -536
  128. package/src/bindings/vault.ts +0 -133
  129. package/src/bindings/worker.ts +0 -164
  130. package/src/channel.ts +0 -102
  131. package/src/commands/client.ts +0 -446
  132. package/src/commands/errors.ts +0 -126
  133. package/src/commands/index.ts +0 -41
  134. package/src/commands/types.ts +0 -52
  135. package/src/commands.ts +0 -76
  136. package/src/context.ts +0 -368
  137. package/src/errors.ts +0 -280
  138. package/src/events.ts +0 -511
  139. package/src/generated/artifact_registry.ts +0 -2096
  140. package/src/generated/build.ts +0 -1311
  141. package/src/generated/container.ts +0 -503
  142. package/src/generated/control.ts +0 -2018
  143. package/src/generated/google/protobuf/duration.ts +0 -196
  144. package/src/generated/google/protobuf/timestamp.ts +0 -226
  145. package/src/generated/kv.ts +0 -1169
  146. package/src/generated/queue.ts +0 -753
  147. package/src/generated/service_account.ts +0 -814
  148. package/src/generated/storage.ts +0 -3820
  149. package/src/generated/vault.ts +0 -543
  150. package/src/generated/wait_until.ts +0 -829
  151. package/src/generated/worker.ts +0 -759
  152. package/src/global.ts +0 -287
  153. package/src/grpc-utils.ts +0 -159
  154. package/src/index.ts +0 -160
  155. package/src/types.ts +0 -397
  156. package/src/wait-until.ts +0 -273
  157. package/tsconfig.json +0 -8
  158. package/tsdown.config.ts +0 -17
  159. /package/dist/{generated → worker-runtime/generated}/google/protobuf/duration.d.ts +0 -0
  160. /package/dist/{generated → worker-runtime/generated}/google/protobuf/timestamp.d.ts +0 -0
@@ -1,240 +0,0 @@
1
- /**
2
- * KV binding implementation.
3
- *
4
- * Provides key-value storage operations with TTL support.
5
- */
6
-
7
- import { type Channel, createClient } from "nice-grpc"
8
- import {
9
- type KvServiceClient as GeneratedKvServiceClient,
10
- KvServiceDefinition,
11
- } from "../generated/kv.js"
12
- import { wrapGrpcCall } from "../grpc-utils.js"
13
- import type { KvPutOptions, KvScanResult } from "../types.js"
14
-
15
- /**
16
- * KV binding for key-value storage operations.
17
- *
18
- * @example
19
- * ```typescript
20
- * import { kv } from "@alienplatform/sdk"
21
- *
22
- * const cache = kv("my-cache")
23
- *
24
- * // Set a value
25
- * await cache.set("user:123", { name: "John", email: "john@example.com" })
26
- *
27
- * // Get a value
28
- * const user = await cache.getJson<User>("user:123")
29
- *
30
- * // Set with TTL (expires in 1 hour)
31
- * await cache.set("session:abc", sessionData, { ttlMs: 60 * 60 * 1000 })
32
- *
33
- * // Delete
34
- * await cache.delete("user:123")
35
- *
36
- * // Scan by prefix
37
- * for await (const { key, value } of cache.scan("user:")) {
38
- * console.log(key, value)
39
- * }
40
- * ```
41
- */
42
- export class Kv {
43
- private readonly client: GeneratedKvServiceClient
44
- private readonly bindingName: string
45
-
46
- constructor(channel: Channel, bindingName: string) {
47
- this.client = createClient(KvServiceDefinition, channel)
48
- this.bindingName = bindingName
49
- }
50
-
51
- /**
52
- * Get a value by key.
53
- *
54
- * @param key - Key to retrieve
55
- * @returns Value as Uint8Array, or undefined if not found
56
- */
57
- async get(key: string): Promise<Uint8Array | undefined> {
58
- return await wrapGrpcCall(
59
- "KvService",
60
- "Get",
61
- async () => {
62
- const response = await this.client.get({
63
- bindingName: this.bindingName,
64
- key,
65
- })
66
- return response.value
67
- },
68
- { bindingName: this.bindingName, key },
69
- )
70
- }
71
-
72
- /**
73
- * Get a value as a UTF-8 string.
74
- *
75
- * @param key - Key to retrieve
76
- * @returns Value as string, or undefined if not found
77
- */
78
- async getText(key: string): Promise<string | undefined> {
79
- const value = await this.get(key)
80
- return value ? new TextDecoder().decode(value) : undefined
81
- }
82
-
83
- /**
84
- * Get a value and parse as JSON.
85
- *
86
- * @param key - Key to retrieve
87
- * @returns Parsed JSON value, or undefined if not found
88
- */
89
- async getJson<T = unknown>(key: string): Promise<T | undefined> {
90
- const text = await this.getText(key)
91
- return text ? (JSON.parse(text) as T) : undefined
92
- }
93
-
94
- /**
95
- * Set a value.
96
- *
97
- * @param key - Key to store
98
- * @param value - Value to store (string, Uint8Array, or object for JSON)
99
- * @param options - Optional put options (TTL, ifNotExists)
100
- * @returns True if stored, false if ifNotExists was true and key already exists
101
- */
102
- async set(
103
- key: string,
104
- value: string | Uint8Array | object,
105
- options?: KvPutOptions,
106
- ): Promise<boolean> {
107
- let bytes: Uint8Array
108
-
109
- if (typeof value === "string") {
110
- bytes = new TextEncoder().encode(value)
111
- } else if (value instanceof Uint8Array) {
112
- bytes = value
113
- } else {
114
- bytes = new TextEncoder().encode(JSON.stringify(value))
115
- }
116
-
117
- return await wrapGrpcCall(
118
- "KvService",
119
- "Put",
120
- async () => {
121
- const response = await this.client.put({
122
- bindingName: this.bindingName,
123
- key,
124
- value: bytes,
125
- options: options
126
- ? {
127
- ttlSeconds: options.ttlMs ? Math.floor(options.ttlMs / 1000) : undefined,
128
- ifNotExists: options.ifNotExists ?? false,
129
- }
130
- : undefined,
131
- })
132
- return response.success
133
- },
134
- { bindingName: this.bindingName, key },
135
- )
136
- }
137
-
138
- /**
139
- * Delete a key.
140
- *
141
- * @param key - Key to delete
142
- */
143
- async delete(key: string): Promise<void> {
144
- await wrapGrpcCall(
145
- "KvService",
146
- "Delete",
147
- async () => {
148
- await this.client.delete({
149
- bindingName: this.bindingName,
150
- key,
151
- })
152
- },
153
- { bindingName: this.bindingName, key },
154
- )
155
- }
156
-
157
- /**
158
- * Check if a key exists.
159
- *
160
- * @param key - Key to check
161
- * @returns True if the key exists
162
- */
163
- async exists(key: string): Promise<boolean> {
164
- return await wrapGrpcCall(
165
- "KvService",
166
- "Exists",
167
- async () => {
168
- const response = await this.client.exists({
169
- bindingName: this.bindingName,
170
- key,
171
- })
172
- return response.exists
173
- },
174
- { bindingName: this.bindingName, key },
175
- )
176
- }
177
-
178
- /**
179
- * Scan keys with a prefix.
180
- *
181
- * @param prefix - Prefix to scan for
182
- * @param options - Optional scan options (limit)
183
- * @returns Async iterable of key-value pairs
184
- *
185
- * @example
186
- * ```typescript
187
- * // Scan all users
188
- * for await (const { key, value } of cache.scan("user:")) {
189
- * console.log(key, new TextDecoder().decode(value))
190
- * }
191
- * ```
192
- */
193
- async *scan(
194
- prefix: string,
195
- options?: { limit?: number },
196
- ): AsyncIterable<{ key: string; value: Uint8Array }> {
197
- let cursor: string | undefined
198
-
199
- do {
200
- const result = await this.scanPage(prefix, options?.limit, cursor)
201
-
202
- for (const item of result.items) {
203
- yield item
204
- }
205
-
206
- cursor = result.nextCursor
207
- } while (cursor)
208
- }
209
-
210
- /**
211
- * Scan a single page of results.
212
- *
213
- * @param prefix - Prefix to scan for
214
- * @param limit - Maximum items to return
215
- * @param cursor - Pagination cursor
216
- * @returns Scan result with items and next cursor
217
- */
218
- async scanPage(prefix: string, limit?: number, cursor?: string): Promise<KvScanResult> {
219
- return await wrapGrpcCall(
220
- "KvService",
221
- "ScanPrefix",
222
- async () => {
223
- const response = await this.client.scanPrefix({
224
- bindingName: this.bindingName,
225
- prefix,
226
- limit,
227
- cursor,
228
- })
229
- return {
230
- items: response.items.map(item => ({
231
- key: item.key,
232
- value: item.value,
233
- })),
234
- nextCursor: response.nextCursor,
235
- }
236
- },
237
- { bindingName: this.bindingName },
238
- )
239
- }
240
- }
@@ -1,17 +0,0 @@
1
- import { describe, expect, it } from "vitest"
2
-
3
- import { encodeUserinfo } from "./postgres.js"
4
-
5
- // The TS `encodeUserinfo` must byte-for-byte match the Rust resolver's `encode_userinfo`
6
- // (crates/alien-bindings/src/providers/postgres/local.rs), or the two runtimes produce mismatched
7
- // connection strings for the same binding. These are the exact vectors the Rust tests pin, so a
8
- // drift in either encoder fails on one side.
9
- describe("encodeUserinfo", () => {
10
- it("percent-encodes the RFC 3986 reserved characters like the Rust resolver", () => {
11
- expect(encodeUserinfo("p@ss/word")).toBe("p%40ss%2Fword")
12
- })
13
-
14
- it("percent-encodes the sub-delims encodeURIComponent leaves literal (! * ' ( ))", () => {
15
- expect(encodeUserinfo("a!b*c'd(e)f")).toBe("a%21b%2Ac%27d%28e%29f")
16
- })
17
- })
@@ -1,446 +0,0 @@
1
- /**
2
- * Postgres binding accessor.
3
- *
4
- * Postgres is connection-only: unlike Storage/Kv/Queue there is no gRPC service to call,
5
- * so this module does not use {@link AlienContext}. Instead the manager injects the
6
- * connection details for a linked database into `ALIEN_<NAME>_BINDING` as JSON, and the
7
- * workload turns them into a live connection here.
8
- *
9
- * The cloud variants deliberately carry only a *reference* to the connection password
10
- * (Secrets Manager ARN / Secret Manager name / Key Vault URI), never the password itself.
11
- * The workload resolves that reference at runtime using its own least-privilege identity
12
- * — exactly what the `postgres/data-access` permission set grants — so the password never
13
- * flows through the control plane or sits as a plaintext environment variable.
14
- *
15
- * The matching cloud secret SDK is imported lazily, so a workload only loads the SDK for
16
- * the cloud it is actually running on.
17
- */
18
-
19
- import { AlienError } from "@alienplatform/core"
20
- import * as z from "zod/v4"
21
- import {
22
- BindingNotFoundError,
23
- InvalidBindingConfigError,
24
- PostgresSecretResolutionError,
25
- } from "../errors.js"
26
-
27
- /**
28
- * Everything a Postgres driver needs to connect. `ssl` is the node-postgres TLS value; for
29
- * node-postgres pass it with the individual fields, not the connectionString (see the `ssl` note).
30
- * It is a plain value any driver can read.
31
- */
32
- export interface PostgresConnection {
33
- /**
34
- * `postgres://user:password@host:port/database?sslmode=<mode>`, with credentials
35
- * percent-encoded. The `sslmode` mirrors the Rust resolver byte-for-byte (`disable`
36
- * local, `prefer` external, `require` cloud) so a sslmode-aware consumer agrees with us.
37
- */
38
- connectionString: string
39
- /**
40
- * The TLS setting for the driver: `false` for plaintext-capable bindings (local/external),
41
- * `{ rejectUnauthorized: false }` for managed clouds (TLS on, cert not yet verified against a
42
- * pinned CA).
43
- *
44
- * For node-postgres, pass this with the individual fields — `new Client({ host, port, ..., ssl })`
45
- * — NOT with the connectionString. node-postgres parses the URL's `sslmode` (it treats `require`
46
- * as `verify-full`) and that overwrites an explicit `ssl`, so the URL form rejects the cloud cert
47
- * with "unable to get local issuer certificate". Field style lets this value take effect.
48
- *
49
- * `rejectUnauthorized: false` is a deliberate v1 posture: Postgres is private-only on every cloud
50
- * (no public IP, reachable only from same-stack workloads), so the network boundary is the primary
51
- * control and an on-path MITM would already be inside the stack VPC. Verified TLS (provider CAs) is
52
- * future defense-in-depth, not a v1 requirement.
53
- *
54
- * For the `external` (BYO) backend this is always `false`: node-postgres has no `prefer` mode, so
55
- * the field stays plaintext while the connection string's `sslmode=prefer` only reaches sslmode-aware
56
- * consumers (psql). A BYO database that *requires* TLS must have TLS configured outside this struct.
57
- */
58
- ssl: false | { rejectUnauthorized: boolean }
59
- /**
60
- * The same connection details as individual fields, for drivers that take them separately
61
- * (`new Client({ host, port, user: username, password, database })`) instead of a URL. These are
62
- * the un-encoded values that `connectionString` percent-encodes; `host` is the address to dial
63
- * (the Aurora cluster endpoint for the managed-AWS backend).
64
- */
65
- host: string
66
- port: number
67
- database: string
68
- username: string
69
- password: string
70
- }
71
-
72
- const connectionFields = {
73
- // Fail fast at the trust boundary: reject any value that can't form a connectable URL rather than
74
- // pass it on. Port 0 is never dialable (the Rust `u16` domain allows it, so `.min(1)` is one
75
- // stricter); `database`/`username` likewise reject the empty string (Postgres has no empty
76
- // identifier, and `postgres://:p@h/` is unconnectable).
77
- port: z.number().int().min(1).max(65535),
78
- database: z.string().min(1),
79
- username: z.string().min(1),
80
- }
81
-
82
- /**
83
- * Mirror of the Rust `PostgresBinding` enum (tagged by `service`, camelCase fields).
84
- * `BindingValue<T>` is serialized untagged, so concrete values arrive as plain scalars.
85
- */
86
- // `.strict()` on every variant: this binding is the control-plane → workload trust boundary, so an
87
- // unexpected key (a Rust-side rename the SDK doesn't yet know, or env tampering) should fail loudly
88
- // here rather than being silently dropped. This is the template other connection bindings will copy.
89
- const postgresBindingSchema = z.discriminatedUnion("service", [
90
- z
91
- .object({
92
- service: z.literal("aurora"),
93
- clusterEndpoint: z.string(),
94
- passwordSecretArn: z.string(),
95
- ...connectionFields,
96
- })
97
- .strict(),
98
- z
99
- .object({
100
- service: z.literal("cloud-sql"),
101
- host: z.string(),
102
- passwordSecretName: z.string(),
103
- ...connectionFields,
104
- })
105
- .strict(),
106
- z
107
- .object({
108
- service: z.literal("flexible-server"),
109
- host: z.string(),
110
- passwordSecretUri: z.string(),
111
- ...connectionFields,
112
- })
113
- .strict(),
114
- z
115
- .object({
116
- service: z.literal("external"),
117
- host: z.string(),
118
- password: z.string().min(1),
119
- ...connectionFields,
120
- })
121
- .strict(),
122
- z
123
- .object({
124
- service: z.literal("local-postgres"),
125
- host: z.string(),
126
- password: z.string().min(1),
127
- ...connectionFields,
128
- })
129
- .strict(),
130
- ])
131
-
132
- /** Matches the Rust `binding_env_var_name`: `ALIEN_<NAME>_BINDING`, hyphens to underscores. */
133
- function bindingEnvVarName(bindingName: string): string {
134
- return `ALIEN_${bindingName.replace(/-/g, "_").toUpperCase()}_BINDING`
135
- }
136
-
137
- /** Mirrors the Rust `SslMode` query param so the two resolvers emit identical URLs. */
138
- type SslMode = "disable" | "prefer" | "require"
139
-
140
- // Percent-encode a userinfo/path component to the RFC 3986 *unreserved* set only, byte-for-byte
141
- // matching the Rust resolver's `encode_userinfo`. `encodeURIComponent` leaves the sub-delims
142
- // ! ' ( ) * literal; encode them too so a password containing any of them produces the same
143
- // connection string on the TS and Rust runtimes (a generated password can include them).
144
- export function encodeUserinfo(value: string): string {
145
- return encodeURIComponent(value).replace(
146
- /[!'()*]/g,
147
- c => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`,
148
- )
149
- }
150
-
151
- function connectionString(
152
- host: string,
153
- params: { port: number; database: string; username: string },
154
- password: string,
155
- sslmode: SslMode,
156
- ): string {
157
- const user = encodeUserinfo(params.username)
158
- const pass = encodeUserinfo(password)
159
- return `postgres://${user}:${pass}@${host}:${params.port}/${encodeUserinfo(params.database)}?sslmode=${sslmode}`
160
- }
161
-
162
- /** Builds the full connection result — the URL plus the same details as individual fields. */
163
- function makeConnection(
164
- host: string,
165
- params: { port: number; database: string; username: string },
166
- password: string,
167
- sslmode: SslMode,
168
- ssl: PostgresConnection["ssl"],
169
- ): PostgresConnection {
170
- return {
171
- connectionString: connectionString(host, params, password, sslmode),
172
- ssl,
173
- host,
174
- port: params.port,
175
- database: params.database,
176
- username: params.username,
177
- password,
178
- }
179
- }
180
-
181
- /**
182
- * Resolves the connection details for a linked Postgres database.
183
- *
184
- * @param bindingName The resource id used when the database was linked (e.g. the value
185
- * passed to `new alien.Postgres(id)`).
186
- * @returns A connection string plus `ssl`, and the same details as individual fields
187
- * (`host`/`port`/`database`/`username`/`password`) — use whichever style your driver prefers.
188
- * @throws AlienError if the binding env var is missing, malformed, or names a backend
189
- * whose secret resolution is not available in this build.
190
- *
191
- * @example
192
- * ```typescript
193
- * import { Client } from "pg"
194
- * import { getPostgresConnection } from "@alienplatform/sdk"
195
- *
196
- * const conn = await getPostgresConnection("my-db")
197
- * // Field style: node-postgres parses a URL's sslmode and would override `ssl`, so pass fields.
198
- * const client = new Client({
199
- * host: conn.host, port: conn.port, database: conn.database,
200
- * user: conn.username, password: conn.password, ssl: conn.ssl,
201
- * })
202
- * await client.connect()
203
- * ```
204
- */
205
- export async function getPostgresConnection(bindingName: string): Promise<PostgresConnection> {
206
- const envVar = bindingEnvVarName(bindingName)
207
- const raw = process.env[envVar]
208
- if (!raw) {
209
- // A missing binding env var means the workload didn't link a Postgres by this name — a
210
- // user-fixable config problem (404), not an internal failure.
211
- throw new AlienError(
212
- BindingNotFoundError.create({
213
- bindingName,
214
- bindingType: "Postgres",
215
- }),
216
- )
217
- }
218
-
219
- let parsed: unknown
220
- try {
221
- parsed = JSON.parse(raw)
222
- } catch (cause) {
223
- throw (await AlienError.from(cause)).withContext(
224
- InvalidBindingConfigError.create({
225
- message: `Postgres binding '${bindingName}' is not valid JSON`,
226
- }),
227
- )
228
- }
229
-
230
- const result = postgresBindingSchema.safeParse(parsed)
231
- if (!result.success) {
232
- // Carry the Zod detail only in the wrapped internal source; the user-facing
233
- // InvalidBindingConfigError is `internal:false` and would leak it externally.
234
- throw (await AlienError.from(result.error)).withContext(
235
- InvalidBindingConfigError.create({
236
- message: `Postgres binding '${bindingName}' has an unexpected shape`,
237
- }),
238
- )
239
- }
240
-
241
- const binding = result.data
242
- switch (binding.service) {
243
- case "local-postgres":
244
- return makeConnection(binding.host, binding, binding.password, "disable", false)
245
- case "external":
246
- // node-postgres has no `prefer` mode: `ssl: false` always connects plaintext. We carry
247
- // `sslmode=prefer` in the URL for sslmode-aware consumers (psql), so against a server that
248
- // offers-but-doesn't-require TLS the two diverge (psql upgrades, pg stays plaintext) — an
249
- // accepted v1 downgrade; a BYO database requiring TLS needs explicit config.
250
- return makeConnection(binding.host, binding, binding.password, "prefer", false)
251
- case "aurora": {
252
- const password = await readAwsSecret(binding.passwordSecretArn)
253
- return makeConnection(binding.clusterEndpoint, binding, password, "require", {
254
- rejectUnauthorized: false,
255
- })
256
- }
257
- case "cloud-sql": {
258
- const password = await readGcpSecret(binding.passwordSecretName)
259
- return makeConnection(binding.host, binding, password, "require", {
260
- rejectUnauthorized: false,
261
- })
262
- }
263
- case "flexible-server": {
264
- const password = await readAzureSecret(binding.passwordSecretUri)
265
- return makeConnection(binding.host, binding, password, "require", {
266
- rejectUnauthorized: false,
267
- })
268
- }
269
- default:
270
- return assertNever(binding)
271
- }
272
- }
273
-
274
- /** Reads the raw password the AWS controller stored as the secret's `SecretString`. */
275
- async function readAwsSecret(secretArn: string): Promise<string> {
276
- // Literal (not computed) import specifier so a bundler can leave it external — see the module
277
- // doc for why each cloud's secret SDK is loaded lazily.
278
- let sdk: typeof import("@aws-sdk/client-secrets-manager")
279
- try {
280
- sdk = await import("@aws-sdk/client-secrets-manager")
281
- } catch (cause) {
282
- throw (await AlienError.from(cause)).withContext(
283
- InvalidBindingConfigError.create({
284
- message: "Failed to load '@aws-sdk/client-secrets-manager'",
285
- suggestion: "Add '@aws-sdk/client-secrets-manager' to this workload's dependencies",
286
- }),
287
- )
288
- }
289
- const client = new sdk.SecretsManagerClient({})
290
- let password: string | undefined
291
- try {
292
- const response = await client.send(new sdk.GetSecretValueCommand({ SecretId: secretArn }))
293
- password = response.SecretString
294
- } catch (cause) {
295
- // A failed read is an upstream/transient failure (throttle, network, service unavailable), not
296
- // a user-fixable config error — make it retryable so an automated retry layer can recover.
297
- throw (await AlienError.from(cause)).withContext(
298
- PostgresSecretResolutionError.create({
299
- secret: secretArn,
300
- reason: "Secrets Manager GetSecretValue failed",
301
- }),
302
- )
303
- }
304
- if (!password) {
305
- // An empty stored secret is a control-plane invariant the workload can't fix, not bad user
306
- // input — surface it as a (retryable) resolution failure, like the read-failure path above.
307
- throw new AlienError(
308
- PostgresSecretResolutionError.create({
309
- secret: secretArn,
310
- reason: "Secrets Manager secret has no SecretString",
311
- }),
312
- )
313
- }
314
- return password
315
- }
316
-
317
- /** Reads the raw password the GCP controller stored as the secret version's payload. */
318
- async function readGcpSecret(secretName: string): Promise<string> {
319
- let sdk: typeof import("@google-cloud/secret-manager")
320
- try {
321
- sdk = await import("@google-cloud/secret-manager")
322
- } catch (cause) {
323
- throw (await AlienError.from(cause)).withContext(
324
- InvalidBindingConfigError.create({
325
- message: "Failed to load '@google-cloud/secret-manager'",
326
- suggestion: "Add '@google-cloud/secret-manager' to this workload's dependencies",
327
- }),
328
- )
329
- }
330
- // `fallback: true` forces the REST/HTTPS transport instead of the default native gRPC
331
- // (@grpc/grpc-js over HTTP/2). Workloads run on Bun, whose HTTP/2 support doesn't carry gRPC
332
- // reliably: the gRPC stub init rejects and the first `accessSecretVersion` re-throws it
333
- // synchronously (no network call). REST matches the HTTP transport the AWS/Azure SDKs already use.
334
- const client = new sdk.SecretManagerServiceClient({ fallback: true })
335
- let data: Uint8Array | string | null | undefined
336
- try {
337
- const projectId = await client.getProjectId()
338
- const [version] = await client.accessSecretVersion({
339
- name: `projects/${projectId}/secrets/${secretName}/versions/latest`,
340
- })
341
- data = version.payload?.data
342
- } catch (cause) {
343
- throw (await AlienError.from(cause)).withContext(
344
- PostgresSecretResolutionError.create({
345
- secret: secretName,
346
- reason: "Secret Manager accessSecretVersion failed",
347
- }),
348
- )
349
- }
350
- // `!data` does not catch a zero-length Uint8Array (an empty typed array is truthy), which would
351
- // otherwise decode to an empty password and silently pass. Guard on length so an empty payload
352
- // fails fast like the AWS/Azure paths (`!password` / `!value`).
353
- if (data == null || data.length === 0) {
354
- // An empty stored secret is a control-plane invariant the workload can't fix, not bad user
355
- // input — surface it as a (retryable) resolution failure, like the read-failure path above.
356
- throw new AlienError(
357
- PostgresSecretResolutionError.create({
358
- secret: secretName,
359
- reason: "Secret Manager secret has no payload",
360
- }),
361
- )
362
- }
363
- return Buffer.from(data).toString("utf8")
364
- }
365
-
366
- /**
367
- * Reads the raw password the Azure controller stored as a Key Vault secret. `secretUri` is the full
368
- * Key Vault secret URI the binding carries:
369
- * `https://<vault>.vault.azure.net/secrets/<name>[/<version>]`.
370
- */
371
- async function readAzureSecret(secretUri: string): Promise<string> {
372
- let url: URL
373
- try {
374
- url = new URL(secretUri)
375
- } catch (cause) {
376
- throw (await AlienError.from(cause)).withContext(
377
- InvalidBindingConfigError.create({
378
- message: `Postgres password secret URI '${secretUri}' is not a valid URL`,
379
- }),
380
- )
381
- }
382
- // Path is exactly `/secrets/<name>[/<version>]`; a missing version means "latest". Reject extra
383
- // segments rather than silently ignore them, matching the schema's fail-loud posture.
384
- const segments = url.pathname.split("/").filter(Boolean)
385
- const secretName = segments[1]
386
- const secretVersion = segments[2]
387
- if (segments[0] !== "secrets" || !secretName || segments.length > 3) {
388
- throw new AlienError(
389
- InvalidBindingConfigError.create({
390
- message: `Key Vault secret URI '${secretUri}' is not a '/secrets/<name>' URL`,
391
- }),
392
- )
393
- }
394
-
395
- let secrets: typeof import("@azure/keyvault-secrets")
396
- let identity: typeof import("@azure/identity")
397
- try {
398
- secrets = await import("@azure/keyvault-secrets")
399
- identity = await import("@azure/identity")
400
- } catch (cause) {
401
- throw (await AlienError.from(cause)).withContext(
402
- InvalidBindingConfigError.create({
403
- message: "Failed to load '@azure/keyvault-secrets' / '@azure/identity'",
404
- suggestion:
405
- "Add '@azure/keyvault-secrets' and '@azure/identity' to this workload's dependencies",
406
- }),
407
- )
408
- }
409
- const client = new secrets.SecretClient(url.origin, new identity.DefaultAzureCredential())
410
- let value: string | undefined
411
- try {
412
- const secret = await client.getSecret(
413
- secretName,
414
- secretVersion ? { version: secretVersion } : undefined,
415
- )
416
- value = secret.value
417
- } catch (cause) {
418
- throw (await AlienError.from(cause)).withContext(
419
- PostgresSecretResolutionError.create({
420
- secret: secretName,
421
- reason: "Key Vault getSecret failed",
422
- }),
423
- )
424
- }
425
- if (!value) {
426
- // An empty stored secret is a control-plane invariant the workload can't fix, not bad user
427
- // input — surface it as a (retryable) resolution failure, like the read-failure path above.
428
- throw new AlienError(
429
- PostgresSecretResolutionError.create({
430
- secret: secretName,
431
- reason: "Key Vault secret has no value",
432
- }),
433
- )
434
- }
435
- return value
436
- }
437
-
438
- function assertNever(_value: never): never {
439
- // Don't serialize the value: the external/local variants carry a plaintext password, and this
440
- // error is `internal: false`, so its message can surface to API clients.
441
- throw new AlienError(
442
- InvalidBindingConfigError.create({
443
- message: "Postgres binding has an unhandled service variant",
444
- }),
445
- )
446
- }