@sequenceholdings/artifact-studio 0.8.0 → 0.9.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/README.md +101 -3
- package/dist/api.d.ts +2 -1
- package/dist/api.js +15 -5
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +58 -20
- package/dist/gateway/authorization-cache.js +1 -1
- package/dist/gateway/control.d.ts +13 -1
- package/dist/gateway/control.js +3 -0
- package/dist/gateway/datadog-startup.d.ts +13 -0
- package/dist/gateway/datadog-startup.js +51 -0
- package/dist/gateway/hosted-proxy.d.ts +2 -0
- package/dist/gateway/hosted-proxy.js +9 -11
- package/dist/gateway/ingress-handler.js +2 -0
- package/dist/gateway/server.d.ts +1 -1
- package/dist/gateway/server.js +51 -20
- package/dist/gateway/session.d.ts +14 -0
- package/dist/gateway/session.js +31 -2
- package/dist/gateway-main.js +5 -3
- package/dist/next-build.d.ts +3 -1
- package/dist/next-build.js +10 -6
- package/dist/sdk.d.ts +2 -0
- package/dist/server/agent-responses.d.ts +425 -0
- package/dist/server/agent-responses.js +154 -0
- package/dist/server/broker-contract.d.ts +5 -0
- package/dist/server/broker-contract.js +6 -2
- package/dist/server/client.d.ts +2 -0
- package/dist/server/client.js +10 -2
- package/dist/server/processes.d.ts +11 -0
- package/dist/server/request-authentication.d.ts +42 -0
- package/dist/server/request-authentication.js +93 -0
- package/dist/server/request-context.d.ts +1 -0
- package/dist/server/request-context.js +1 -0
- package/dist/server.d.ts +2 -1
- package/dist/server.js +1 -1
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/tunnel/broker.d.ts +14 -1
- package/dist/tunnel/broker.js +94 -10
- package/dist/tunnel/client.js +6 -0
- package/dist/tunnel/redis-types.d.ts +5 -1
- package/dist/tunnel/redis.d.ts +12 -0
- package/dist/tunnel/redis.js +102 -4
- package/dist/tunnel/transport.d.ts +3 -0
- package/package.json +3 -3
- package/templates/react-vite/package.json +1 -1
package/README.md
CHANGED
|
@@ -163,6 +163,25 @@ workspace source awaiting a matching SDK release and platform deployment;
|
|
|
163
163
|
they are not a newly supported public SDK or direct HTTP contract. See
|
|
164
164
|
[VERSION-PIN.md](./VERSION-PIN.md) for compatibility requirements.
|
|
165
165
|
|
|
166
|
+
Human completion receipts require a genuine Auth0 browser authentication carried
|
|
167
|
+
by the platform. The gateway signs a separate, optional authentication proof
|
|
168
|
+
bound to the exact request context; the server SDK forwards it unchanged.
|
|
169
|
+
Legacy contexts continue to work without producing human receipts. Service mode,
|
|
170
|
+
external ingress, impersonation and an authored `assertedBy` never supply human
|
|
171
|
+
authentication. This proof requires the matching platform and gateway release.
|
|
172
|
+
|
|
173
|
+
`client.processes.start` accepts an optional `version` selector for a registered
|
|
174
|
+
immutable process version. Omitting it selects the active version. In the
|
|
175
|
+
upcoming SDK 0.8.1, use this selector to start a retained release after the
|
|
176
|
+
active process has been promoted.
|
|
177
|
+
|
|
178
|
+
The separate `expectedProcess: { version, bundleHash }` option asserts the
|
|
179
|
+
resolved target; it does not select a version. Atlas returns
|
|
180
|
+
`409 PROCESS_IDENTITY_MISMATCH` before creating a run with a different
|
|
181
|
+
definition. Retrying a known run ID compares its persisted definition, so a
|
|
182
|
+
lost response can recover the original run after a process promotion. Current
|
|
183
|
+
process admission and manifest capability checks still apply.
|
|
184
|
+
|
|
166
185
|
On the request-scoped client returned by `createArtifactServerClient`, declare
|
|
167
186
|
the exact process IDs the application reads or acts on:
|
|
168
187
|
|
|
@@ -350,15 +369,80 @@ until the host rollout completes. Host/session replacement closes the preview.
|
|
|
350
369
|
|
|
351
370
|
## Calling agents from a Next.js server
|
|
352
371
|
|
|
372
|
+
The upcoming SDK **0.8.1** adds typed, user-mode job admission. Obtain the
|
|
373
|
+
reviewed source tuple with `client.agents.getCurrentIdentity({ kind, id })` and
|
|
374
|
+
pass it as `expectedIdentity` to `createJob` or `createWorkflowJob`, together
|
|
375
|
+
with a stable `idempotencyKey`. An application should retain its reviewed tuple
|
|
376
|
+
and execution intent rather than silently replace a mismatched source with the
|
|
377
|
+
latest definition. The current user's permissions are checked at admission and
|
|
378
|
+
recovery; neither the tuple nor the key grants access.
|
|
379
|
+
|
|
380
|
+
Declare the exact read routes for current identity and these strict write
|
|
381
|
+
routes in the server manifest:
|
|
382
|
+
|
|
383
|
+
- `POST /api/agents/jobs/create-idempotent` for `createJob`.
|
|
384
|
+
- `POST /api/agents/workflows/*/jobs` for `createWorkflowJob`.
|
|
385
|
+
|
|
386
|
+
Keep the same key and semantic request when recovering an uncertain outcome.
|
|
387
|
+
Changed input or source pins under an existing key are refused. A matching
|
|
388
|
+
replay returns the original admission response without resolving a newer source
|
|
389
|
+
head; read the Job separately for its current execution status. These methods use distinct
|
|
390
|
+
strict endpoints so an older host fails before creating a legacy job that
|
|
391
|
+
ignores the idempotency contract. Do not fall back to the legacy create route
|
|
392
|
+
or generate another key merely because an acknowledgement was lost.
|
|
393
|
+
|
|
394
|
+
`client.agents.getCompletedRun({ agent, runId })` reads a completed response as
|
|
395
|
+
the current user. Declare its `GET /api/agents/jobs/*/completed-response` route
|
|
396
|
+
in `server.capabilities.api.read`. Both the run and private thread must belong
|
|
397
|
+
to that user, and current Agent, Job and Thread read grants remain enforced.
|
|
398
|
+
The bounded response carries JSON output, assistant text and citation resources;
|
|
399
|
+
it omits reasoning, tool inputs and arbitrary tool results. Reused or changed
|
|
400
|
+
conversations, incomplete runs and results over 1,000 messages or 8 MiB fail
|
|
401
|
+
explicitly. Service mode and machine credentials cannot use this method.
|
|
402
|
+
|
|
403
|
+
`client.agents.captureCompletedRun({ agent, runId })` returns the neutral response
|
|
404
|
+
with a historical `responseHash` and `sourceReceipt`. Explicit
|
|
405
|
+
`client.agents.captureCompletedConversation({ agent, runId })` additionally
|
|
406
|
+
captures permitted user/assistant text and tool inputs/results in
|
|
407
|
+
`response.context`. Declare the respective `completed-source` and
|
|
408
|
+
`completed-conversation` GET routes. Atlas requires current Agent, Job and Thread
|
|
409
|
+
read grants for the actual private source. These user-only captures reject changed
|
|
410
|
+
or unfinished history, recognizable credentials, unverified compaction context
|
|
411
|
+
and responses over 512 KiB. A receipt proves the captured bytes and actual actor;
|
|
412
|
+
receiving functions verify it under their own live evidence capability, and the
|
|
413
|
+
application separately validates its protected source/release association.
|
|
414
|
+
|
|
415
|
+
`client.agents.captureConversation({ agent, threadId })` explicitly captures a
|
|
416
|
+
sidebar transcript through `GET /api/agents/threads/*/conversation-source`.
|
|
417
|
+
Its response binds a durable settled `revision` and `cutoffAt`, with the same
|
|
418
|
+
permitted `context` and neutral `conversation` metrics. It has no invented agent
|
|
419
|
+
run ID. Active, interrupted or legacy conversations without proven settlement
|
|
420
|
+
are refused. The receiver uses `verifyConversationEvidence` and checks the
|
|
421
|
+
application's protected sidebar/source association before persisting it.
|
|
422
|
+
|
|
423
|
+
`client.agents.captureCompletedWorkflow({ workflow, runId })` explicitly
|
|
424
|
+
captures a completed standalone Workflow source as an authenticated human with
|
|
425
|
+
current access. The capturing user remains distinct from the original source
|
|
426
|
+
owner recorded in the evidence.
|
|
427
|
+
Declare `GET /api/agents/jobs/*/completed-workflow-source`. The typed response
|
|
428
|
+
contains the persisted merged root output and historical source evidence,
|
|
429
|
+
including real branch outcomes and the original job-admission association;
|
|
430
|
+
it is not an Agent conversation. Current Workflow Invoker and root/child Job
|
|
431
|
+
visibility remain required. Incomplete or changed sources and captures over
|
|
432
|
+
512 KiB fail explicitly. A receiver verifies the historical bytes with its
|
|
433
|
+
separate Workflow evidence capability and checks the application's current
|
|
434
|
+
protected intent; a source receipt grants no current task or write authority.
|
|
435
|
+
|
|
353
436
|
An application that declares `/api/agents/` in `server.capabilities.api.read`
|
|
354
437
|
and `write` can reach a fixed set of agents API routes as the signed-in user:
|
|
355
438
|
|
|
356
439
|
| Area | Routes |
|
|
357
440
|
| --- | --- |
|
|
358
441
|
| Chat | `POST chat`, `POST chat/abort`, `GET chat/resume` |
|
|
359
|
-
| Agents | `GET agents`, `GET agents/:id` |
|
|
360
|
-
| Threads | `GET threads`, `POST threads`, `GET threads/:id`, `GET threads/:id/messages`, `PATCH threads/:id/messages/:messageId` |
|
|
361
|
-
| Jobs | `POST jobs`, `GET jobs/:id`, `DELETE jobs/:id` |
|
|
442
|
+
| Agents | `GET agents`, `GET agents/:id`, `GET agents/:id/current-identity` |
|
|
443
|
+
| Threads | `GET threads`, `POST threads`, `GET threads/:id`, `GET threads/:id/messages`, `GET threads/:id/conversation-source`, `PATCH threads/:id/messages/:messageId` |
|
|
444
|
+
| Jobs | `POST jobs`, `POST jobs/create-idempotent`, `GET jobs/:id`, `GET jobs/:id/completed-response`, `GET jobs/:id/completed-source`, `GET jobs/:id/completed-conversation`, `GET jobs/:id/completed-workflow-source`, `DELETE jobs/:id` |
|
|
445
|
+
| Workflows | `GET workflows/:id/current-identity`, `POST workflows/:id/jobs` |
|
|
362
446
|
| Files | `POST files`, `GET files/:id`, `DELETE files/:id`, `GET files/:id/url` |
|
|
363
447
|
|
|
364
448
|
Atlas answers them with the agents API's own handlers, so agent `can_invoke`,
|
|
@@ -416,3 +500,17 @@ streaming and file-transfer helpers retain their ordinary host tracing behavior.
|
|
|
416
500
|
Tracing does not grant permissions, change service-account behavior or guarantee
|
|
417
501
|
that every transport is instrumented. Traces availability depends on the deployed
|
|
418
502
|
platform version and your app/resource permissions.
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
### Microphone dictation in an isolated application
|
|
506
|
+
|
|
507
|
+
Explicitly declare browser API read `/api/files/speech/config/get` and write
|
|
508
|
+
`/api/files/speech/stt` to opt an isolated application iframe into microphone
|
|
509
|
+
requests. Both exact paths are required; broad API wildcards do not enable it.
|
|
510
|
+
The browser still asks the user for permission. Existing sandbox origin privileges
|
|
511
|
+
are unchanged, and camera/geolocation remain unavailable. Legacy opaque-origin
|
|
512
|
+
static frames do not receive microphone delegation.
|
|
513
|
+
Upload with `api.upload` option `{ fieldName: 'audio' }`; ordinary uploads still
|
|
514
|
+
default to `file`. Atlas transcription trims silence by default; multipart
|
|
515
|
+
`trimSilence=false` opts out. Speech uses the declared host paths without requiring
|
|
516
|
+
the separate general Next host API rollout flag.
|
package/dist/api.d.ts
CHANGED
|
@@ -12,11 +12,12 @@ export declare function getJson<T>({ baseUrl, token, path }: ApiClientOptions &
|
|
|
12
12
|
export declare function getJsonOr404<T>({ baseUrl, token, path }: ApiClientOptions & {
|
|
13
13
|
path: string;
|
|
14
14
|
}): Promise<T | null>;
|
|
15
|
-
export declare function postJson<T>({ baseUrl, token, path, body, retry503, signal, }: ApiClientOptions & {
|
|
15
|
+
export declare function postJson<T>({ baseUrl, token, path, body, retry503, signal, headers, }: ApiClientOptions & {
|
|
16
16
|
path: string;
|
|
17
17
|
body?: unknown;
|
|
18
18
|
retry503?: boolean;
|
|
19
19
|
signal?: AbortSignal;
|
|
20
|
+
headers?: Record<string, string>;
|
|
20
21
|
}): Promise<T>;
|
|
21
22
|
export declare function deleteJson({ baseUrl, token, path, signal }: ApiClientOptions & {
|
|
22
23
|
path: string;
|
package/dist/api.js
CHANGED
|
@@ -19,7 +19,17 @@ const DEFAULT_RETRY_AFTER_SECONDS = 2;
|
|
|
19
19
|
* Content-Type) always win over anything here. Malformed JSON is ignored — a
|
|
20
20
|
* bad bypass header should never crash a deploy before the request is even made.
|
|
21
21
|
*/
|
|
22
|
-
function extraHeaders() {
|
|
22
|
+
function extraHeaders(baseUrl) {
|
|
23
|
+
// Preview gateway credentials belong only to the target, never to a separately
|
|
24
|
+
// authenticated source repository. An unbound header set fails closed.
|
|
25
|
+
const target = process.env.ARTIFACT_STUDIO_BASE_URL;
|
|
26
|
+
try {
|
|
27
|
+
if (!target || new URL(target).origin !== new URL(baseUrl).origin)
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
23
33
|
const raw = process.env.ARTIFACT_STUDIO_EXTRA_HEADERS?.trim();
|
|
24
34
|
if (!raw)
|
|
25
35
|
return {};
|
|
@@ -33,7 +43,7 @@ function extraHeaders() {
|
|
|
33
43
|
// Prevent case-variant duplicates from being merged by Headers/fetch
|
|
34
44
|
// (e.g. `authorization` + `Authorization` => "a, b"). The client always
|
|
35
45
|
// sets these explicitly below, so ignore any env-supplied variants.
|
|
36
|
-
if (lower === 'authorization' || lower === 'content-type')
|
|
46
|
+
if (lower === 'authorization' || lower === 'content-type' || lower === 'x-sequence-source-authorization')
|
|
37
47
|
continue;
|
|
38
48
|
if (typeof value === 'string')
|
|
39
49
|
headers[key] = value;
|
|
@@ -64,7 +74,7 @@ async function authenticatedFetch({ baseUrl, init = {}, path, token, retry503 =
|
|
|
64
74
|
...init,
|
|
65
75
|
redirect: 'manual',
|
|
66
76
|
headers: {
|
|
67
|
-
...extraHeaders(),
|
|
77
|
+
...extraHeaders(baseUrl),
|
|
68
78
|
...init.headers,
|
|
69
79
|
Authorization: `Bearer ${token}`,
|
|
70
80
|
},
|
|
@@ -84,7 +94,7 @@ export async function getJsonOr404({ baseUrl, token, path }) {
|
|
|
84
94
|
throw new ArtifactApiError(await responseError('GET', path, response), response.status);
|
|
85
95
|
return response.json();
|
|
86
96
|
}
|
|
87
|
-
export async function postJson({ baseUrl, token, path, body, retry503 = true, signal, }) {
|
|
97
|
+
export async function postJson({ baseUrl, token, path, body, retry503 = true, signal, headers, }) {
|
|
88
98
|
const response = await authenticatedFetch({
|
|
89
99
|
baseUrl,
|
|
90
100
|
token,
|
|
@@ -92,7 +102,7 @@ export async function postJson({ baseUrl, token, path, body, retry503 = true, si
|
|
|
92
102
|
retry503,
|
|
93
103
|
init: {
|
|
94
104
|
method: 'POST',
|
|
95
|
-
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
96
106
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
97
107
|
...(signal ? { signal } : {}),
|
|
98
108
|
},
|
package/dist/cli.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export declare function getConfiguredDefaultEnv(): Promise<string | undefined>;
|
|
|
32
32
|
*/
|
|
33
33
|
export interface TokenProviderOptions {
|
|
34
34
|
allowInteractiveLogin: boolean;
|
|
35
|
+
/** A separate source session; omitted for target requests. Never a caller URL. */
|
|
36
|
+
sourceEnvironment?: string;
|
|
35
37
|
}
|
|
36
38
|
export interface ProvidedToken {
|
|
37
39
|
authMode: SourceAuthMode;
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { constants, existsSync } from 'node:fs';
|
|
|
3
3
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { getJson, getJsonOr404, postJson } from './api.js';
|
|
6
|
-
import { ENV_URLS, readLocalConfig, resolveEnvironment, writeLocalConfig, } from './config.js';
|
|
6
|
+
import { ENV_URLS, knownEnvUrls, readLocalConfig, resolveEnvironment, writeLocalConfig, } from './config.js';
|
|
7
7
|
import { writeBuildArtifact } from './build.js';
|
|
8
8
|
import { buildArtifactStudioProjectIsolated } from './build-subprocess.js';
|
|
9
9
|
import { resolveArtifactStudioPeerVersions, } from './peer-versions.js';
|
|
@@ -33,9 +33,9 @@ const USAGE = `usage:
|
|
|
33
33
|
seq-studio artifact list --env <env> [--json]
|
|
34
34
|
seq-studio artifact show <slug-or-id> --env <env> [--json]
|
|
35
35
|
seq-studio artifact validate [dir]
|
|
36
|
-
seq-studio artifact build [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] [--out dist/artifact-bundle.json]
|
|
37
|
-
seq-studio artifact plan [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] --env <env>
|
|
38
|
-
seq-studio artifact deploy [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] --env <env> [--skip-unchanged] [--no-create] [--project <id>]
|
|
36
|
+
seq-studio artifact build [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] [--source-env <env>] [--out dist/artifact-bundle.json]
|
|
37
|
+
seq-studio artifact plan [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] [--source-env <env>] --env <env>
|
|
38
|
+
seq-studio artifact deploy [dir | --repo <ns>/<name> | --git-url <url>] [--ref <ref>] [--path <dir>] [--source-env <env>] --env <env> [--skip-unchanged] [--no-create] [--project <id>]
|
|
39
39
|
seq-studio artifact dev [dir] --env <env> [--once] [--key default] [--link <pkgDir>[,<pkgDir>]] [--port <port>]
|
|
40
40
|
seq-studio artifact promote <deployment-id> --env <env>
|
|
41
41
|
seq-studio artifact pull <project-id> --env <env> --out <dir>
|
|
@@ -51,7 +51,7 @@ const USAGE = `usage:
|
|
|
51
51
|
--repo / --git-url repository (default: its root). The target project comes from
|
|
52
52
|
the source's artifact.bundle.yml project_id.
|
|
53
53
|
|
|
54
|
-
Official (active) deploys
|
|
54
|
+
Official (active) deploys require \`main\` (or \`staging\` for staging targets) with a
|
|
55
55
|
resolved git commit and a clean artifact source directory. Dirty files
|
|
56
56
|
outside a nested local source directory are ignored with a warning.
|
|
57
57
|
Deployments targeting \`local\` may use any branch, including dirty working
|
|
@@ -61,7 +61,11 @@ const USAGE = `usage:
|
|
|
61
61
|
Interactive --repo builds clone over smart-HTTP using your seq-studio login
|
|
62
62
|
and repository read access. No PAT is needed. Headless M2M builds use JSON
|
|
63
63
|
materialize and accept only platform-managed --repo sources. Use --env to
|
|
64
|
-
select the
|
|
64
|
+
select the target environment. Next.js build/plan/deploy accept --source-env
|
|
65
|
+
to read a Git Service repository from a separately authenticated environment.
|
|
66
|
+
Log in to both environments; source read and target deploy permissions apply.
|
|
67
|
+
The target must support verified cross-environment builds. Source credentials
|
|
68
|
+
are used only to fetch source, never passed to the build or stored with it.
|
|
65
69
|
|
|
66
70
|
Next.js: init --template nextjs, then pnpm install with Node.js 24. dev starts
|
|
67
71
|
a local Next server and an owner-only Atlas preview tunnel (no cloud credentials).
|
|
@@ -114,9 +118,9 @@ function validateKnownOptions(args) {
|
|
|
114
118
|
link: { values: ['env', 'project'], booleans: [] }, env: { values: [], booleans: [] },
|
|
115
119
|
status: { values: ['env'], booleans: [] }, list: { values: ['env'], booleans: ['json'] },
|
|
116
120
|
show: { values: ['env'], booleans: ['json'] }, validate: { values: [], booleans: [] },
|
|
117
|
-
build: { values: ['env', 'repo', 'git-url', 'ref', 'path', 'out'], booleans: [] },
|
|
118
|
-
plan: { values: ['env', 'repo', 'git-url', 'ref', 'path'], booleans: [] },
|
|
119
|
-
deploy: { values: ['env', 'repo', 'git-url', 'ref', 'path', 'project', 'out'], booleans: ['skip-unchanged', 'no-create'] },
|
|
121
|
+
build: { values: ['source-env', 'env', 'repo', 'git-url', 'ref', 'path', 'out'], booleans: [] },
|
|
122
|
+
plan: { values: ['source-env', 'env', 'repo', 'git-url', 'ref', 'path'], booleans: [] },
|
|
123
|
+
deploy: { values: ['source-env', 'env', 'repo', 'git-url', 'ref', 'path', 'project', 'out'], booleans: ['skip-unchanged', 'no-create'] },
|
|
120
124
|
dev: { values: ['env', 'key', 'link', 'port'], booleans: ['once'] },
|
|
121
125
|
promote: { values: ['env'], booleans: [] }, pull: { values: ['env', 'out'], booleans: [] },
|
|
122
126
|
rollback: { values: ['env'], booleans: [] },
|
|
@@ -441,16 +445,17 @@ function reportBuildDiagnostics(result) {
|
|
|
441
445
|
}
|
|
442
446
|
async function buildCommand(args) {
|
|
443
447
|
const spec = parseSourceSpec(args);
|
|
448
|
+
const sourceEnvironment = sourceEnvironmentFromArgs(args);
|
|
444
449
|
// Local builds stay offline. Public git URLs need no bearer token, but an
|
|
445
450
|
// optional auth lookup still identifies a selected M2M principal so the
|
|
446
451
|
// source policy can reject arbitrary CI input before cloning.
|
|
447
|
-
const auth = spec.kind === 'git-service'
|
|
448
|
-
? await
|
|
452
|
+
const auth = spec.kind === 'git-service' || sourceEnvironment
|
|
453
|
+
? await getSourceAuth(args)
|
|
449
454
|
: spec.kind === 'git-url'
|
|
450
455
|
? await getOptionalAuth(args)
|
|
451
456
|
: null;
|
|
452
457
|
const remote = spec.kind === 'git-service' && auth
|
|
453
|
-
? { authMode: auth.authMode, baseUrl: (await envFromArgs(args)).url, token: auth.token }
|
|
458
|
+
? { authMode: auth.authMode, baseUrl: (sourceEnvironment ?? await envFromArgs(args)).url, token: auth.token }
|
|
454
459
|
: auth
|
|
455
460
|
? { authMode: auth.authMode }
|
|
456
461
|
: {};
|
|
@@ -459,6 +464,7 @@ async function buildCommand(args) {
|
|
|
459
464
|
if (isNextArtifactManifest(await readArtifactStudioManifest(source.dir))) {
|
|
460
465
|
return await buildNextCommand({ args, source, active: false });
|
|
461
466
|
}
|
|
467
|
+
assertSourceEnvironmentSupportsManifest(args);
|
|
462
468
|
const result = await buildPreparedArtifactSource(source);
|
|
463
469
|
reportBuildDiagnostics(result);
|
|
464
470
|
// A remote source's dir is a temp dir we tear down below, so default the
|
|
@@ -483,10 +489,12 @@ async function planCommand(args) {
|
|
|
483
489
|
const auth = await getOptionalAuth(args);
|
|
484
490
|
const token = auth?.token ?? null;
|
|
485
491
|
const spec = parseSourceSpec(args);
|
|
492
|
+
const sourceEnvironment = sourceEnvironmentFromArgs(args);
|
|
493
|
+
const sourceAuth = sourceEnvironment ? await getSourceAuth(args) : auth;
|
|
486
494
|
const source = await resolveArtifactSource(spec, {
|
|
487
|
-
...(
|
|
488
|
-
baseUrl: env.url,
|
|
489
|
-
token,
|
|
495
|
+
...(sourceAuth ? { authMode: sourceAuth.authMode } : {}),
|
|
496
|
+
baseUrl: (sourceEnvironment ?? env).url,
|
|
497
|
+
token: sourceAuth?.token ?? null,
|
|
490
498
|
});
|
|
491
499
|
try {
|
|
492
500
|
const manifest = await readArtifactStudioManifest(source.dir);
|
|
@@ -494,6 +502,7 @@ async function planCommand(args) {
|
|
|
494
502
|
const { planNextProject } = await import('./next-build.js');
|
|
495
503
|
return await planNextProject({ root: source.dir, manifest, environment: env.name });
|
|
496
504
|
}
|
|
505
|
+
assertSourceEnvironmentSupportsManifest(args);
|
|
497
506
|
const result = await buildPreparedArtifactSource(source);
|
|
498
507
|
reportBuildDiagnostics(result);
|
|
499
508
|
console.log(`[seq-studio] plan ${result.manifest.artifact.project_id} -> ${env.name}`);
|
|
@@ -539,10 +548,12 @@ async function deployCommand(args) {
|
|
|
539
548
|
const { token } = auth;
|
|
540
549
|
const spec = parseSourceSpec(args);
|
|
541
550
|
const explicitProjectId = typeof args.flags.project === 'string' ? args.flags.project : null;
|
|
551
|
+
const sourceEnvironment = sourceEnvironmentFromArgs(args);
|
|
552
|
+
const sourceAuth = sourceEnvironment ? await getSourceAuth(args) : auth;
|
|
542
553
|
const source = await resolveArtifactSource(spec, {
|
|
543
|
-
authMode:
|
|
544
|
-
baseUrl: env.url,
|
|
545
|
-
token,
|
|
554
|
+
authMode: sourceAuth.authMode,
|
|
555
|
+
baseUrl: (sourceEnvironment ?? env).url,
|
|
556
|
+
token: sourceAuth.token,
|
|
546
557
|
});
|
|
547
558
|
try {
|
|
548
559
|
// Fail closed before build / project create for deployed environments.
|
|
@@ -558,6 +569,7 @@ async function deployCommand(args) {
|
|
|
558
569
|
if (isNextArtifactManifest(manifest)) {
|
|
559
570
|
return await buildNextCommand({ args, source, active: true });
|
|
560
571
|
}
|
|
572
|
+
assertSourceEnvironmentSupportsManifest(args);
|
|
561
573
|
// --skip-unchanged: hash the raw source (no build needed) and no-op when
|
|
562
574
|
// the remote active deployment already matches *and* was built with the
|
|
563
575
|
// same CLI/atlas-ui pin. Lets CI redeploy every artifact on every release
|
|
@@ -618,8 +630,10 @@ async function buildNextCommand({ args, source, active }) {
|
|
|
618
630
|
if (active) {
|
|
619
631
|
assertActiveDeployProvenance({ ...source.provenance, environment: env.name });
|
|
620
632
|
}
|
|
621
|
-
const
|
|
622
|
-
|
|
633
|
+
const sourceEnvironment = sourceEnvironmentFromArgs(args);
|
|
634
|
+
const sourceToken = sourceEnvironment ? (await getSourceAuth(args)).token : token;
|
|
635
|
+
const repositoryId = active || spec.kind === 'git-service' || sourceEnvironment
|
|
636
|
+
? await resolveNextRepositoryId({ spec, repository, baseUrl: (sourceEnvironment ?? env).url, token: sourceToken })
|
|
623
637
|
: undefined;
|
|
624
638
|
const project = await ensureRemoteProject({
|
|
625
639
|
env: env.url, token, result: { manifest }, linkConfigDir: spec.kind === 'local' ? source.dir : null,
|
|
@@ -632,6 +646,7 @@ async function buildNextCommand({ args, source, active }) {
|
|
|
632
646
|
return submitNextBuild({
|
|
633
647
|
root: source.dir, sourceRoot: source.sourceRoot, manifest, projectId: project.id, baseUrl: env.url,
|
|
634
648
|
getToken: () => getRequiredToken(args), provenance: source.provenance, repository, repositoryId,
|
|
649
|
+
...(sourceEnvironment ? { sourceEnvironment: sourceEnvironment.name, sourceToken } : {}),
|
|
635
650
|
channel: active ? 'active' : 'preview', out: typeof args.flags.out === 'string' ? resolve(args.flags.out) : undefined,
|
|
636
651
|
});
|
|
637
652
|
}
|
|
@@ -1027,6 +1042,29 @@ export async function getConfiguredDefaultEnv() {
|
|
|
1027
1042
|
async function envFromArgs(args) {
|
|
1028
1043
|
return resolveEnvironment(typeof args.flags.env === 'string' ? args.flags.env : undefined, await getConfiguredDefaultEnv());
|
|
1029
1044
|
}
|
|
1045
|
+
/** Source selection must never inherit the target URL override. */
|
|
1046
|
+
function sourceEnvironmentFromArgs(args) {
|
|
1047
|
+
const name = args.flags['source-env'];
|
|
1048
|
+
if (typeof name !== 'string')
|
|
1049
|
+
return undefined;
|
|
1050
|
+
const url = knownEnvUrls()[name];
|
|
1051
|
+
if (typeof url !== 'string' || !url)
|
|
1052
|
+
throw new Error(`Unknown source environment "${name}". Run seq-studio envs list and login to the source environment.`);
|
|
1053
|
+
return { name, url };
|
|
1054
|
+
}
|
|
1055
|
+
async function getSourceAuth(args) {
|
|
1056
|
+
const source = sourceEnvironmentFromArgs(args);
|
|
1057
|
+
if (!source)
|
|
1058
|
+
return getRequiredAuth(args);
|
|
1059
|
+
const auth = await resolveAuth({ allowInteractiveLogin: true, sourceEnvironment: source.name });
|
|
1060
|
+
if (!auth)
|
|
1061
|
+
throw new Error(`Authenticate to the source with seq-studio login -e ${source.name}.`);
|
|
1062
|
+
return auth;
|
|
1063
|
+
}
|
|
1064
|
+
function assertSourceEnvironmentSupportsManifest(args) {
|
|
1065
|
+
if (args.flags['source-env'])
|
|
1066
|
+
throw new Error('--source-env is supported for Next.js repository builds only.');
|
|
1067
|
+
}
|
|
1030
1068
|
let tokenProvider = null;
|
|
1031
1069
|
export function setTokenProvider(provider) {
|
|
1032
1070
|
tokenProvider = provider;
|
|
@@ -121,7 +121,7 @@ function digest(parts) {
|
|
|
121
121
|
* bridge values and the cookie's own expiry do not change the decision. */
|
|
122
122
|
export function gatewayAuthorizationKey(session) {
|
|
123
123
|
return digest(['session-v1', session.kind, session.projectId, session.ownerId, session.environment, session.publicOrigin,
|
|
124
|
-
session.sessionId, session.deploymentId, session.frameworkBuildId, session.published, session.lattice ?? null, session.taskRoutes ?? null]);
|
|
124
|
+
session.sessionId, session.deploymentId, session.frameworkBuildId, session.published, session.lattice ?? null, session.taskRoutes ?? null, session.authentication ?? null]);
|
|
125
125
|
}
|
|
126
126
|
/** A tunnel credential is keyed by its hash, never retained in the key itself. */
|
|
127
127
|
export function tunnelAuthorizationKey({ token, sessionId, epoch }) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { TunnelLease } from '../tunnel/index.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type GatewaySession } from './session.js';
|
|
4
4
|
import type { GatewayUsageEvent } from './usage.js';
|
|
5
5
|
import { type IngressAuditBatch, type IngressExchangeRequest, type IngressExchangeResponse, type IngressGrantRequest, type IngressGrantResponse, type IngressRevokeRequest, type IngressSnapshot } from '../server/ingress-control-contract.js';
|
|
6
6
|
export declare const gatewayTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -22,6 +22,10 @@ export declare const gatewayTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
22
22
|
processId: z.ZodNullable<z.ZodString>;
|
|
23
23
|
}, z.core.$strict>>;
|
|
24
24
|
taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
25
|
+
authentication: z.ZodOptional<z.ZodObject<{
|
|
26
|
+
method: z.ZodLiteral<"auth0_user">;
|
|
27
|
+
sub: z.ZodString;
|
|
28
|
+
}, z.core.$strict>>;
|
|
25
29
|
manifest: z.ZodOptional<z.ZodObject<{
|
|
26
30
|
server: z.ZodObject<{
|
|
27
31
|
resources: z.ZodObject<{
|
|
@@ -58,6 +62,10 @@ export declare const gatewayTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
58
62
|
processId: z.ZodNullable<z.ZodString>;
|
|
59
63
|
}, z.core.$strict>>;
|
|
60
64
|
taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
65
|
+
authentication: z.ZodOptional<z.ZodObject<{
|
|
66
|
+
method: z.ZodLiteral<"auth0_user">;
|
|
67
|
+
sub: z.ZodString;
|
|
68
|
+
}, z.core.$strict>>;
|
|
61
69
|
manifest: z.ZodOptional<z.ZodObject<{
|
|
62
70
|
server: z.ZodObject<{
|
|
63
71
|
resources: z.ZodObject<{
|
|
@@ -88,6 +96,10 @@ export declare const gatewayTargetSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
88
96
|
processId: z.ZodNullable<z.ZodString>;
|
|
89
97
|
}, z.core.$strict>>;
|
|
90
98
|
taskRoutes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
99
|
+
authentication: z.ZodOptional<z.ZodObject<{
|
|
100
|
+
method: z.ZodLiteral<"auth0_user">;
|
|
101
|
+
sub: z.ZodString;
|
|
102
|
+
}, z.core.$strict>>;
|
|
91
103
|
manifest: z.ZodOptional<z.ZodObject<{
|
|
92
104
|
server: z.ZodObject<{
|
|
93
105
|
resources: z.ZodObject<{
|
package/dist/gateway/control.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { normalizePublicOrigin } from '../tunnel/protocol.js';
|
|
4
|
+
import { gatewayAuthenticationSchema } from './session.js';
|
|
4
5
|
import { artifactRequestContextSchema } from '../server/request-context.js';
|
|
5
6
|
import { ARTIFACT_NEXT_CONTROL_PATH, artifactNextFrameworkBuildIdSchema } from '../runtime-contracts.js';
|
|
6
7
|
import { untilAborted } from './abort.js';
|
|
@@ -13,6 +14,7 @@ const base = {
|
|
|
13
14
|
capabilities: z.array(id).max(256).default([]), serviceAccountId: id.nullable().default(null),
|
|
14
15
|
lattice: artifactRequestContextSchema.shape.lattice.optional(),
|
|
15
16
|
taskRoutes: artifactRequestContextSchema.shape.taskRoutes,
|
|
17
|
+
authentication: gatewayAuthenticationSchema.optional(),
|
|
16
18
|
manifest: z.object({ server: z.object({ resources: z.object({
|
|
17
19
|
concurrency: z.number().int().min(1).max(80), max_instances: z.number().int().min(1).max(100), timeout_seconds: z.number().int().min(1).max(300),
|
|
18
20
|
}) }) }).optional(),
|
|
@@ -228,6 +230,7 @@ export class ArtifactGatewayControl {
|
|
|
228
230
|
published: target.kind !== 'development' && (target.published ?? target.active), bridge,
|
|
229
231
|
...(target.lattice ? { lattice: target.lattice } : {}),
|
|
230
232
|
...(target.taskRoutes ? { taskRoutes: target.taskRoutes } : {}),
|
|
233
|
+
...(target.authentication ? { authentication: target.authentication } : {}),
|
|
231
234
|
};
|
|
232
235
|
return { session, target };
|
|
233
236
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Messages contain configuration names, never supplied values or connection errors. */
|
|
2
|
+
export declare class DatadogStartupError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Replaces the sidecar's TCP startup probe only in the opt-in image mode.
|
|
6
|
+
* The app remains serverless-init's direct child, so its existing signal and
|
|
7
|
+
* drain handlers still own shutdown. This checks collector startup, not remote
|
|
8
|
+
* telemetry delivery; the app's /health probe continues to check its own readiness.
|
|
9
|
+
*/
|
|
10
|
+
export declare function waitForDatadogStartup({ env, timeoutMs, }: {
|
|
11
|
+
env: Record<string, string | undefined>;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createConnection } from 'node:net';
|
|
2
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
3
|
+
/** Messages contain configuration names, never supplied values or connection errors. */
|
|
4
|
+
export class DatadogStartupError extends Error {
|
|
5
|
+
}
|
|
6
|
+
function collectorListening({ port, timeoutMs }) {
|
|
7
|
+
return new Promise(resolve => {
|
|
8
|
+
const socket = createConnection({ host: '127.0.0.1', port });
|
|
9
|
+
let finished = false;
|
|
10
|
+
const finish = (ready) => {
|
|
11
|
+
if (finished)
|
|
12
|
+
return;
|
|
13
|
+
finished = true;
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
socket.destroy();
|
|
16
|
+
resolve(ready);
|
|
17
|
+
};
|
|
18
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
19
|
+
socket.once('connect', () => finish(true));
|
|
20
|
+
socket.once('error', () => finish(false));
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Replaces the sidecar's TCP startup probe only in the opt-in image mode.
|
|
25
|
+
* The app remains serverless-init's direct child, so its existing signal and
|
|
26
|
+
* drain handlers still own shutdown. This checks collector startup, not remote
|
|
27
|
+
* telemetry delivery; the app's /health probe continues to check its own readiness.
|
|
28
|
+
*/
|
|
29
|
+
export async function waitForDatadogStartup({ env, timeoutMs = 30_000, }) {
|
|
30
|
+
const mode = env.ARTIFACT_NEXT_DATADOG_IN_CONTAINER;
|
|
31
|
+
if (mode === undefined || mode === 'false')
|
|
32
|
+
return;
|
|
33
|
+
if (mode !== 'true')
|
|
34
|
+
throw new DatadogStartupError('Invalid ARTIFACT_NEXT_DATADOG_IN_CONTAINER: expected true or false');
|
|
35
|
+
const configuredPort = env.DD_HEALTH_PORT ?? '5555';
|
|
36
|
+
const port = Number(configuredPort);
|
|
37
|
+
if (!/^\d+$/.test(configuredPort) || !Number.isSafeInteger(port) || port < 1 || port > 65535)
|
|
38
|
+
throw new DatadogStartupError('Invalid DD_HEALTH_PORT: expected an integer from 1 to 65535');
|
|
39
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 30_000)
|
|
40
|
+
throw new DatadogStartupError('Invalid Datadog startup budget: expected at most 30000 milliseconds');
|
|
41
|
+
// A monotonic deadline includes both connection attempts and retry delays.
|
|
42
|
+
const deadline = performance.now() + timeoutMs;
|
|
43
|
+
while (performance.now() < deadline) {
|
|
44
|
+
if (await collectorListening({ port, timeoutMs: Math.min(1_000, deadline - performance.now()) }))
|
|
45
|
+
return;
|
|
46
|
+
const remaining = deadline - performance.now();
|
|
47
|
+
if (remaining > 0)
|
|
48
|
+
await delay(Math.min(100, remaining));
|
|
49
|
+
}
|
|
50
|
+
throw new DatadogStartupError('Datadog collector did not become ready within the startup budget');
|
|
51
|
+
}
|
|
@@ -18,6 +18,8 @@ export interface HostedArtifactProxyOptions {
|
|
|
18
18
|
deploymentId: string;
|
|
19
19
|
};
|
|
20
20
|
requestContext: string;
|
|
21
|
+
/** Optional actual-human proof generated alongside this exact request context by the gateway. */
|
|
22
|
+
requestAuthentication?: string;
|
|
21
23
|
/** Admission may inspect a native form prefix and replay it without buffering the body. */
|
|
22
24
|
body?: AsyncIterable<Uint8Array>;
|
|
23
25
|
identityTokenProvider?: (input: {
|
|
@@ -9,6 +9,7 @@ import { createReleaseFormTransform } from '../server/release-forms.js';
|
|
|
9
9
|
import { declaredContentLength, INGRESS_PROXY_ERROR_CODES, INGRESS_PROXY_LIMITS, writeIngressResponseHead } from './ingress-response.js';
|
|
10
10
|
import { resolveHostedTarget } from './runtime-transport.js';
|
|
11
11
|
import { gatewayElapsedMs } from './usage.js';
|
|
12
|
+
import { ArtifactRequestHeaderError, appendArtifactAuthenticationHeader, assertArtifactRequestHeaderBudget, stripArtifactCloudHeaders, validatedArtifactAuthenticationHeader } from '../server/request-authentication.js';
|
|
12
13
|
export { cloudRunOrigin, resolveHostedTarget } from './runtime-transport.js';
|
|
13
14
|
export const HOSTED_ARTIFACT_PROXY_LIMITS = Object.freeze({ maxRequestBytes: 32 * 1024 * 1024, maxResponseBytes: 32 * 1024 * 1024, timeoutMs: 60_000, maxTimeoutMs: 300_000 });
|
|
14
15
|
const googleAuth = new GoogleAuth();
|
|
@@ -40,12 +41,6 @@ async function defaultIdentityTokenProvider({ audience }) {
|
|
|
40
41
|
export function hostedIdentityToken(input) {
|
|
41
42
|
return defaultIdentityTokenProvider(input);
|
|
42
43
|
}
|
|
43
|
-
function stripCloudHeaders(headers) {
|
|
44
|
-
for (const name of Object.keys(headers)) {
|
|
45
|
-
if (name === 'x-serverless-authorization' || name.startsWith('x-goog-'))
|
|
46
|
-
delete headers[name];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
44
|
/** HTTP-only streaming hop. Authentication, CSRF and release admission happen before calling this. */
|
|
50
45
|
export function proxyHostedArtifact(options) {
|
|
51
46
|
return new HostedExchange(options).run();
|
|
@@ -92,11 +87,11 @@ class HostedExchange {
|
|
|
92
87
|
request.once('error', this.finish);
|
|
93
88
|
this.timer = setTimeout(() => this.fail(504), this.timeoutMs);
|
|
94
89
|
this.timer.unref();
|
|
95
|
-
void this.prepare().catch(
|
|
90
|
+
void this.prepare().catch(error => this.fail(error instanceof ArtifactRequestHeaderError ? 400 : this.unavailableStatus));
|
|
96
91
|
});
|
|
97
92
|
}
|
|
98
93
|
async prepare() {
|
|
99
|
-
const { request, response, target, requestContext } = this.options;
|
|
94
|
+
const { request, response, target, requestContext, requestAuthentication } = this.options;
|
|
100
95
|
const { upstream, audience } = resolveHostedTarget({ target });
|
|
101
96
|
// Cloud Run revision tags select immutable transport destinations, but IAM
|
|
102
97
|
// requires the canonical service audience. Both are control-plane owned;
|
|
@@ -107,6 +102,7 @@ class HostedExchange {
|
|
|
107
102
|
validateHeaderValue('x-sequence-artifact-context', requestContext);
|
|
108
103
|
if (!requestContext || requestContext.length > 16384)
|
|
109
104
|
throw new Error('Invalid request context');
|
|
105
|
+
validatedArtifactAuthenticationHeader({ token: requestAuthentication, contextToken: requestContext, external: Boolean(this.options.ingress) });
|
|
110
106
|
}
|
|
111
107
|
catch {
|
|
112
108
|
this.fail(400);
|
|
@@ -127,7 +123,9 @@ class HostedExchange {
|
|
|
127
123
|
this.fail(this.options.ingress ? 400 : 502);
|
|
128
124
|
return;
|
|
129
125
|
}
|
|
130
|
-
|
|
126
|
+
stripArtifactCloudHeaders({ headers });
|
|
127
|
+
headers['x-sequence-artifact-context'] = requestContext;
|
|
128
|
+
appendArtifactAuthenticationHeader({ headers, token: requestAuthentication, contextToken: requestContext });
|
|
131
129
|
if (audience) {
|
|
132
130
|
const provider = this.options.identityTokenProvider ?? defaultIdentityTokenProvider;
|
|
133
131
|
const identityStarted = performance.now();
|
|
@@ -147,7 +145,7 @@ class HostedExchange {
|
|
|
147
145
|
// HTML forms receive release markers. Avoid compression when possible;
|
|
148
146
|
// runtimes that still compress HTML are decoded as a bounded stream below.
|
|
149
147
|
headers['accept-encoding'] = 'identity';
|
|
150
|
-
headers
|
|
148
|
+
assertArtifactRequestHeaderBudget({ headers });
|
|
151
149
|
const destination = this.resolveDestination(upstream);
|
|
152
150
|
const transport = destination.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
153
151
|
const upstreamStarted = performance.now();
|
|
@@ -204,7 +202,7 @@ class HostedExchange {
|
|
|
204
202
|
// Read before sanitising, which removes content-length as a hop header.
|
|
205
203
|
const declaredLength = declaredContentLength(incoming.headers['content-length']);
|
|
206
204
|
const headers = sanitizeResponseHeaders(incoming.headers);
|
|
207
|
-
|
|
205
|
+
stripArtifactCloudHeaders({ headers });
|
|
208
206
|
const response = this.options.response;
|
|
209
207
|
if (this.options.ingress) {
|
|
210
208
|
// Decided on the head, before any body byte; never the HTML release-marker path.
|
|
@@ -191,6 +191,8 @@ export function createIngressHandler({ options, runtimeDomain, protocol, isDrain
|
|
|
191
191
|
};
|
|
192
192
|
response.once('finish', report);
|
|
193
193
|
response.once('close', report);
|
|
194
|
+
if (response.destroyed)
|
|
195
|
+
report();
|
|
194
196
|
}
|
|
195
197
|
/** Preflights are answered locally and never forwarded; a refusal carries no CORS headers. */
|
|
196
198
|
function preflight({ request, response, origin, allowed, methods, headers }) {
|
package/dist/gateway/server.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export interface ArtifactGatewayOptions {
|
|
|
41
41
|
}
|
|
42
42
|
export declare function createArtifactGateway(options: ArtifactGatewayOptions): {
|
|
43
43
|
server: import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
44
|
-
/**
|
|
44
|
+
/** Recorded observations not yet confirmed by Atlas; excludes responses still in flight. */
|
|
45
45
|
readonly pendingUsage: number;
|
|
46
46
|
drain(): Promise<void>;
|
|
47
47
|
};
|