@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.
- package/dist/dist.js +371 -275
- package/dist/index.d.ts +22 -28
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -14356
- package/dist/native.d.ts +26 -0
- package/dist/native.d.ts.map +1 -0
- package/dist/native.js +2 -0
- package/dist/worker-runtime/channel.d.ts +25 -0
- package/dist/worker-runtime/channel.d.ts.map +1 -0
- package/dist/worker-runtime/errors.d.ts +79 -0
- package/dist/worker-runtime/errors.d.ts.map +1 -0
- package/dist/worker-runtime/event-loop.d.ts +43 -0
- package/dist/worker-runtime/event-loop.d.ts.map +1 -0
- package/dist/{generated → worker-runtime/generated}/control.d.ts +15 -15
- package/dist/worker-runtime/generated/control.d.ts.map +1 -0
- package/dist/worker-runtime/generated/google/protobuf/duration.d.ts.map +1 -0
- package/dist/worker-runtime/generated/google/protobuf/timestamp.d.ts.map +1 -0
- package/dist/{generated → worker-runtime/generated}/wait_until.d.ts +29 -29
- package/dist/worker-runtime/generated/wait_until.d.ts.map +1 -0
- package/dist/worker-runtime/grpc-utils.d.ts +23 -0
- package/dist/worker-runtime/grpc-utils.d.ts.map +1 -0
- package/dist/worker-runtime/index.d.ts +33 -0
- package/dist/worker-runtime/index.d.ts.map +1 -0
- package/dist/{src.js → worker-runtime/index.js} +9370 -4035
- package/dist/worker-runtime/registry.d.ts +169 -0
- package/dist/worker-runtime/registry.d.ts.map +1 -0
- package/dist/worker-runtime/service-definitions.d.ts +12 -0
- package/dist/worker-runtime/service-definitions.d.ts.map +1 -0
- package/dist/worker-runtime/wait-until-manager.d.ts +34 -0
- package/dist/worker-runtime/wait-until-manager.d.ts.map +1 -0
- package/package.json +18 -38
- package/.turbo/turbo-build.log +0 -30
- package/AGENTS.md +0 -70
- package/dist/bearerTokenAuthenticationPolicy.js +0 -3028
- package/dist/bindings/artifact-registry.d.ts +0 -88
- package/dist/bindings/artifact-registry.d.ts.map +0 -1
- package/dist/bindings/build.d.ts +0 -67
- package/dist/bindings/build.d.ts.map +0 -1
- package/dist/bindings/index.d.ts +0 -13
- package/dist/bindings/index.d.ts.map +0 -1
- package/dist/bindings/kv.d.ts +0 -113
- package/dist/bindings/kv.d.ts.map +0 -1
- package/dist/bindings/postgres.d.ts +0 -90
- package/dist/bindings/postgres.d.ts.map +0 -1
- package/dist/bindings/postgres.test.d.ts +0 -2
- package/dist/bindings/postgres.test.d.ts.map +0 -1
- package/dist/bindings/queue.d.ts +0 -81
- package/dist/bindings/queue.d.ts.map +0 -1
- package/dist/bindings/service-account.d.ts +0 -46
- package/dist/bindings/service-account.d.ts.map +0 -1
- package/dist/bindings/storage.d.ts +0 -164
- package/dist/bindings/storage.d.ts.map +0 -1
- package/dist/bindings/vault.d.ts +0 -65
- package/dist/bindings/vault.d.ts.map +0 -1
- package/dist/bindings/worker.d.ts +0 -72
- package/dist/bindings/worker.d.ts.map +0 -1
- package/dist/channel.d.ts +0 -39
- package/dist/channel.d.ts.map +0 -1
- package/dist/checksum.js +0 -6176
- package/dist/commands/client.d.ts +0 -47
- package/dist/commands/client.d.ts.map +0 -1
- package/dist/commands/errors.d.ts +0 -198
- package/dist/commands/errors.d.ts.map +0 -1
- package/dist/commands/index.d.ts +0 -23
- package/dist/commands/index.d.ts.map +0 -1
- package/dist/commands/index.js +0 -387
- package/dist/commands/types.d.ts +0 -40
- package/dist/commands/types.d.ts.map +0 -1
- package/dist/commands.d.ts +0 -55
- package/dist/commands.d.ts.map +0 -1
- package/dist/context.d.ts +0 -182
- package/dist/context.d.ts.map +0 -1
- package/dist/dist-cjs.js +0 -15258
- package/dist/dist2.js +0 -1239
- package/dist/errors.d.ts +0 -359
- package/dist/errors.d.ts.map +0 -1
- package/dist/es5.js +0 -1720
- package/dist/esm.js +0 -18068
- package/dist/esm2.js +0 -3166
- package/dist/event-streams.js +0 -770
- package/dist/events.d.ts +0 -163
- package/dist/events.d.ts.map +0 -1
- package/dist/generated/artifact_registry.d.ts +0 -302
- package/dist/generated/artifact_registry.d.ts.map +0 -1
- package/dist/generated/build.d.ts +0 -184
- package/dist/generated/build.d.ts.map +0 -1
- package/dist/generated/container.d.ts +0 -101
- package/dist/generated/container.d.ts.map +0 -1
- package/dist/generated/control.d.ts.map +0 -1
- package/dist/generated/google/protobuf/duration.d.ts.map +0 -1
- package/dist/generated/google/protobuf/timestamp.d.ts.map +0 -1
- package/dist/generated/kv.d.ts +0 -182
- package/dist/generated/kv.d.ts.map +0 -1
- package/dist/generated/queue.d.ts +0 -127
- package/dist/generated/queue.d.ts.map +0 -1
- package/dist/generated/service_account.d.ts +0 -112
- package/dist/generated/service_account.d.ts.map +0 -1
- package/dist/generated/storage.d.ts +0 -784
- package/dist/generated/storage.d.ts.map +0 -1
- package/dist/generated/vault.d.ts +0 -107
- package/dist/generated/vault.d.ts.map +0 -1
- package/dist/generated/wait_until.d.ts.map +0 -1
- package/dist/generated/worker.d.ts +0 -107
- package/dist/generated/worker.d.ts.map +0 -1
- package/dist/global.d.ts +0 -208
- package/dist/global.d.ts.map +0 -1
- package/dist/grpc-utils.d.ts +0 -25
- package/dist/grpc-utils.d.ts.map +0 -1
- package/dist/jws.js +0 -611
- package/dist/open.js +0 -461
- package/dist/rolldown-runtime.js +0 -44
- package/dist/src2.js +0 -114032
- package/dist/types.d.ts +0 -337
- package/dist/types.d.ts.map +0 -1
- package/dist/wait-until.d.ts +0 -115
- package/dist/wait-until.d.ts.map +0 -1
- package/scripts/generate-proto.sh +0 -46
- package/src/bindings/AGENTS.md +0 -105
- package/src/bindings/artifact-registry.ts +0 -317
- package/src/bindings/build.ts +0 -195
- package/src/bindings/index.ts +0 -13
- package/src/bindings/kv.ts +0 -240
- package/src/bindings/postgres.test.ts +0 -17
- package/src/bindings/postgres.ts +0 -446
- package/src/bindings/queue.ts +0 -191
- package/src/bindings/service-account.ts +0 -113
- package/src/bindings/storage.ts +0 -536
- package/src/bindings/vault.ts +0 -133
- package/src/bindings/worker.ts +0 -164
- package/src/channel.ts +0 -102
- package/src/commands/client.ts +0 -446
- package/src/commands/errors.ts +0 -126
- package/src/commands/index.ts +0 -41
- package/src/commands/types.ts +0 -52
- package/src/commands.ts +0 -76
- package/src/context.ts +0 -368
- package/src/errors.ts +0 -280
- package/src/events.ts +0 -511
- package/src/generated/artifact_registry.ts +0 -2096
- package/src/generated/build.ts +0 -1311
- package/src/generated/container.ts +0 -503
- package/src/generated/control.ts +0 -2018
- package/src/generated/google/protobuf/duration.ts +0 -196
- package/src/generated/google/protobuf/timestamp.ts +0 -226
- package/src/generated/kv.ts +0 -1169
- package/src/generated/queue.ts +0 -753
- package/src/generated/service_account.ts +0 -814
- package/src/generated/storage.ts +0 -3820
- package/src/generated/vault.ts +0 -543
- package/src/generated/wait_until.ts +0 -829
- package/src/generated/worker.ts +0 -759
- package/src/global.ts +0 -287
- package/src/grpc-utils.ts +0 -159
- package/src/index.ts +0 -160
- package/src/types.ts +0 -397
- package/src/wait-until.ts +0 -273
- package/tsconfig.json +0 -8
- package/tsdown.config.ts +0 -17
- /package/dist/{generated → worker-runtime/generated}/google/protobuf/duration.d.ts +0 -0
- /package/dist/{generated → worker-runtime/generated}/google/protobuf/timestamp.d.ts +0 -0
package/src/bindings/kv.ts
DELETED
|
@@ -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
|
-
})
|
package/src/bindings/postgres.ts
DELETED
|
@@ -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
|
-
}
|