@sequenceholdings/artifact-studio 0.5.0 → 0.6.2
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 +43 -0
- package/VERSION-PIN.md +27 -0
- package/dist/cli.js +1 -1
- package/dist/manifest-next-ingress.d.ts +213 -0
- package/dist/manifest-next-ingress.js +187 -0
- package/dist/manifest-next-reach.d.ts +40 -0
- package/dist/manifest-next-reach.js +48 -0
- package/dist/manifest-next.d.ts +133 -8
- package/dist/manifest-next.js +7 -46
- package/dist/manifest-static.d.ts +6 -0
- package/dist/manifest-static.js +10 -0
- package/dist/manifest.d.ts +67 -0
- package/dist/manifest.js +7 -0
- package/dist/offline/connector.d.ts +2 -0
- package/dist/offline/connector.js +6 -2
- package/dist/offline/management-types.d.ts +25 -0
- package/dist/offline/management-types.js +1 -0
- package/dist/runtime-contracts.js +1 -1
- package/dist/sdk.d.ts +8 -1
- package/dist/server/api-client.d.ts +27 -0
- package/dist/server/api-client.js +39 -0
- package/dist/server/broker-contract.d.ts +33 -6
- package/dist/server/broker-contract.js +20 -2
- package/dist/server/client.d.ts +32 -45
- package/dist/server/client.js +72 -101
- package/dist/server/connection-retry.d.ts +12 -0
- package/dist/server/connection-retry.js +40 -0
- package/dist/server/errors.d.ts +37 -2
- package/dist/server/errors.js +66 -6
- package/dist/server/external-context.d.ts +21 -0
- package/dist/server/external-context.js +19 -0
- package/dist/server/ingress-control-contract.d.ts +272 -0
- package/dist/server/ingress-control-contract.js +196 -0
- package/dist/server/ingress-routes.d.ts +76 -0
- package/dist/server/ingress-routes.js +150 -0
- package/dist/server/internal/signed-token.d.ts +15 -0
- package/dist/server/internal/signed-token.js +67 -0
- package/dist/server/internal/v1-context.d.ts +18 -0
- package/dist/server/internal/v1-context.js +26 -0
- package/dist/server/internal/verify-any.d.ts +22 -0
- package/dist/server/internal/verify-any.js +61 -0
- package/dist/server/logger.d.ts +1 -1
- package/dist/server/principal.d.ts +48 -0
- package/dist/server/principal.js +39 -0
- package/dist/server/processes.d.ts +56 -0
- package/dist/server/processes.js +13 -0
- package/dist/server/request-context.d.ts +121 -5
- package/dist/server/request-context.js +105 -34
- package/dist/server/runtime-config.d.ts +1 -1
- package/dist/server/testing.d.ts +40 -0
- package/dist/server/testing.js +86 -0
- package/dist/server.d.ts +9 -4
- package/dist/server.js +5 -2
- package/dist/templates/nextjs/README.md +9 -0
- package/dist/templates/nextjs/package.json +3 -1
- package/dist/templates/nextjs/tests/server.test.mjs +25 -0
- package/dist/templates/react-vite/package.json +1 -1
- package/dist/tunnel/protocol.js +2 -4
- package/dist/tunnel/reserved-headers.d.ts +7 -0
- package/dist/tunnel/reserved-headers.js +20 -0
- package/package.json +19 -3
- package/templates/nextjs/README.md +9 -0
- package/templates/nextjs/package.json +3 -1
- package/templates/nextjs/tests/server.test.mjs +25 -0
- package/templates/react-vite/package.json +1 -1
|
@@ -31,6 +31,8 @@ const latticeVersion = z.string().min(1).max(128).regex(/^[A-Za-z0-9._-]+$/);
|
|
|
31
31
|
export const ARTIFACT_PROCESS_RUN_STATUSES = ['pending', 'running', 'completed', 'failed', 'cancelled', 'timed_out'];
|
|
32
32
|
/** Lattice lists at most this many runs per call. */
|
|
33
33
|
export const ARTIFACT_PROCESS_RUNS_LIMIT = 500;
|
|
34
|
+
/** A run-events read returns at most this many events per call, as the run's stream drains them. */
|
|
35
|
+
export const ARTIFACT_PROCESS_RUN_EVENTS_LIMIT = 1000;
|
|
34
36
|
/** Which of the user's human steps to list: parked and waiting on them, or already done. */
|
|
35
37
|
export const ARTIFACT_PROCESS_TASK_VIEWS = ['active', 'completed'];
|
|
36
38
|
/** Lattice lists at most this many human steps per call, as its own inbox does. */
|
|
@@ -106,10 +108,12 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
|
|
|
106
108
|
}, 'Expected a Storage API path'), body: z.unknown().optional() }).strict(),
|
|
107
109
|
z.object({ operation: z.literal('function'), mode, id, params: z.record(z.string(), z.unknown()).optional() }).strict(),
|
|
108
110
|
// Starts a run of a declared process, as `POST /api/lattice/runs` does: the application names the
|
|
109
|
-
// run, passes its input
|
|
110
|
-
//
|
|
111
|
+
// run, passes its input, may seed who works each human node and may enter the graph at a node
|
|
112
|
+
// other than the process's start (the route's `start_node_id`). The version and engine stay with
|
|
113
|
+
// the process and its owning routes.
|
|
111
114
|
z.object({
|
|
112
115
|
operation: z.literal('lattice'), mode, process: latticeId, runId: latticeId.optional(),
|
|
116
|
+
startNodeId: z.string().min(1).max(255).regex(/^[A-Za-z0-9._-]+$/).optional(),
|
|
113
117
|
title: z.string().max(500).optional(),
|
|
114
118
|
entity: z.object({ type: entityRef, id: entityRef }).strict().optional(),
|
|
115
119
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -127,6 +131,14 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
|
|
|
127
131
|
// Reads one run the user may view, as `GET /api/lattice/runs/[runId]` does: the run, its steps, the
|
|
128
132
|
// collapsed per-node states and the event cursor to resume its stream from.
|
|
129
133
|
z.object({ operation: z.literal('lattice_run'), mode, runId: latticeId }).strict(),
|
|
134
|
+
// A page of a run's events after a cursor, oldest first, as `GET /api/lattice/runs/[runId]/stream`
|
|
135
|
+
// sends them: `since` is the cursor `lattice_run` returned, or the one the previous page returned.
|
|
136
|
+
// Polling with it sees every event exactly once. The user's Viewer relation on the run decides.
|
|
137
|
+
z.object({
|
|
138
|
+
operation: z.literal('lattice_run_events'), mode, runId: latticeId,
|
|
139
|
+
since: z.string().min(1).max(512).optional(),
|
|
140
|
+
limit: z.number().int().min(1).max(ARTIFACT_PROCESS_RUN_EVENTS_LIMIT).optional(),
|
|
141
|
+
}).strict(),
|
|
130
142
|
// Describes a declared process — the applied template its runs are filled in from: nodes, edges and
|
|
131
143
|
// what a person working each human step is shown. The active version, or the one a run pinned.
|
|
132
144
|
z.object({ operation: z.literal('lattice_process'), mode, process: latticeId, version: latticeVersion.optional() }).strict(),
|
|
@@ -150,6 +162,12 @@ export const artifactServerRequestSchema = z.discriminatedUnion('operation', [
|
|
|
150
162
|
// The user's run `owner` relation decides; the node's assignee allowlist still applies.
|
|
151
163
|
z.object({ operation: z.literal('lattice_assign'), mode, runId: latticeId, stepId: latticeId, ...principalSet }).strict()
|
|
152
164
|
.refine(namesPrincipals, PRINCIPAL_SET_MESSAGE),
|
|
165
|
+
// Cancels a run of a process declared under `processes.act`, with its forked lineage, as
|
|
166
|
+
// `POST /api/lattice/runs/[runId]/cancel` does. The user's Invoker relation on the run decides.
|
|
167
|
+
z.object({ operation: z.literal('lattice_cancel'), mode, runId: latticeId }).strict(),
|
|
168
|
+
// Resets a generating agent step without cancelling its run, as `POST .../steps/[stepId]/reset`
|
|
169
|
+
// does: the step settles and the run re-parks at the node's reset edge. Invoker-gated.
|
|
170
|
+
z.object({ operation: z.literal('lattice_reset_step'), mode, runId: latticeId, stepId: latticeId }).strict(),
|
|
153
171
|
// The explicit viewer grants on a run, for its owner.
|
|
154
172
|
z.object({ operation: z.literal('lattice_viewers'), mode, runId: latticeId }).strict(),
|
|
155
173
|
// Shares a run with users and groups (grants `viewer`), or takes a share back. Owner-gated;
|
package/dist/server/client.d.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { storageClient } from '@sequenceholdings/storage';
|
|
2
2
|
import { type OperationExecutor } from '@sequenceholdings/orm/client';
|
|
3
|
-
import type
|
|
4
|
-
import { type ArtifactAuditAction, type ArtifactServerMode, type ArtifactServerRequest } from './broker-contract.js';
|
|
3
|
+
import { type ArtifactAuditAction, type ArtifactServerMode } from './broker-contract.js';
|
|
5
4
|
import { type ArtifactServerProcesses } from './processes.js';
|
|
6
5
|
import { type ArtifactOrmJsonScalars } from '../orm-json-scalars.js';
|
|
7
6
|
import { createArtifactLogger, type ArtifactLogRecord } from './logger.js';
|
|
8
|
-
import { type ArtifactRequestContext } from './request-context.js';
|
|
7
|
+
import { type ArtifactRequestContext, type RequestContextV2External } from './request-context.js';
|
|
8
|
+
import { type ArtifactExternalAccept } from './external-context.js';
|
|
9
|
+
import { createApiClient } from './api-client.js';
|
|
10
|
+
import { type ArtifactPrincipal } from './principal.js';
|
|
9
11
|
import { type ArtifactRuntimeConfig, type ArtifactRequestHeaders } from './runtime-config.js';
|
|
10
|
-
export { ArtifactServerError } from './errors.js';
|
|
12
|
+
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './errors.js';
|
|
11
13
|
export type * from './processes.js';
|
|
12
|
-
export
|
|
13
|
-
method?: 'GET' | 'HEAD' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
14
|
-
body?: unknown;
|
|
15
|
-
signal?: AbortSignal;
|
|
16
|
-
}
|
|
14
|
+
export type { ArtifactServerFetchOptions } from './api-client.js';
|
|
17
15
|
/**
|
|
18
16
|
* The namespace's generated operations.manifest.json. Only each operation's persisted hash
|
|
19
17
|
* is read, so the file can be passed as imported: a JSON import types `kind` as string.
|
|
@@ -71,8 +69,11 @@ export interface ArtifactAuditEvent {
|
|
|
71
69
|
export interface ArtifactServerAudit {
|
|
72
70
|
record(event: ArtifactAuditEvent): Promise<void>;
|
|
73
71
|
}
|
|
74
|
-
export
|
|
75
|
-
|
|
72
|
+
export type ArtifactServerContext = ArtifactRequestContext | RequestContextV2External;
|
|
73
|
+
export interface ArtifactServerClient<TContext extends ArtifactServerContext = ArtifactRequestContext> {
|
|
74
|
+
context: TContext;
|
|
75
|
+
/** Who the request acts for. An `external` principal reaches platform services only through `asService()`. */
|
|
76
|
+
principal: ArtifactPrincipal;
|
|
76
77
|
mode: ArtifactServerMode;
|
|
77
78
|
log: ReturnType<typeof createArtifactLogger>;
|
|
78
79
|
api: ReturnType<typeof createApiClient>;
|
|
@@ -84,46 +85,32 @@ export interface ArtifactServerClient {
|
|
|
84
85
|
flags: ArtifactServerFlags;
|
|
85
86
|
audit: ArtifactServerAudit;
|
|
86
87
|
orm(input: ArtifactOrmOptions): OperationExecutor;
|
|
87
|
-
asService(): ArtifactServerClient
|
|
88
|
+
asService(): ArtifactServerClient<TContext>;
|
|
88
89
|
}
|
|
89
|
-
|
|
90
|
-
export declare function createArtifactServerClient({ headers, runtime, fetch: fetcher, method, path, logSink }: {
|
|
90
|
+
export interface ArtifactServerClientOptions {
|
|
91
91
|
headers: ArtifactRequestHeaders;
|
|
92
92
|
/** Optional explicit platform config; otherwise reads SEQUENCE_ARTIFACT_RUNTIME_FILE. */
|
|
93
93
|
runtime?: ArtifactRuntimeConfig;
|
|
94
94
|
fetch?: typeof globalThis.fetch;
|
|
95
|
+
/** Required, with `path`, to accept an external caller. */
|
|
95
96
|
method?: string;
|
|
96
97
|
path?: string;
|
|
97
98
|
logSink?: (record: ArtifactLogRecord) => void;
|
|
98
|
-
}): Promise<ArtifactServerClient>;
|
|
99
|
-
type ClientArguments = {
|
|
100
|
-
context: ArtifactRequestContext;
|
|
101
|
-
token: string;
|
|
102
|
-
getRuntime: () => Promise<ArtifactRuntimeConfig>;
|
|
103
|
-
fetcher: typeof globalThis.fetch;
|
|
104
|
-
mode: ArtifactServerMode;
|
|
105
|
-
logSink?: (record: ArtifactLogRecord) => void;
|
|
106
|
-
};
|
|
107
|
-
type BrokerTransport = ReturnType<typeof createBrokerTransport>;
|
|
108
|
-
interface BrokerCallOptions {
|
|
109
|
-
signal?: AbortSignal;
|
|
110
|
-
/** Whether sending the call twice is harmless. Defaults to the operation's own answer (isReadOperation). */
|
|
111
|
-
replaySafe?: boolean;
|
|
112
99
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
100
|
+
/**
|
|
101
|
+
* One instance per incoming request. Never cache it at module scope.
|
|
102
|
+
*
|
|
103
|
+
* Accepts Atlas-user and platform-service contexts. A handler that also serves external callers
|
|
104
|
+
* names their ingresses in `accept`; its client then carries an external context, whose
|
|
105
|
+
* `principal` reaches platform services only through `asService()`.
|
|
106
|
+
*/
|
|
107
|
+
export declare function createArtifactServerClient(options: ArtifactServerClientOptions & {
|
|
108
|
+
accept?: undefined;
|
|
109
|
+
}): Promise<ArtifactServerClient>;
|
|
110
|
+
export declare function createArtifactServerClient(options: ArtifactServerClientOptions & {
|
|
111
|
+
accept: ArtifactExternalAccept;
|
|
112
|
+
}): Promise<ArtifactServerClient<ArtifactServerContext>>;
|
|
113
|
+
/** For a helper that forwards an optional `accept`: the context may be either version. */
|
|
114
|
+
export declare function createArtifactServerClient(options: ArtifactServerClientOptions & {
|
|
115
|
+
accept?: ArtifactExternalAccept;
|
|
116
|
+
}): Promise<ArtifactServerClient<ArtifactServerContext>>;
|
package/dist/server/client.js
CHANGED
|
@@ -3,36 +3,40 @@ import { StorageApiError, storageClient, storageErrorSchema } from '@sequencehol
|
|
|
3
3
|
import { OrmOperationError } from '@sequenceholdings/orm/client';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { artifactServerRequestSchema } from './broker-contract.js';
|
|
6
|
-
import { ArtifactServerError, platformRequestError } from './errors.js';
|
|
7
6
|
import { createArtifactProcessesClient } from './processes.js';
|
|
8
7
|
import { assertJsonScalarsAcknowledged, ORM_JSON_SCALARS_HEADER } from '../orm-json-scalars.js';
|
|
9
8
|
import { createArtifactLogger } from './logger.js';
|
|
10
9
|
import { ARTIFACT_REQUEST_CONTEXT_HEADER, ARTIFACT_RUNTIME_TOKEN_HEADER, ARTIFACT_SERVER_REQUEST_PATH, verifyArtifactRequestContext } from './request-context.js';
|
|
10
|
+
import { verifyArtifactExternalContext } from './external-context.js';
|
|
11
|
+
import { isExternalContextToken } from './internal/signed-token.js';
|
|
12
|
+
import { createApiClient } from './api-client.js';
|
|
13
|
+
import { pause, transientConnectionCode } from './connection-retry.js';
|
|
14
|
+
import { principalFromContext } from './principal.js';
|
|
11
15
|
import { artifactRuntimeConfigSchema, readArtifactRuntimeConfig } from './runtime-config.js';
|
|
12
|
-
|
|
16
|
+
import { ArtifactPrincipalError, ArtifactServerError, refusalFromResponse, stepUpError, stepUpErrorFromResponse } from './errors.js';
|
|
17
|
+
export { ArtifactPrincipalError, ArtifactServerError, INSUFFICIENT_USER_AUTHENTICATION, stepUpResponse } from './errors.js';
|
|
13
18
|
const graphqlResponseSchema = z.object({
|
|
14
19
|
data: z.unknown().optional(),
|
|
15
20
|
errors: z.array(z.object({ message: z.string(), extensions: z.object({ code: z.string().optional() }).passthrough().optional() })).optional(),
|
|
16
21
|
});
|
|
17
22
|
const flagsResponseSchema = z.object({ flags: z.record(z.string(), z.boolean()) });
|
|
18
|
-
const responseHeaders = ['content-type', 'content-disposition', 'content-length', 'retry-after'];
|
|
19
23
|
const maxBodyBytes = 1024 * 1024;
|
|
20
24
|
/** A read whose connection failed is sent at most three times, pausing up to 100 ms, then up to 200 ms. */
|
|
21
25
|
const reconnect = { attempts: 3, baseDelayMs: 100 };
|
|
22
|
-
|
|
23
|
-
* Socket and DNS errors after which Atlas never answered, and which clear by themselves: the
|
|
24
|
-
* route to Atlas was briefly gone (an egress NAT out of ports drops new connections, which Node
|
|
25
|
-
* reports as EHOSTUNREACH), Atlas's edge refused or reset the connection, or a lookup failed
|
|
26
|
-
* transiently. Timeouts aren't here: a call that waited out its timeout shouldn't wait it out again.
|
|
27
|
-
*/
|
|
28
|
-
const transientConnectionCodes = new Set(['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH', 'EADDRNOTAVAIL', 'EPIPE', 'EAI_AGAIN', 'UND_ERR_SOCKET']);
|
|
29
|
-
/** One instance per incoming request. Never cache it at module scope. */
|
|
30
|
-
export async function createArtifactServerClient({ headers, runtime, fetch: fetcher = globalThis.fetch, method, path, logSink }) {
|
|
26
|
+
export async function createArtifactServerClient({ headers, runtime, fetch: fetcher = globalThis.fetch, method, path, logSink, accept }) {
|
|
31
27
|
const config = runtime ? artifactRuntimeConfigSchema.parse(runtime) : await readArtifactRuntimeConfig();
|
|
32
28
|
if (!config)
|
|
33
29
|
throw new ArtifactServerError(401, 'runtime_not_connected', 'Run seq-studio artifact dev to connect platform services, or use a hosted preview.');
|
|
34
30
|
const token = headers.get(ARTIFACT_REQUEST_CONTEXT_HEADER);
|
|
35
|
-
|
|
31
|
+
let context;
|
|
32
|
+
if (accept && isExternalContextToken(token)) {
|
|
33
|
+
if (!method || !path)
|
|
34
|
+
throw new ArtifactServerError(401, 'invalid_request_context', 'An external request context is verified only against the route method and path');
|
|
35
|
+
context = verifyArtifactExternalContext({ token, publicKeys: config.publicKeys, projectId: config.projectId, environment: config.environment, method, path, accept });
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
context = verifyArtifactRequestContext({ token, publicKeys: config.publicKeys, projectId: config.projectId, environment: config.environment, method, path });
|
|
39
|
+
}
|
|
36
40
|
if (!token || (config.devSessionId && context.devSessionId !== config.devSessionId) ||
|
|
37
41
|
(config.deploymentId && context.deploymentId !== config.deploymentId))
|
|
38
42
|
throw new ArtifactServerError(401, 'invalid_runtime_session', 'Artifact request does not belong to this runtime');
|
|
@@ -42,24 +46,27 @@ export async function createArtifactServerClient({ headers, runtime, fetch: fetc
|
|
|
42
46
|
throw new ArtifactServerError(401, 'runtime_changed', 'Artifact runtime disconnected or changed; reload the preview');
|
|
43
47
|
return current;
|
|
44
48
|
};
|
|
45
|
-
return buildClient({ context, token, getRuntime, fetcher, mode: 'user', logSink });
|
|
49
|
+
return buildClient({ context, principal: principalFromContext(context), token, getRuntime, fetcher, mode: 'user', logSink });
|
|
46
50
|
}
|
|
47
51
|
function buildClient(args) {
|
|
48
|
-
const { context, mode, logSink } = args;
|
|
52
|
+
const { context, principal, mode, logSink } = args;
|
|
49
53
|
const log = createArtifactLogger({ context, sink: logSink });
|
|
50
54
|
const request = createBrokerTransport({ ...args, log });
|
|
51
55
|
const json = async (operation, signal) => {
|
|
52
56
|
const response = await request(operation, { signal });
|
|
57
|
+
// One read of the body: a step-up refusal keeps reason and maxAge, any other keeps Atlas's { detail, code }.
|
|
53
58
|
if (!response.ok)
|
|
54
|
-
throw await
|
|
59
|
+
throw await refusalFromResponse({ response, operation: operation.operation });
|
|
55
60
|
return response.status === 204 ? undefined : response.json();
|
|
56
61
|
};
|
|
57
62
|
// storageClient retries replay-safe calls on 503 permissions_pending / storage_busy, so Storage
|
|
58
63
|
// failures keep the envelope's status and code rather than collapsing into platform_request_failed.
|
|
59
64
|
const storageJson = async (operation) => {
|
|
60
65
|
const response = await request(operation);
|
|
61
|
-
if (!response.ok)
|
|
62
|
-
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
// A step-up refusal keeps its reason and maxAge; storageApiError reads the body, hence the clone.
|
|
68
|
+
throw (response.status === 401 ? await stepUpErrorFromResponse(response.clone()) : null) ?? await storageApiError(response);
|
|
69
|
+
}
|
|
63
70
|
return response.status === 204 ? undefined : response.json();
|
|
64
71
|
};
|
|
65
72
|
const evaluateFlags = async (flagNames) => {
|
|
@@ -72,6 +79,7 @@ function buildClient(args) {
|
|
|
72
79
|
};
|
|
73
80
|
return {
|
|
74
81
|
context,
|
|
82
|
+
principal,
|
|
75
83
|
mode,
|
|
76
84
|
log,
|
|
77
85
|
api: createApiClient({ mode, request, json }),
|
|
@@ -97,7 +105,11 @@ function buildClient(args) {
|
|
|
97
105
|
const response = await request(artifactServerRequestSchema.parse({ operation: 'orm', mode, namespace, op: hash, variables, ...(jsonScalars ? { jsonScalars } : {}) }),
|
|
98
106
|
// Only an operation the manifest lists as a query is known to change nothing.
|
|
99
107
|
{ replaySafe: listed?.kind === 'query' });
|
|
100
|
-
const
|
|
108
|
+
const body = await response.json().catch(() => null);
|
|
109
|
+
const stepUp = stepUpError(response.status, body);
|
|
110
|
+
if (stepUp)
|
|
111
|
+
throw stepUp;
|
|
112
|
+
const parsed = graphqlResponseSchema.safeParse(body);
|
|
101
113
|
if (!parsed.success)
|
|
102
114
|
throw new OrmOperationError('Malformed Data API response', response.status, 'invalid_response');
|
|
103
115
|
const first = parsed.data.errors?.[0];
|
|
@@ -139,7 +151,12 @@ async function recordAuditEvent({ request, mode, event }) {
|
|
|
139
151
|
failure = new ArtifactServerError(503, 'audit_unavailable', 'Atlas did not answer the audit request');
|
|
140
152
|
continue;
|
|
141
153
|
}
|
|
142
|
-
|
|
154
|
+
// A step-up refusal is the broker's answer, not a lost delivery: never resent.
|
|
155
|
+
const stepUp = await stepUpErrorFromResponse(response);
|
|
156
|
+
if (stepUp)
|
|
157
|
+
throw stepUp;
|
|
158
|
+
if (!response.bodyUsed)
|
|
159
|
+
await response.body?.cancel();
|
|
143
160
|
if (response.ok)
|
|
144
161
|
return;
|
|
145
162
|
failure = new ArtifactServerError(response.status, response.status === 503 ? 'audit_unavailable' : 'platform_request_failed', `Atlas rejected the audit request (${response.status})`);
|
|
@@ -153,6 +170,8 @@ async function storageApiError(response) {
|
|
|
153
170
|
const envelope = storageErrorSchema.safeParse(await response.json().catch(() => undefined));
|
|
154
171
|
return new StorageApiError(response.status, envelope.success ? envelope.data.error.code : 'request_failed', envelope.success ? envelope.data.error.message : `Storage request failed (${response.status})`);
|
|
155
172
|
}
|
|
173
|
+
/** How long Atlas has to answer a broker call; for a stream, to send its first byte of headers. */
|
|
174
|
+
const BROKER_RESPONSE_TIMEOUT_MS = 60_000;
|
|
156
175
|
/**
|
|
157
176
|
* Broker operations that change nothing in Atlas. An ORM call's caller decides from its manifest
|
|
158
177
|
* kind; function invokes and process starts can act, and audit delivery resends on its own terms.
|
|
@@ -168,40 +187,11 @@ function isReadOperation(operation) {
|
|
|
168
187
|
return false;
|
|
169
188
|
}
|
|
170
189
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
function transientConnectionCode(error) {
|
|
177
|
-
let current = error;
|
|
178
|
-
for (let depth = 0; depth < 8 && typeof current === 'object' && current !== null; depth++) {
|
|
179
|
-
if ('code' in current && typeof current.code === 'string' && transientConnectionCodes.has(current.code))
|
|
180
|
-
return current.code;
|
|
181
|
-
current = 'cause' in current ? current.cause : undefined;
|
|
182
|
-
}
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
/** Resolves after `ms`, or rejects with the caller's abort reason as soon as it aborts. */
|
|
186
|
-
function pause({ ms, signal }) {
|
|
187
|
-
return new Promise((resolve, reject) => {
|
|
188
|
-
if (signal?.aborted) {
|
|
189
|
-
reject(signal.reason);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const aborted = () => {
|
|
193
|
-
clearTimeout(timer);
|
|
194
|
-
reject(signal?.reason);
|
|
195
|
-
};
|
|
196
|
-
const timer = setTimeout(() => {
|
|
197
|
-
signal?.removeEventListener('abort', aborted);
|
|
198
|
-
resolve();
|
|
199
|
-
}, ms);
|
|
200
|
-
signal?.addEventListener('abort', aborted, { once: true });
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
function createBrokerTransport({ context, token, getRuntime, fetcher, log }) {
|
|
204
|
-
const send = async (input, signal) => {
|
|
190
|
+
function createBrokerTransport({ context, principal, token, getRuntime, fetcher, log }) {
|
|
191
|
+
const send = async (input, signal, stream = false) => {
|
|
192
|
+
// The one choke point for every broker operation: an external caller never acts as a user.
|
|
193
|
+
if (input.mode === 'user' && principal.kind === 'external')
|
|
194
|
+
throw new ArtifactPrincipalError('external_requires_service');
|
|
205
195
|
if (context.exp <= Math.floor(Date.now() / 1000))
|
|
206
196
|
throw new ArtifactServerError(401, 'request_context_expired', 'Artifact request context expired; begin a new request');
|
|
207
197
|
const operation = artifactServerRequestSchema.parse(input);
|
|
@@ -211,19 +201,32 @@ function createBrokerTransport({ context, token, getRuntime, fetcher, log }) {
|
|
|
211
201
|
if (Buffer.byteLength(body, 'utf8') > maxBodyBytes)
|
|
212
202
|
throw new ArtifactServerError(413, 'request_too_large', 'Server SDK requests are limited to 1 MiB; use Storage uploads for files');
|
|
213
203
|
const runtime = await getRuntime();
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
204
|
+
// The timeout covers the whole call, body included, except for a stream: its timer is cleared
|
|
205
|
+
// once Atlas answers, so a long body isn't cut off mid-read.
|
|
206
|
+
const expiry = new AbortController();
|
|
207
|
+
const timer = setTimeout(() => expiry.abort(new DOMException('Atlas did not answer in time', 'TimeoutError')), BROKER_RESPONSE_TIMEOUT_MS);
|
|
208
|
+
if (!stream)
|
|
209
|
+
timer.unref?.();
|
|
210
|
+
const timeout = expiry.signal;
|
|
211
|
+
try {
|
|
212
|
+
return await fetcher(`${runtime.atlasUrl}${ARTIFACT_SERVER_REQUEST_PATH}`, {
|
|
213
|
+
method: 'POST',
|
|
214
|
+
headers: {
|
|
215
|
+
'Content-Type': 'application/json',
|
|
216
|
+
[ARTIFACT_RUNTIME_TOKEN_HEADER]: runtime.sessionToken,
|
|
217
|
+
[ARTIFACT_REQUEST_CONTEXT_HEADER]: token,
|
|
218
|
+
},
|
|
219
|
+
body,
|
|
220
|
+
cache: 'no-store',
|
|
221
|
+
redirect: 'error',
|
|
222
|
+
credentials: 'omit',
|
|
223
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
if (stream)
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
}
|
|
227
230
|
};
|
|
228
231
|
/**
|
|
229
232
|
* Sends a read again when its connection to Atlas failed before Atlas answered: such failures
|
|
@@ -231,10 +234,10 @@ function createBrokerTransport({ context, token, getRuntime, fetcher, log }) {
|
|
|
231
234
|
* again, because a reset can come after Atlas acted on it. A response, whatever its status, is
|
|
232
235
|
* Atlas's answer and is returned as is.
|
|
233
236
|
*/
|
|
234
|
-
return async (input, { signal, replaySafe = isReadOperation(input) } = {}) => {
|
|
237
|
+
return async (input, { signal, replaySafe = isReadOperation(input), stream = false } = {}) => {
|
|
235
238
|
for (let attempt = 1;; attempt++) {
|
|
236
239
|
try {
|
|
237
|
-
return await send(input, signal);
|
|
240
|
+
return await send(input, signal, stream);
|
|
238
241
|
}
|
|
239
242
|
catch (error) {
|
|
240
243
|
const code = replaySafe && attempt < reconnect.attempts ? transientConnectionCode(error) : null;
|
|
@@ -248,35 +251,3 @@ function createBrokerTransport({ context, token, getRuntime, fetcher, log }) {
|
|
|
248
251
|
}
|
|
249
252
|
};
|
|
250
253
|
}
|
|
251
|
-
function createApiClient({ mode, request, json }) {
|
|
252
|
-
const operation = (path, options = {}) => artifactServerRequestSchema.parse({ operation: 'api', mode, method: options.method ?? 'GET', path, body: options.body });
|
|
253
|
-
const fetchResponse = (path, options = {}) => request(operation(path, options), { signal: options.signal });
|
|
254
|
-
const typedJson = async (path, options = {}) => {
|
|
255
|
-
// Explicit caller result types match the existing browser SDK; the server validates endpoint schemas.
|
|
256
|
-
return await json(operation(path, options), options.signal);
|
|
257
|
-
};
|
|
258
|
-
return {
|
|
259
|
-
request: fetchResponse,
|
|
260
|
-
async fetch(path, options = {}) {
|
|
261
|
-
const response = await fetchResponse(path, options);
|
|
262
|
-
const contentType = response.headers.get('content-type') ?? '';
|
|
263
|
-
const body = response.status === 204 ? undefined : contentType.includes('json') ? await response.json() : await response.arrayBuffer();
|
|
264
|
-
// The caller declares the endpoint's response type, consistent with SequenceApiFetchResponse.
|
|
265
|
-
return { ok: response.ok, status: response.status, body: body, headers: Object.fromEntries(responseHeaders.flatMap(key => {
|
|
266
|
-
const value = response.headers.get(key);
|
|
267
|
-
return value === null ? [] : [[key, value]];
|
|
268
|
-
})) };
|
|
269
|
-
},
|
|
270
|
-
async stream(path, options = {}) {
|
|
271
|
-
const response = await fetchResponse(path, options);
|
|
272
|
-
if (!response.ok || !response.body)
|
|
273
|
-
throw new ArtifactServerError(response.status, 'stream_unavailable', 'Atlas did not return a readable response');
|
|
274
|
-
return response.body;
|
|
275
|
-
},
|
|
276
|
-
get: (path) => typedJson(path),
|
|
277
|
-
post: (path, body) => typedJson(path, { method: 'POST', body }),
|
|
278
|
-
put: (path, body) => typedJson(path, { method: 'PUT', body }),
|
|
279
|
-
patch: (path, body) => typedJson(path, { method: 'PATCH', body }),
|
|
280
|
-
delete: (path, body) => typedJson(path, { method: 'DELETE', body }),
|
|
281
|
-
};
|
|
282
|
-
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Recognising a lost connection to Atlas, and waiting before a broker read is resent. */
|
|
2
|
+
/**
|
|
3
|
+
* The transient connection code behind a failed fetch, or null. Node's fetch throws
|
|
4
|
+
* `TypeError: fetch failed` with the socket error as its cause. When every address of the host
|
|
5
|
+
* failed to connect, that cause is an AggregateError carrying the first address's code.
|
|
6
|
+
*/
|
|
7
|
+
export declare function transientConnectionCode(error: unknown): string | null;
|
|
8
|
+
/** Resolves after `ms`, or rejects with the caller's abort reason as soon as it aborts. */
|
|
9
|
+
export declare function pause({ ms, signal }: {
|
|
10
|
+
ms: number;
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Recognising a lost connection to Atlas, and waiting before a broker read is resent. */
|
|
2
|
+
/**
|
|
3
|
+
* Socket and DNS errors after which Atlas never answered, and which clear by themselves: the
|
|
4
|
+
* route to Atlas was briefly gone (an egress NAT out of ports drops new connections, which Node
|
|
5
|
+
* reports as EHOSTUNREACH), Atlas's edge refused or reset the connection, or a lookup failed
|
|
6
|
+
* transiently. Timeouts aren't here: a call that waited out its timeout shouldn't wait it out again.
|
|
7
|
+
*/
|
|
8
|
+
const transientConnectionCodes = new Set(['ECONNREFUSED', 'ECONNRESET', 'EHOSTUNREACH', 'ENETUNREACH', 'EADDRNOTAVAIL', 'EPIPE', 'EAI_AGAIN', 'UND_ERR_SOCKET']);
|
|
9
|
+
/**
|
|
10
|
+
* The transient connection code behind a failed fetch, or null. Node's fetch throws
|
|
11
|
+
* `TypeError: fetch failed` with the socket error as its cause. When every address of the host
|
|
12
|
+
* failed to connect, that cause is an AggregateError carrying the first address's code.
|
|
13
|
+
*/
|
|
14
|
+
export function transientConnectionCode(error) {
|
|
15
|
+
let current = error;
|
|
16
|
+
for (let depth = 0; depth < 8 && typeof current === 'object' && current !== null; depth++) {
|
|
17
|
+
if ('code' in current && typeof current.code === 'string' && transientConnectionCodes.has(current.code))
|
|
18
|
+
return current.code;
|
|
19
|
+
current = 'cause' in current ? current.cause : undefined;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/** Resolves after `ms`, or rejects with the caller's abort reason as soon as it aborts. */
|
|
24
|
+
export function pause({ ms, signal }) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
if (signal?.aborted) {
|
|
27
|
+
reject(signal.reason);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const aborted = () => {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
reject(signal?.reason);
|
|
33
|
+
};
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
signal?.removeEventListener('abort', aborted);
|
|
36
|
+
resolve();
|
|
37
|
+
}, ms);
|
|
38
|
+
signal?.addEventListener('abort', aborted, { once: true });
|
|
39
|
+
});
|
|
40
|
+
}
|
package/dist/server/errors.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export declare const INSUFFICIENT_USER_AUTHENTICATION = "insufficient_user_authentication";
|
|
1
2
|
/**
|
|
2
3
|
* What Atlas's error envelope said about a refusal, when the response carried one. Atlas routes
|
|
3
4
|
* answer `{ detail, code? }`: `detail` is the human-readable reason, `code` the route's machine
|
|
@@ -8,19 +9,53 @@ export interface ArtifactAtlasRefusal {
|
|
|
8
9
|
code: string | null;
|
|
9
10
|
detail: string | null;
|
|
10
11
|
}
|
|
12
|
+
export interface ArtifactServerErrorDetails {
|
|
13
|
+
reason?: string;
|
|
14
|
+
maxAge?: number;
|
|
15
|
+
atlas?: ArtifactAtlasRefusal;
|
|
16
|
+
}
|
|
11
17
|
export declare class ArtifactServerError extends Error {
|
|
12
18
|
readonly status: number;
|
|
13
19
|
readonly code: string;
|
|
20
|
+
/** A named refusal a handler can act on, such as `insufficient_user_authentication`. */
|
|
21
|
+
readonly reason: string | undefined;
|
|
22
|
+
/** With `insufficient_user_authentication`: the sign-in age, in seconds, the operation requires. */
|
|
23
|
+
readonly maxAge: number | undefined;
|
|
14
24
|
/** Atlas's own account of the refusal; both null when the response had no readable envelope. */
|
|
15
25
|
readonly atlas: ArtifactAtlasRefusal;
|
|
16
|
-
constructor(status: number, code: string, message: string,
|
|
26
|
+
constructor(status: number, code: string, message: string, details?: ArtifactServerErrorDetails);
|
|
27
|
+
}
|
|
28
|
+
/** A user-mode call made for a caller that may act only through `asService()`. */
|
|
29
|
+
export declare class ArtifactPrincipalError extends ArtifactServerError {
|
|
30
|
+
readonly reason: 'external_requires_service';
|
|
31
|
+
constructor(reason: 'external_requires_service');
|
|
17
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The step-up refusal in a broker 401 body (`{reason|error: 'insufficient_user_authentication',
|
|
35
|
+
* maxAge|max_age}`), or null. A missing or malformed age asks for a fresh sign-in (0).
|
|
36
|
+
*/
|
|
37
|
+
export declare function stepUpError(status: number, body: unknown): ArtifactServerError | null;
|
|
38
|
+
export declare function stepUpErrorFromResponse(response: Response): Promise<ArtifactServerError | null>;
|
|
18
39
|
/**
|
|
19
40
|
* A refused broker call, keeping what Atlas said: the message carries the route's `detail`
|
|
20
41
|
* and `error.atlas` its machine `code`, so a consumer can branch on a Lattice refusal
|
|
21
42
|
* (`EDGE_ID_MISMATCH`, `WORKFLOW_NOT_RUNNING`, …) the way a caller of the route itself would.
|
|
22
43
|
*/
|
|
23
|
-
export declare function platformRequestError({
|
|
44
|
+
export declare function platformRequestError({ status, body, operation }: {
|
|
45
|
+
status: number;
|
|
46
|
+
body: unknown;
|
|
47
|
+
operation: string;
|
|
48
|
+
}): ArtifactServerError;
|
|
49
|
+
/**
|
|
50
|
+
* The error for a refused broker response: a step-up refusal keeps its reason and maxAge, any other
|
|
51
|
+
* refusal keeps Atlas's `{ detail, code }`. The body is read once, so neither reading starves the other.
|
|
52
|
+
*/
|
|
53
|
+
export declare function refusalFromResponse({ response, operation }: {
|
|
24
54
|
response: Response;
|
|
25
55
|
operation: string;
|
|
26
56
|
}): Promise<ArtifactServerError>;
|
|
57
|
+
/**
|
|
58
|
+
* The RFC 9470 response for a step-up refusal, for the app to return as is: the caller signs in
|
|
59
|
+
* again with `max_age` and retries.
|
|
60
|
+
*/
|
|
61
|
+
export declare function stepUpResponse(error: ArtifactServerError): Response;
|
package/dist/server/errors.js
CHANGED
|
@@ -1,18 +1,53 @@
|
|
|
1
|
+
/** Errors the server SDK raises, and the step-up (RFC 9470) mapping a handler returns. */
|
|
1
2
|
import { z } from 'zod';
|
|
3
|
+
export const INSUFFICIENT_USER_AUTHENTICATION = 'insufficient_user_authentication';
|
|
2
4
|
const NO_REFUSAL = { code: null, detail: null };
|
|
3
5
|
export class ArtifactServerError extends Error {
|
|
4
6
|
status;
|
|
5
7
|
code;
|
|
8
|
+
/** A named refusal a handler can act on, such as `insufficient_user_authentication`. */
|
|
9
|
+
reason;
|
|
10
|
+
/** With `insufficient_user_authentication`: the sign-in age, in seconds, the operation requires. */
|
|
11
|
+
maxAge;
|
|
6
12
|
/** Atlas's own account of the refusal; both null when the response had no readable envelope. */
|
|
7
13
|
atlas;
|
|
8
|
-
constructor(status, code, message,
|
|
14
|
+
constructor(status, code, message, details = {}) {
|
|
9
15
|
super(message);
|
|
10
16
|
this.status = status;
|
|
11
17
|
this.code = code;
|
|
12
18
|
this.name = 'ArtifactServerError';
|
|
13
|
-
this.
|
|
19
|
+
this.reason = details.reason;
|
|
20
|
+
this.maxAge = details.maxAge;
|
|
21
|
+
this.atlas = details.atlas ?? NO_REFUSAL;
|
|
14
22
|
}
|
|
15
23
|
}
|
|
24
|
+
/** A user-mode call made for a caller that may act only through `asService()`. */
|
|
25
|
+
export class ArtifactPrincipalError extends ArtifactServerError {
|
|
26
|
+
constructor(reason) {
|
|
27
|
+
super(403, reason, 'External callers reach platform services only through asService()', { reason });
|
|
28
|
+
this.name = 'ArtifactPrincipalError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The step-up refusal in a broker 401 body (`{reason|error: 'insufficient_user_authentication',
|
|
33
|
+
* maxAge|max_age}`), or null. A missing or malformed age asks for a fresh sign-in (0).
|
|
34
|
+
*/
|
|
35
|
+
export function stepUpError(status, body) {
|
|
36
|
+
if (status !== 401 || body === null || typeof body !== 'object')
|
|
37
|
+
return null;
|
|
38
|
+
const named = ('reason' in body && body.reason === INSUFFICIENT_USER_AUTHENTICATION) ||
|
|
39
|
+
('error' in body && body.error === INSUFFICIENT_USER_AUTHENTICATION);
|
|
40
|
+
if (!named)
|
|
41
|
+
return null;
|
|
42
|
+
const age = 'maxAge' in body ? body.maxAge : 'max_age' in body ? body.max_age : undefined;
|
|
43
|
+
const maxAge = typeof age === 'number' && Number.isSafeInteger(age) && age >= 0 ? age : 0;
|
|
44
|
+
return new ArtifactServerError(401, INSUFFICIENT_USER_AUTHENTICATION, 'A more recent sign-in is required', { reason: INSUFFICIENT_USER_AUTHENTICATION, maxAge });
|
|
45
|
+
}
|
|
46
|
+
export async function stepUpErrorFromResponse(response) {
|
|
47
|
+
if (response.status !== 401)
|
|
48
|
+
return null;
|
|
49
|
+
return stepUpError(response.status, await response.json().catch(() => null));
|
|
50
|
+
}
|
|
16
51
|
/** Atlas answers a refusal as `{ detail, code? }`; anything else (a proxy page, an empty body) reads as no envelope. */
|
|
17
52
|
const atlasRefusalSchema = z.object({ detail: z.string().optional(), code: z.string().optional() });
|
|
18
53
|
/**
|
|
@@ -20,10 +55,35 @@ const atlasRefusalSchema = z.object({ detail: z.string().optional(), code: z.str
|
|
|
20
55
|
* and `error.atlas` its machine `code`, so a consumer can branch on a Lattice refusal
|
|
21
56
|
* (`EDGE_ID_MISMATCH`, `WORKFLOW_NOT_RUNNING`, …) the way a caller of the route itself would.
|
|
22
57
|
*/
|
|
23
|
-
export
|
|
24
|
-
const envelope = atlasRefusalSchema.safeParse(
|
|
58
|
+
export function platformRequestError({ status, body, operation }) {
|
|
59
|
+
const envelope = atlasRefusalSchema.safeParse(body);
|
|
25
60
|
const detail = envelope.success ? envelope.data.detail ?? null : null;
|
|
26
61
|
const code = envelope.success ? envelope.data.code ?? null : null;
|
|
27
|
-
const message = `Atlas rejected the ${operation} request (${
|
|
28
|
-
return new ArtifactServerError(
|
|
62
|
+
const message = `Atlas rejected the ${operation} request (${status})${detail ? `: ${detail}` : ''}`;
|
|
63
|
+
return new ArtifactServerError(status, 'platform_request_failed', message, { atlas: { code, detail } });
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The error for a refused broker response: a step-up refusal keeps its reason and maxAge, any other
|
|
67
|
+
* refusal keeps Atlas's `{ detail, code }`. The body is read once, so neither reading starves the other.
|
|
68
|
+
*/
|
|
69
|
+
export async function refusalFromResponse({ response, operation }) {
|
|
70
|
+
const body = await response.json().catch(() => undefined);
|
|
71
|
+
return stepUpError(response.status, body) ?? platformRequestError({ status: response.status, body, operation });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The RFC 9470 response for a step-up refusal, for the app to return as is: the caller signs in
|
|
75
|
+
* again with `max_age` and retries.
|
|
76
|
+
*/
|
|
77
|
+
export function stepUpResponse(error) {
|
|
78
|
+
if (error.reason !== INSUFFICIENT_USER_AUTHENTICATION)
|
|
79
|
+
throw new TypeError('stepUpResponse requires an insufficient_user_authentication error');
|
|
80
|
+
const maxAge = error.maxAge !== undefined && Number.isSafeInteger(error.maxAge) && error.maxAge >= 0 ? error.maxAge : 0;
|
|
81
|
+
return new Response(JSON.stringify({ error: INSUFFICIENT_USER_AUTHENTICATION, max_age: maxAge }), {
|
|
82
|
+
status: 401,
|
|
83
|
+
headers: {
|
|
84
|
+
'content-type': 'application/json',
|
|
85
|
+
'cache-control': 'no-store',
|
|
86
|
+
'www-authenticate': `Bearer error="${INSUFFICIENT_USER_AUTHENTICATION}", max_age=${maxAge}`,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
29
89
|
}
|