@alienplatform/sdk 1.10.7 → 1.11.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 (49) hide show
  1. package/.turbo/turbo-build.log +37 -8
  2. package/dist/bearerTokenAuthenticationPolicy.js +3028 -0
  3. package/dist/bindings/index.d.ts +1 -0
  4. package/dist/bindings/index.d.ts.map +1 -1
  5. package/dist/bindings/postgres.d.ts +90 -0
  6. package/dist/bindings/postgres.d.ts.map +1 -0
  7. package/dist/bindings/postgres.test.d.ts +2 -0
  8. package/dist/bindings/postgres.test.d.ts.map +1 -0
  9. package/dist/client.js +2376 -0
  10. package/dist/commands/index.js +1 -1
  11. package/dist/dist-cjs.js +3 -0
  12. package/dist/dist-cjs2.js +718 -0
  13. package/dist/dist-cjs3.js +2486 -0
  14. package/dist/dist-cjs4.js +171 -0
  15. package/dist/dist-cjs5.js +429 -0
  16. package/dist/dist-cjs6.js +60 -0
  17. package/dist/dist-cjs7.js +978 -0
  18. package/dist/dist-cjs8.js +86 -0
  19. package/dist/dist-cjs9.js +319 -0
  20. package/dist/dist.js +498 -36
  21. package/dist/dist2.js +1239 -0
  22. package/dist/errors.d.ts +25 -0
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/es5.js +1624 -0
  25. package/dist/esm.js +18211 -0
  26. package/dist/esm2.js +3104 -0
  27. package/dist/event-streams.js +203 -0
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +336 -24996
  31. package/dist/jws.js +611 -0
  32. package/dist/open.js +460 -0
  33. package/dist/package.js +4 -0
  34. package/dist/protocols.js +954 -0
  35. package/dist/protocols2.js +5565 -0
  36. package/dist/rolldown-runtime.js +37 -0
  37. package/dist/serde.js +5542 -0
  38. package/dist/signin.js +723 -0
  39. package/dist/src.js +24900 -0
  40. package/dist/src2.js +114030 -0
  41. package/dist/sso-oidc.js +940 -0
  42. package/dist/sts.js +1329 -0
  43. package/dist/tslib.es6.js +543 -0
  44. package/package.json +26 -2
  45. package/src/bindings/index.ts +1 -0
  46. package/src/bindings/postgres.test.ts +17 -0
  47. package/src/bindings/postgres.ts +446 -0
  48. package/src/errors.ts +21 -0
  49. package/src/index.ts +5 -0
@@ -0,0 +1,446 @@
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
+ }
package/src/errors.ts CHANGED
@@ -257,3 +257,24 @@ export const InvalidBindingConfigError = defineError({
257
257
  internal: false,
258
258
  httpStatusCode: 400,
259
259
  })
260
+
261
+ /**
262
+ * Error thrown when reading a Postgres connection-password secret from a cloud secret store yields
263
+ * no usable value — an upstream read failure (throttle, network blip, service unavailable) or a
264
+ * stored secret that is present but empty. Both are control-plane invariants the workload cannot
265
+ * fix, so unlike {@link InvalidBindingConfigError} this is retryable and maps to 503, not a
266
+ * user-fixable 400. The genuinely user-invalid cases (a malformed secret URI, a malformed binding)
267
+ * keep using InvalidBindingConfigError.
268
+ */
269
+ export const PostgresSecretResolutionError = defineError({
270
+ code: "POSTGRES_SECRET_RESOLUTION_ERROR",
271
+ context: z.object({
272
+ secret: z.string(),
273
+ reason: z.string(),
274
+ }),
275
+ message: ({ secret, reason }) =>
276
+ `Failed to read Postgres connection-password secret '${secret}': ${reason}`,
277
+ retryable: true,
278
+ internal: false,
279
+ httpStatusCode: 503,
280
+ })
package/src/index.ts CHANGED
@@ -74,6 +74,10 @@ export { ArtifactRegistry } from "./bindings/artifact-registry.js"
74
74
  export { WorkerBinding } from "./bindings/worker.js"
75
75
  export { ServiceAccount } from "./bindings/service-account.js"
76
76
 
77
+ // Postgres is connection-only (no gRPC service): a function that resolves connection
78
+ // details for a linked database, not a channel-backed binding class.
79
+ export { getPostgresConnection, type PostgresConnection } from "./bindings/postgres.js"
80
+
77
81
  // ============================================================================
78
82
  // Types
79
83
  // ============================================================================
@@ -140,6 +144,7 @@ export {
140
144
  EventHandlerAlreadyRegisteredError,
141
145
  CommandAlreadyRegisteredError,
142
146
  InvalidBindingConfigError,
147
+ PostgresSecretResolutionError,
143
148
  } from "./errors.js"
144
149
 
145
150
  // ============================================================================