@alienplatform/sdk 1.14.1 → 2.0.1
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/worker.ts
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Worker binding implementation.
|
|
3
|
-
*
|
|
4
|
-
* Provides direct worker-to-worker invocation.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { type Channel, createClient } from "nice-grpc"
|
|
8
|
-
import {
|
|
9
|
-
type WorkerServiceClient as GeneratedClient,
|
|
10
|
-
WorkerServiceDefinition,
|
|
11
|
-
} from "../generated/worker.js"
|
|
12
|
-
import { wrapGrpcCall } from "../grpc-utils.js"
|
|
13
|
-
import type { WorkerInvokeRequest, WorkerInvokeResponse } from "../types.js"
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Worker binding for direct worker invocation.
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```typescript
|
|
20
|
-
* import { worker } from "@alienplatform/sdk"
|
|
21
|
-
*
|
|
22
|
-
* const processor = worker("image-processor")
|
|
23
|
-
*
|
|
24
|
-
* // Invoke with JSON body
|
|
25
|
-
* const response = await processor.invokeJson("resize-image", {
|
|
26
|
-
* imageUrl: "https://...",
|
|
27
|
-
* width: 800,
|
|
28
|
-
* height: 600,
|
|
29
|
-
* })
|
|
30
|
-
*
|
|
31
|
-
* // Get worker URL
|
|
32
|
-
* const url = await processor.getUrl()
|
|
33
|
-
* ```
|
|
34
|
-
*/
|
|
35
|
-
export class WorkerBinding {
|
|
36
|
-
private readonly client: GeneratedClient
|
|
37
|
-
private readonly bindingName: string
|
|
38
|
-
|
|
39
|
-
constructor(channel: Channel, bindingName: string) {
|
|
40
|
-
this.client = createClient(WorkerServiceDefinition, channel)
|
|
41
|
-
this.bindingName = bindingName
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Invoke a worker with raw request data.
|
|
46
|
-
*
|
|
47
|
-
* @param request - Invocation request
|
|
48
|
-
* @returns Worker response
|
|
49
|
-
*/
|
|
50
|
-
async invoke(request: WorkerInvokeRequest): Promise<WorkerInvokeResponse> {
|
|
51
|
-
return await wrapGrpcCall(
|
|
52
|
-
"WorkerService",
|
|
53
|
-
"Invoke",
|
|
54
|
-
async () => {
|
|
55
|
-
const response = await this.client.invoke({
|
|
56
|
-
bindingName: this.bindingName,
|
|
57
|
-
targetWorker: request.targetWorker,
|
|
58
|
-
method: request.method,
|
|
59
|
-
path: request.path,
|
|
60
|
-
headers: request.headers ?? {},
|
|
61
|
-
body: request.body ?? new Uint8Array(),
|
|
62
|
-
timeoutSeconds: request.timeoutMs ? Math.floor(request.timeoutMs / 1000) : undefined,
|
|
63
|
-
})
|
|
64
|
-
return {
|
|
65
|
-
status: response.status,
|
|
66
|
-
headers: response.headers,
|
|
67
|
-
body: response.body,
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
{ bindingName: this.bindingName },
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Invoke a worker with a JSON body.
|
|
76
|
-
*
|
|
77
|
-
* @param targetWorker - Target worker identifier
|
|
78
|
-
* @param body - JSON body
|
|
79
|
-
* @param options - Optional request options
|
|
80
|
-
* @returns Parsed JSON response
|
|
81
|
-
*/
|
|
82
|
-
async invokeJson<TRequest, TResponse = unknown>(
|
|
83
|
-
targetWorker: string,
|
|
84
|
-
body: TRequest,
|
|
85
|
-
options?: {
|
|
86
|
-
method?: string
|
|
87
|
-
path?: string
|
|
88
|
-
headers?: Record<string, string>
|
|
89
|
-
timeoutMs?: number
|
|
90
|
-
},
|
|
91
|
-
): Promise<TResponse> {
|
|
92
|
-
const response = await this.invoke({
|
|
93
|
-
targetWorker,
|
|
94
|
-
method: options?.method ?? "POST",
|
|
95
|
-
path: options?.path ?? "/",
|
|
96
|
-
headers: {
|
|
97
|
-
"content-type": "application/json",
|
|
98
|
-
...options?.headers,
|
|
99
|
-
},
|
|
100
|
-
body: new TextEncoder().encode(JSON.stringify(body)),
|
|
101
|
-
timeoutMs: options?.timeoutMs,
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
if (response.status >= 400) {
|
|
105
|
-
const errorText = new TextDecoder().decode(response.body)
|
|
106
|
-
throw new Error(`Worker invocation failed with status ${response.status}: ${errorText}`)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const responseText = new TextDecoder().decode(response.body)
|
|
110
|
-
return JSON.parse(responseText) as TResponse
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Invoke a worker with GET request.
|
|
115
|
-
*
|
|
116
|
-
* @param targetWorker - Target worker identifier
|
|
117
|
-
* @param path - Request path
|
|
118
|
-
* @param options - Optional request options
|
|
119
|
-
* @returns Parsed JSON response
|
|
120
|
-
*/
|
|
121
|
-
async get<TResponse = unknown>(
|
|
122
|
-
targetWorker: string,
|
|
123
|
-
path = "/",
|
|
124
|
-
options?: {
|
|
125
|
-
headers?: Record<string, string>
|
|
126
|
-
timeoutMs?: number
|
|
127
|
-
},
|
|
128
|
-
): Promise<TResponse> {
|
|
129
|
-
const response = await this.invoke({
|
|
130
|
-
targetWorker,
|
|
131
|
-
method: "GET",
|
|
132
|
-
path,
|
|
133
|
-
headers: options?.headers,
|
|
134
|
-
timeoutMs: options?.timeoutMs,
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
if (response.status >= 400) {
|
|
138
|
-
const errorText = new TextDecoder().decode(response.body)
|
|
139
|
-
throw new Error(`Worker GET failed with status ${response.status}: ${errorText}`)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const responseText = new TextDecoder().decode(response.body)
|
|
143
|
-
return JSON.parse(responseText) as TResponse
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Get the public URL of the worker.
|
|
148
|
-
*
|
|
149
|
-
* @returns Worker URL if available
|
|
150
|
-
*/
|
|
151
|
-
async getUrl(): Promise<string | undefined> {
|
|
152
|
-
return await wrapGrpcCall(
|
|
153
|
-
"WorkerService",
|
|
154
|
-
"GetWorkerUrl",
|
|
155
|
-
async () => {
|
|
156
|
-
const response = await this.client.getWorkerUrl({
|
|
157
|
-
bindingName: this.bindingName,
|
|
158
|
-
})
|
|
159
|
-
return response.url
|
|
160
|
-
},
|
|
161
|
-
{ bindingName: this.bindingName },
|
|
162
|
-
)
|
|
163
|
-
}
|
|
164
|
-
}
|
package/src/channel.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* gRPC channel management for the Alien bindings SDK.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { AlienError } from "@alienplatform/core"
|
|
6
|
-
import type { ChannelOptions } from "@grpc/grpc-js"
|
|
7
|
-
import { type Channel, createChannel } from "nice-grpc"
|
|
8
|
-
import { GrpcConnectionError, MissingEnvVarError } from "./errors.js"
|
|
9
|
-
|
|
10
|
-
/** Default channel options for gRPC connections */
|
|
11
|
-
const DEFAULT_CHANNEL_OPTIONS: ChannelOptions = {
|
|
12
|
-
// Explicit message size limits (128 MB) to avoid platform-specific defaults
|
|
13
|
-
"grpc.max_send_message_length": 128 * 1024 * 1024,
|
|
14
|
-
"grpc.max_receive_message_length": 128 * 1024 * 1024,
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Environment variable containing the gRPC endpoint */
|
|
18
|
-
const GRPC_ENDPOINT_VAR = "ALIEN_BINDINGS_GRPC_ADDRESS"
|
|
19
|
-
|
|
20
|
-
/** Cached channels by address */
|
|
21
|
-
const channelCache = new Map<string, Channel>()
|
|
22
|
-
|
|
23
|
-
/** Re-export the Channel type */
|
|
24
|
-
export type GrpcChannel = Channel
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Get the gRPC endpoint from environment variables.
|
|
28
|
-
*/
|
|
29
|
-
export function getGrpcEndpoint(): string {
|
|
30
|
-
const endpoint = process.env[GRPC_ENDPOINT_VAR]
|
|
31
|
-
if (!endpoint) {
|
|
32
|
-
throw new AlienError(
|
|
33
|
-
MissingEnvVarError.create({
|
|
34
|
-
variable: GRPC_ENDPOINT_VAR,
|
|
35
|
-
description:
|
|
36
|
-
"This variable is set by alien-runtime when running inside the Alien environment.",
|
|
37
|
-
}),
|
|
38
|
-
)
|
|
39
|
-
}
|
|
40
|
-
return endpoint
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Create a gRPC channel to the specified address.
|
|
45
|
-
*
|
|
46
|
-
* @param address - The gRPC server address
|
|
47
|
-
* @returns The created channel
|
|
48
|
-
*/
|
|
49
|
-
export async function createGrpcChannel(address: string): Promise<Channel> {
|
|
50
|
-
try {
|
|
51
|
-
return createChannel(address, undefined, DEFAULT_CHANNEL_OPTIONS)
|
|
52
|
-
} catch (error) {
|
|
53
|
-
throw (await AlienError.from(error)).withContext(
|
|
54
|
-
GrpcConnectionError.create({
|
|
55
|
-
endpoint: address,
|
|
56
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
57
|
-
}),
|
|
58
|
-
)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Get or create a cached gRPC channel to the specified address.
|
|
64
|
-
*
|
|
65
|
-
* @param address - The gRPC server address
|
|
66
|
-
* @returns The cached or newly created channel
|
|
67
|
-
*/
|
|
68
|
-
export async function getOrCreateChannel(address: string): Promise<Channel> {
|
|
69
|
-
let channel = channelCache.get(address)
|
|
70
|
-
if (!channel) {
|
|
71
|
-
channel = await createGrpcChannel(address)
|
|
72
|
-
channelCache.set(address, channel)
|
|
73
|
-
}
|
|
74
|
-
return channel
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Get a gRPC channel to the alien-runtime using the default env var.
|
|
79
|
-
* The channel is cached for reuse across all bindings.
|
|
80
|
-
*/
|
|
81
|
-
export async function getChannel(): Promise<Channel> {
|
|
82
|
-
const endpoint = getGrpcEndpoint()
|
|
83
|
-
return await getOrCreateChannel(endpoint)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Close all cached gRPC channels.
|
|
88
|
-
* This should be called when shutting down the application.
|
|
89
|
-
*/
|
|
90
|
-
export function closeChannel(): void {
|
|
91
|
-
for (const channel of channelCache.values()) {
|
|
92
|
-
channel.close()
|
|
93
|
-
}
|
|
94
|
-
channelCache.clear()
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Reset the cached channels (useful for testing).
|
|
99
|
-
*/
|
|
100
|
-
export function resetChannel(): void {
|
|
101
|
-
channelCache.clear()
|
|
102
|
-
}
|
package/src/commands/client.ts
DELETED
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Commands Client - Lightweight client for deployment command invocation
|
|
3
|
-
*
|
|
4
|
-
* Handles command invocation with automatic:
|
|
5
|
-
* - Base64 encoding/decoding of params and responses
|
|
6
|
-
* - Large payload upload/download via storage
|
|
7
|
-
* - Polling with exponential backoff
|
|
8
|
-
* - Error handling and timeout
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { AlienError } from "@alienplatform/core"
|
|
12
|
-
import {
|
|
13
|
-
CommandCreationFailedError,
|
|
14
|
-
CommandExpiredError,
|
|
15
|
-
CommandTimeoutError,
|
|
16
|
-
DeploymentCommandError,
|
|
17
|
-
ManagerHttpError,
|
|
18
|
-
ResponseDecodingFailedError,
|
|
19
|
-
StorageOperationFailedError,
|
|
20
|
-
} from "./errors.js"
|
|
21
|
-
import type {
|
|
22
|
-
BodySpec,
|
|
23
|
-
CommandResponse,
|
|
24
|
-
CommandState,
|
|
25
|
-
CommandStatusResponse,
|
|
26
|
-
CommandsClientConfig,
|
|
27
|
-
CreateCommandResponse,
|
|
28
|
-
InvokeOptions,
|
|
29
|
-
} from "./types.js"
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Base64 encode data
|
|
33
|
-
*/
|
|
34
|
-
function base64Encode(data: string | Record<string, unknown>): string {
|
|
35
|
-
const json = typeof data === "string" ? data : JSON.stringify(data)
|
|
36
|
-
if (typeof btoa !== "undefined") {
|
|
37
|
-
// Browser
|
|
38
|
-
return btoa(json)
|
|
39
|
-
}
|
|
40
|
-
// Node.js
|
|
41
|
-
return Buffer.from(json, "utf-8").toString("base64")
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Base64 decode data
|
|
46
|
-
*/
|
|
47
|
-
function base64Decode(encoded: string): string {
|
|
48
|
-
if (typeof atob !== "undefined") {
|
|
49
|
-
// Browser
|
|
50
|
-
return atob(encoded)
|
|
51
|
-
}
|
|
52
|
-
// Node.js
|
|
53
|
-
return Buffer.from(encoded, "base64").toString("utf-8")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Create inline body spec. Always sends inline — the server handles storage
|
|
58
|
-
* decisions transparently (auto-promoting to blob if needed).
|
|
59
|
-
*/
|
|
60
|
-
function createBodySpec(data: string | Record<string, unknown>): BodySpec {
|
|
61
|
-
const json = typeof data === "string" ? data : JSON.stringify(data)
|
|
62
|
-
return {
|
|
63
|
-
mode: "inline",
|
|
64
|
-
inlineBase64: base64Encode(json),
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Decode response body spec
|
|
70
|
-
*/
|
|
71
|
-
async function decodeBodySpec(
|
|
72
|
-
body: BodySpec,
|
|
73
|
-
commandId: string,
|
|
74
|
-
command: string,
|
|
75
|
-
allowLocalStorage: boolean,
|
|
76
|
-
): Promise<unknown> {
|
|
77
|
-
if (body.mode === "inline") {
|
|
78
|
-
const json = base64Decode(body.inlineBase64)
|
|
79
|
-
return JSON.parse(json)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (body.mode === "storage") {
|
|
83
|
-
// Storage mode - download from presigned URL
|
|
84
|
-
if (!body.storageGetRequest) {
|
|
85
|
-
throw new AlienError(
|
|
86
|
-
ResponseDecodingFailedError.create({
|
|
87
|
-
commandId,
|
|
88
|
-
command,
|
|
89
|
-
reason: "Storage response missing storageGetRequest",
|
|
90
|
-
}),
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const request = body.storageGetRequest
|
|
95
|
-
|
|
96
|
-
// Check if request has expired
|
|
97
|
-
const expiration = new Date(request.expiration)
|
|
98
|
-
if (Date.now() > expiration.getTime()) {
|
|
99
|
-
throw new AlienError(
|
|
100
|
-
StorageOperationFailedError.create({
|
|
101
|
-
operation: "download",
|
|
102
|
-
url: request.backend.type === "http" ? request.backend.url : "local",
|
|
103
|
-
reason: `Presigned request expired at ${expiration.toISOString()}`,
|
|
104
|
-
}),
|
|
105
|
-
)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Handle different backend types
|
|
109
|
-
if (request.backend.type === "http") {
|
|
110
|
-
// HTTP backend (AWS S3, GCP GCS, Azure Blob)
|
|
111
|
-
try {
|
|
112
|
-
const response = await fetch(request.backend.url, {
|
|
113
|
-
method: request.backend.method,
|
|
114
|
-
headers: request.backend.headers,
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
if (!response.ok) {
|
|
118
|
-
throw new AlienError(
|
|
119
|
-
StorageOperationFailedError.create({
|
|
120
|
-
operation: "download",
|
|
121
|
-
url: request.backend.url,
|
|
122
|
-
reason: `HTTP ${response.status} ${response.statusText}`,
|
|
123
|
-
}),
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const text = await response.text()
|
|
128
|
-
return JSON.parse(text)
|
|
129
|
-
} catch (error) {
|
|
130
|
-
if (error instanceof AlienError) {
|
|
131
|
-
throw error
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Wrap fetch/parse errors
|
|
135
|
-
const alienError = await AlienError.from(error)
|
|
136
|
-
throw alienError.withContext(
|
|
137
|
-
StorageOperationFailedError.create({
|
|
138
|
-
operation: "download",
|
|
139
|
-
url: request.backend.url,
|
|
140
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
141
|
-
}),
|
|
142
|
-
)
|
|
143
|
-
}
|
|
144
|
-
} else if (request.backend.type === "local") {
|
|
145
|
-
// Local backend: Only enabled when explicitly allowed (local dev/testing)
|
|
146
|
-
if (!allowLocalStorage) {
|
|
147
|
-
throw new AlienError(
|
|
148
|
-
StorageOperationFailedError.create({
|
|
149
|
-
operation: "download",
|
|
150
|
-
url: `local://${request.backend.filePath}`,
|
|
151
|
-
reason: "Local storage backend not enabled (set allowLocalStorage: true for local dev)",
|
|
152
|
-
}),
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Validate path doesn't contain path traversal attempts
|
|
157
|
-
const filePath = request.backend.filePath
|
|
158
|
-
if (filePath.includes("..")) {
|
|
159
|
-
throw new AlienError(
|
|
160
|
-
StorageOperationFailedError.create({
|
|
161
|
-
operation: "download",
|
|
162
|
-
url: `local://${filePath}`,
|
|
163
|
-
reason: "Path traversal not allowed in local storage paths",
|
|
164
|
-
}),
|
|
165
|
-
)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Dynamically import fs/promises only when needed (local dev)
|
|
169
|
-
try {
|
|
170
|
-
const { readFile } = await import("node:fs/promises")
|
|
171
|
-
const content = await readFile(filePath, "utf-8")
|
|
172
|
-
return JSON.parse(content)
|
|
173
|
-
} catch (error) {
|
|
174
|
-
if (error instanceof AlienError) {
|
|
175
|
-
throw error
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Wrap filesystem/parse errors
|
|
179
|
-
const alienError = await AlienError.from(error)
|
|
180
|
-
throw alienError.withContext(
|
|
181
|
-
StorageOperationFailedError.create({
|
|
182
|
-
operation: "download",
|
|
183
|
-
url: `local://${filePath}`,
|
|
184
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
185
|
-
}),
|
|
186
|
-
)
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
throw new AlienError(
|
|
190
|
-
StorageOperationFailedError.create({
|
|
191
|
-
operation: "download",
|
|
192
|
-
url: "unknown",
|
|
193
|
-
reason: `Unknown storage backend type: ${(request.backend as any).type}`,
|
|
194
|
-
}),
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
throw new AlienError(
|
|
200
|
-
ResponseDecodingFailedError.create({
|
|
201
|
-
commandId,
|
|
202
|
-
command,
|
|
203
|
-
reason: `Unknown body mode: ${(body as any).mode}`,
|
|
204
|
-
}),
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Check if state is terminal
|
|
210
|
-
*/
|
|
211
|
-
function isTerminalState(state: CommandState): boolean {
|
|
212
|
-
return state === "SUCCEEDED" || state === "FAILED" || state === "EXPIRED"
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Commands client for invoking deployment commands
|
|
217
|
-
*/
|
|
218
|
-
export class CommandsClient {
|
|
219
|
-
private readonly managerUrl: string
|
|
220
|
-
private readonly deploymentId: string
|
|
221
|
-
private readonly token: string
|
|
222
|
-
private readonly defaultTimeout: number
|
|
223
|
-
private readonly allowLocalStorage: boolean
|
|
224
|
-
|
|
225
|
-
constructor(config: CommandsClientConfig) {
|
|
226
|
-
this.managerUrl = config.managerUrl.replace(/\/+$/, "")
|
|
227
|
-
this.deploymentId = config.deploymentId
|
|
228
|
-
this.token = config.token
|
|
229
|
-
this.defaultTimeout = config.timeout ?? 60_000
|
|
230
|
-
this.allowLocalStorage = config.allowLocalStorage ?? false
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Invoke a command on the deployment and wait for response
|
|
235
|
-
*
|
|
236
|
-
* @param command - Command name (e.g., "generate-report")
|
|
237
|
-
* @param params - Command parameters (JSON-serializable)
|
|
238
|
-
* @param options - Invocation options
|
|
239
|
-
* @returns Decoded response data
|
|
240
|
-
*/
|
|
241
|
-
async invoke<TParams = unknown, TResponse = unknown>(
|
|
242
|
-
command: string,
|
|
243
|
-
params: TParams,
|
|
244
|
-
options?: InvokeOptions,
|
|
245
|
-
): Promise<TResponse> {
|
|
246
|
-
const timeout = options?.timeout ?? this.defaultTimeout
|
|
247
|
-
const startTime = Date.now()
|
|
248
|
-
|
|
249
|
-
// Step 1: Create command
|
|
250
|
-
const createResponse = await this.createCommand(command, params, options)
|
|
251
|
-
|
|
252
|
-
// Step 2: Poll for completion
|
|
253
|
-
const pollInterval = options?.pollInterval ?? 500
|
|
254
|
-
const maxPollInterval = options?.maxPollInterval ?? 5000
|
|
255
|
-
const pollBackoff = options?.pollBackoff ?? 1.5
|
|
256
|
-
let currentInterval = pollInterval
|
|
257
|
-
|
|
258
|
-
while (Date.now() - startTime < timeout) {
|
|
259
|
-
await this.sleep(currentInterval)
|
|
260
|
-
|
|
261
|
-
const status = await this.getCommandStatus(createResponse.commandId)
|
|
262
|
-
|
|
263
|
-
if (isTerminalState(status.state)) {
|
|
264
|
-
return await this.handleTerminalState(command, status)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Exponential backoff
|
|
268
|
-
currentInterval = Math.min(currentInterval * pollBackoff, maxPollInterval)
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Timeout
|
|
272
|
-
const finalStatus = await this.getCommandStatus(createResponse.commandId)
|
|
273
|
-
throw new AlienError(
|
|
274
|
-
CommandTimeoutError.create({
|
|
275
|
-
commandId: createResponse.commandId,
|
|
276
|
-
command,
|
|
277
|
-
timeoutMs: timeout,
|
|
278
|
-
lastState: finalStatus.state,
|
|
279
|
-
}),
|
|
280
|
-
)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* Create a command
|
|
285
|
-
*/
|
|
286
|
-
private async createCommand(
|
|
287
|
-
command: string,
|
|
288
|
-
params: unknown,
|
|
289
|
-
options?: InvokeOptions,
|
|
290
|
-
): Promise<CreateCommandResponse> {
|
|
291
|
-
const url = `${this.managerUrl}/v1/commands`
|
|
292
|
-
const body = {
|
|
293
|
-
deploymentId: this.deploymentId,
|
|
294
|
-
command,
|
|
295
|
-
params: createBodySpec(params as string | Record<string, unknown>),
|
|
296
|
-
deadline: options?.deadline?.toISOString(),
|
|
297
|
-
idempotencyKey: options?.idempotencyKey,
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
try {
|
|
301
|
-
const response = await fetch(url, {
|
|
302
|
-
method: "POST",
|
|
303
|
-
headers: {
|
|
304
|
-
"Content-Type": "application/json",
|
|
305
|
-
Authorization: `Bearer ${this.token}`,
|
|
306
|
-
},
|
|
307
|
-
body: JSON.stringify(body),
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
if (!response.ok) {
|
|
311
|
-
const errorBody = await response.text().catch(() => "")
|
|
312
|
-
throw new AlienError(
|
|
313
|
-
ManagerHttpError.create({
|
|
314
|
-
method: "POST",
|
|
315
|
-
url,
|
|
316
|
-
status: response.status,
|
|
317
|
-
statusText: response.statusText,
|
|
318
|
-
body: errorBody,
|
|
319
|
-
}),
|
|
320
|
-
)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
return (await response.json()) as CreateCommandResponse
|
|
324
|
-
} catch (error) {
|
|
325
|
-
if (error instanceof AlienError) {
|
|
326
|
-
throw error
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
const alienError = await AlienError.from(error)
|
|
330
|
-
throw alienError.withContext(
|
|
331
|
-
CommandCreationFailedError.create({
|
|
332
|
-
deploymentId: this.deploymentId,
|
|
333
|
-
command,
|
|
334
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
335
|
-
}),
|
|
336
|
-
)
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* Get command status
|
|
342
|
-
*/
|
|
343
|
-
private async getCommandStatus(commandId: string): Promise<CommandStatusResponse> {
|
|
344
|
-
const url = `${this.managerUrl}/v1/commands/${commandId}`
|
|
345
|
-
|
|
346
|
-
try {
|
|
347
|
-
const response = await fetch(url, {
|
|
348
|
-
method: "GET",
|
|
349
|
-
headers: {
|
|
350
|
-
Authorization: `Bearer ${this.token}`,
|
|
351
|
-
},
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
if (!response.ok) {
|
|
355
|
-
const errorBody = await response.text().catch(() => "")
|
|
356
|
-
throw new AlienError(
|
|
357
|
-
ManagerHttpError.create({
|
|
358
|
-
method: "GET",
|
|
359
|
-
url,
|
|
360
|
-
status: response.status,
|
|
361
|
-
statusText: response.statusText,
|
|
362
|
-
body: errorBody,
|
|
363
|
-
}),
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return (await response.json()) as CommandStatusResponse
|
|
368
|
-
} catch (error) {
|
|
369
|
-
if (error instanceof AlienError) {
|
|
370
|
-
throw error
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
throw await AlienError.from(error)
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Handle terminal state and return response
|
|
379
|
-
*/
|
|
380
|
-
private async handleTerminalState<TResponse>(
|
|
381
|
-
command: string,
|
|
382
|
-
status: CommandStatusResponse,
|
|
383
|
-
): Promise<TResponse> {
|
|
384
|
-
if (status.state === "EXPIRED") {
|
|
385
|
-
throw new AlienError(
|
|
386
|
-
CommandExpiredError.create({
|
|
387
|
-
commandId: status.commandId,
|
|
388
|
-
command,
|
|
389
|
-
}),
|
|
390
|
-
)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (!status.response) {
|
|
394
|
-
throw new AlienError(
|
|
395
|
-
ResponseDecodingFailedError.create({
|
|
396
|
-
commandId: status.commandId,
|
|
397
|
-
command,
|
|
398
|
-
reason: "Terminal state but no response present",
|
|
399
|
-
}),
|
|
400
|
-
)
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const response: CommandResponse = status.response
|
|
404
|
-
|
|
405
|
-
if (response.status === "error") {
|
|
406
|
-
throw new AlienError(
|
|
407
|
-
DeploymentCommandError.create({
|
|
408
|
-
commandId: status.commandId,
|
|
409
|
-
command,
|
|
410
|
-
errorCode: response.code,
|
|
411
|
-
errorMessage: response.message,
|
|
412
|
-
errorDetails: response.details ?? undefined,
|
|
413
|
-
}),
|
|
414
|
-
)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Decode success response
|
|
418
|
-
try {
|
|
419
|
-
return (await decodeBodySpec(
|
|
420
|
-
response.response,
|
|
421
|
-
status.commandId,
|
|
422
|
-
command,
|
|
423
|
-
this.allowLocalStorage,
|
|
424
|
-
)) as TResponse
|
|
425
|
-
} catch (error) {
|
|
426
|
-
if (error instanceof AlienError) {
|
|
427
|
-
throw error
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
throw new AlienError(
|
|
431
|
-
ResponseDecodingFailedError.create({
|
|
432
|
-
commandId: status.commandId,
|
|
433
|
-
command,
|
|
434
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
435
|
-
}),
|
|
436
|
-
)
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Sleep helper
|
|
442
|
-
*/
|
|
443
|
-
private sleep(ms: number): Promise<void> {
|
|
444
|
-
return new Promise(resolve => setTimeout(resolve, ms))
|
|
445
|
-
}
|
|
446
|
-
}
|