@onesi/genesis 0.0.1-stage → 0.1.13
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 +29 -0
- package/dist/beacons.d.ts +103 -0
- package/dist/beacons.js +77 -0
- package/dist/blinds-owner-enrollment.d.ts +171 -0
- package/dist/blinds-owner-enrollment.js +150 -0
- package/dist/client.d.ts +19996 -0
- package/dist/client.js +277 -0
- package/dist/context.d.ts +50 -0
- package/dist/context.js +153 -0
- package/dist/dials.d.ts +299 -0
- package/dist/dials.js +100 -0
- package/dist/errors.d.ts +35 -0
- package/dist/errors.js +66 -0
- package/dist/flares.d.ts +170 -0
- package/dist/flares.js +56 -0
- package/dist/fleets.d.ts +186 -0
- package/dist/fleets.js +136 -0
- package/dist/flocks.d.ts +155 -0
- package/dist/flocks.js +39 -0
- package/dist/foundry-billing.d.ts +97 -0
- package/dist/foundry-billing.js +107 -0
- package/dist/gears.d.ts +107 -0
- package/dist/gears.js +20 -0
- package/dist/generated/operations.d.ts +19949 -0
- package/dist/generated/operations.js +23846 -0
- package/dist/generated/schema.d.ts +35413 -0
- package/dist/generated/schema.js +1 -0
- package/dist/genesis.d.ts +2926 -0
- package/dist/genesis.js +206 -0
- package/dist/genesis.openapi.json +49321 -0
- package/dist/henry.d.ts +168 -0
- package/dist/henry.js +45 -0
- package/dist/hives-cells.d.ts +127 -0
- package/dist/hives-cells.js +151 -0
- package/dist/hives-environment.d.ts +28 -0
- package/dist/hives-environment.js +58 -0
- package/dist/hives-functions.d.ts +36 -0
- package/dist/hives-functions.js +185 -0
- package/dist/hives-marketplace.d.ts +128 -0
- package/dist/hives-marketplace.js +187 -0
- package/dist/hives-node.d.ts +9 -0
- package/dist/hives-node.js +152 -0
- package/dist/hives-operations.d.ts +337 -0
- package/dist/hives-operations.js +41 -0
- package/dist/hives-queue-consumer.d.ts +56 -0
- package/dist/hives-queue-consumer.js +95 -0
- package/dist/hives-queue-funding.d.ts +28 -0
- package/dist/hives-queue-funding.js +81 -0
- package/dist/hives-queues.d.ts +137 -0
- package/dist/hives-queues.js +143 -0
- package/dist/hives-resources-transport.d.ts +23 -0
- package/dist/hives-resources-transport.js +204 -0
- package/dist/hives-response.d.ts +2 -0
- package/dist/hives-response.js +32 -0
- package/dist/hives-runtimes.d.ts +34 -0
- package/dist/hives-runtimes.js +32 -0
- package/dist/hives-topics.d.ts +51 -0
- package/dist/hives-topics.js +79 -0
- package/dist/hives-upload.d.ts +9 -0
- package/dist/hives-upload.js +23 -0
- package/dist/hives-volumes.d.ts +64 -0
- package/dist/hives-volumes.js +117 -0
- package/dist/idempotency.d.ts +4 -0
- package/dist/idempotency.js +17 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +25 -0
- package/dist/instances.d.ts +25 -0
- package/dist/instances.js +11 -0
- package/dist/lanes.d.ts +267 -0
- package/dist/lanes.js +41 -0
- package/dist/minds-agent.d.ts +60 -0
- package/dist/minds-agent.js +190 -0
- package/dist/minds-owner.d.ts +110 -0
- package/dist/minds-owner.js +226 -0
- package/dist/minds.d.ts +269 -0
- package/dist/minds.js +193 -0
- package/dist/relays-enrollment.d.ts +33 -0
- package/dist/relays-enrollment.js +115 -0
- package/dist/runner-grant.d.ts +10 -0
- package/dist/runner-grant.js +133 -0
- package/dist/runner.d.ts +68 -0
- package/dist/runner.js +116 -0
- package/dist/types.d.ts +87 -0
- package/dist/types.js +1 -0
- package/dist/urls.d.ts +6 -0
- package/dist/urls.js +31 -0
- package/package.json +49 -1
package/dist/client.js
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { isHivesQueueFundingOperation, hivesQueueOperationError } from './hives-queue-funding.js';
|
|
2
|
+
import { isMindsOperation, mindsTransport } from './minds.js';
|
|
3
|
+
import { isMindsAgentOperation, mindsAgentTransport } from './minds-agent.js';
|
|
4
|
+
import { isMindsOwnerOperation, mindsOwnerTransport } from './minds-owner.js';
|
|
5
|
+
import { isFoundryBillingOperation, foundryBillingTransport } from './foundry-billing.js';
|
|
6
|
+
import { isRelaysEnrollmentOperation, relaysEnrollmentTransport } from './relays-enrollment.js';
|
|
7
|
+
import { blindsOperationKey, blindsOwnerTransport, isBlindsOwnerOperation } from './blinds-owner-enrollment.js';
|
|
8
|
+
import { isHivesObjectUpload, hivesObjectBody, hivesContentType, isHivesResourceOperation, resourceError, resourceErrorFromResponse } from './hives-resources-transport.js';
|
|
9
|
+
import { operationTable, services } from './generated/operations.js';
|
|
10
|
+
import { GenesisError } from './errors.js';
|
|
11
|
+
import { requireSecureUrl } from './urls.js';
|
|
12
|
+
import { validateHivesInvocation, hivesFunctionResponseHeaders, hivesFunctionMetadata, hivesOperationError } from './hives-functions.js';
|
|
13
|
+
import { validateHivesEnvironment, hivesEnvironmentMetadata, hivesEnvironmentReveal } from './hives-environment.js';
|
|
14
|
+
import { RUNNER_SELF_GRANT_OPERATION, snapshotRunnerSelfGrant, runnerGrantResponse, runnerGrantTransportError } from './runner-grant.js';
|
|
15
|
+
/** The Genesis API gateway. `https://console.life.one/api` remains an alias for the same gateway. */
|
|
16
|
+
export const DEFAULT_BASE_URL = 'https://os.one.ml/api';
|
|
17
|
+
const env = globalThis.process?.env ?? {};
|
|
18
|
+
export class GenesisClient {
|
|
19
|
+
baseUrl;
|
|
20
|
+
routing;
|
|
21
|
+
opts;
|
|
22
|
+
fetchImpl;
|
|
23
|
+
get contextFetch() { return this.fetchImpl; }
|
|
24
|
+
constructor(opts = {}) {
|
|
25
|
+
this.opts = opts;
|
|
26
|
+
this.baseUrl = requireSecureUrl(opts.baseUrl ?? env.GENESIS_BASE_URL ?? DEFAULT_BASE_URL, 'baseUrl').replace(/\/+$/, '');
|
|
27
|
+
this.routing = opts.routing ?? (env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway');
|
|
28
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
29
|
+
if (!f)
|
|
30
|
+
throw new Error('No fetch available; pass options.fetch');
|
|
31
|
+
this.fetchImpl = f.bind(globalThis);
|
|
32
|
+
}
|
|
33
|
+
static operations = operationTable;
|
|
34
|
+
static services = services;
|
|
35
|
+
/** Resolve the credential: explicit apiKey > token > env GENESIS_API_KEY > GENESIS_TOKEN > provider. */
|
|
36
|
+
async credential() {
|
|
37
|
+
return this.opts.apiKey ?? this.opts.token ?? env.GENESIS_API_KEY ?? env.GENESIS_TOKEN ?? (await this.opts.credentials?.());
|
|
38
|
+
}
|
|
39
|
+
/** Build the URL for an operation (exposed for tests and `--dry-run`). */
|
|
40
|
+
url(operationId, input = {}) {
|
|
41
|
+
const op = operationTable[operationId];
|
|
42
|
+
if (!op)
|
|
43
|
+
throw new Error(`Unknown Genesis operation: ${operationId}`);
|
|
44
|
+
const fill = (tpl) => tpl.replace(/\{([^}]+)\}/g, (_, k) => {
|
|
45
|
+
const v = input[k];
|
|
46
|
+
if (v === undefined || v === null || v === '')
|
|
47
|
+
throw new GenesisError({ status: 0, type: 'invalid_request', message: `Missing required path parameter "${k}" for ${operationId}`, operationId });
|
|
48
|
+
return encodeURIComponent(String(v));
|
|
49
|
+
});
|
|
50
|
+
let base;
|
|
51
|
+
let p;
|
|
52
|
+
if (this.routing === 'direct') {
|
|
53
|
+
base = (this.opts.upstreams?.[op.service] ?? op.upstream.base).replace(/\/+$/, '');
|
|
54
|
+
p = fill(op.upstream.path);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
base = this.baseUrl;
|
|
58
|
+
p = fill(op.path);
|
|
59
|
+
}
|
|
60
|
+
const qs = new URLSearchParams();
|
|
61
|
+
for (const q of op.queryParams) {
|
|
62
|
+
const v = input[q];
|
|
63
|
+
if (v === undefined || v === null)
|
|
64
|
+
continue;
|
|
65
|
+
if (Array.isArray(v))
|
|
66
|
+
v.forEach((x) => qs.append(q, String(x)));
|
|
67
|
+
else
|
|
68
|
+
qs.set(q, String(v));
|
|
69
|
+
}
|
|
70
|
+
const s = qs.toString();
|
|
71
|
+
return `${base}${p}${s ? `?${s}` : ''}`;
|
|
72
|
+
}
|
|
73
|
+
/** Call any Genesis operation by id, fully typed from the merged spec. */
|
|
74
|
+
async request(operationId, input, init = {}) {
|
|
75
|
+
const res = await this.raw(operationId, (input ?? {}), init);
|
|
76
|
+
if (operationTable[operationId].rawResponse)
|
|
77
|
+
return res;
|
|
78
|
+
if (operationId === 'hives_stop_deployment' && res.status !== 200) {
|
|
79
|
+
void res.body?.cancel().catch(() => { });
|
|
80
|
+
throw new GenesisError({ status: 0, type: 'invalid_response', operationId, message: 'Hives did not confirm this deployment stopped. Read its current state before explicitly reconciling stop.' });
|
|
81
|
+
}
|
|
82
|
+
if (res.status === 204)
|
|
83
|
+
return undefined;
|
|
84
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
85
|
+
let value;
|
|
86
|
+
try {
|
|
87
|
+
value = ct.includes('json') ? await res.json() : await res.text();
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (String(operationId).startsWith('hives_') && String(operationId).endsWith('_project_environment'))
|
|
91
|
+
throw new Error('Hives returned invalid environment data.');
|
|
92
|
+
if (String(operationId).startsWith('hives_') && /_functions?$/.test(operationId))
|
|
93
|
+
throw new Error('Hives returned invalid function metadata.');
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
if (operationId === 'hives_stop_deployment') {
|
|
97
|
+
const requested = input;
|
|
98
|
+
if (!value || value.id !== requested.deployment_id || value.project_id !== requested.project_id || value.status !== 'cancelled') {
|
|
99
|
+
throw new GenesisError({ status: 0, type: 'invalid_response', operationId, message: 'Hives did not confirm this deployment stopped. Read its current state before explicitly reconciling stop.' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (operationId === 'hives_list_functions') {
|
|
103
|
+
if (!value || !Array.isArray(value.data) || !value.pagination)
|
|
104
|
+
throw new Error('Hives returned invalid function list metadata.');
|
|
105
|
+
const pagination = Object.fromEntries(['page', 'per_page', 'total', 'total_pages', 'has_next', 'has_prev'].map((key) => [key, value.pagination[key]]));
|
|
106
|
+
return { data: value.data.map((row) => hivesFunctionMetadata(row, input?.project_id)), pagination };
|
|
107
|
+
}
|
|
108
|
+
if (['hives_get_function', 'hives_create_function', 'hives_update_function'].includes(operationId)) {
|
|
109
|
+
return hivesFunctionMetadata(value, input?.project_id, operationId === 'hives_create_function' ? undefined : input?.function_id);
|
|
110
|
+
}
|
|
111
|
+
if (operationId === 'hives_list_project_environment' || operationId === 'hives_update_project_environment') {
|
|
112
|
+
return hivesEnvironmentMetadata(value, input?.project_id, operationId === 'hives_update_project_environment');
|
|
113
|
+
}
|
|
114
|
+
if (operationId === 'hives_reveal_project_environment') {
|
|
115
|
+
return hivesEnvironmentReveal(value, input?.key);
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
/** Raw Response. Completed Hives guest responses retain their actual status, including non-2xx. */
|
|
120
|
+
async raw(operationId, input = {}, init = {}) {
|
|
121
|
+
if (isMindsOwnerOperation(operationId))
|
|
122
|
+
return mindsOwnerTransport({ operationId, input, headers: init.headers, routing: this.routing, url: this.url(operationId, input), credential: () => this.credential(), fetch: this.fetchImpl, signal: init.signal });
|
|
123
|
+
if (isMindsAgentOperation(operationId))
|
|
124
|
+
return mindsAgentTransport({ operationId, input, headers: init.headers, routing: this.routing, url: this.url(operationId, input), credential: () => this.credential(), fetch: this.fetchImpl, signal: init.signal });
|
|
125
|
+
if (isMindsOperation(operationId))
|
|
126
|
+
return mindsTransport({ operationId, input, headers: init.headers, routing: this.routing, url: this.url(operationId, input), credential: () => this.credential(), fetch: this.fetchImpl, signal: init.signal });
|
|
127
|
+
if (isFoundryBillingOperation(operationId))
|
|
128
|
+
return foundryBillingTransport({ operationId, input, headers: init.headers, routing: this.routing, url: this.url(operationId, input), credential: () => this.credential(), fetch: this.fetchImpl, signal: init.signal });
|
|
129
|
+
if (isRelaysEnrollmentOperation(operationId))
|
|
130
|
+
return relaysEnrollmentTransport({ operationId, input, routing: this.routing, url: this.url(operationId, input), credential: () => this.credential(), fetch: this.fetchImpl, signal: init.signal });
|
|
131
|
+
if (isBlindsOwnerOperation(operationId))
|
|
132
|
+
return blindsOwnerTransport({ operationId, input, routing: this.routing, url: this.url(operationId, input), credential: () => this.credential(), fetch: this.fetchImpl, signal: init.signal });
|
|
133
|
+
const runnerGrant = operationId === RUNNER_SELF_GRANT_OPERATION ? snapshotRunnerSelfGrant(input, this.routing) : undefined;
|
|
134
|
+
if (runnerGrant)
|
|
135
|
+
input = { body: runnerGrant };
|
|
136
|
+
const op = operationTable[operationId];
|
|
137
|
+
if (op.rawResponse && op.service === 'hives')
|
|
138
|
+
validateHivesInvocation(input.body);
|
|
139
|
+
const flocksNative = ['flocks_exchangeJoinToken', 'flocks_activateNode', 'flocks_rotateNodeCredential'].includes(operationId);
|
|
140
|
+
if (flocksNative && this.routing === 'direct')
|
|
141
|
+
throw new GenesisError({ status: 0, type: 'invalid_request', message: 'Native Flocks credentials are brokered only through the Genesis gateway.', operationId });
|
|
142
|
+
if (operationId === 'hives_update_project_environment')
|
|
143
|
+
input = { ...input, body: validateHivesEnvironment(input.body) };
|
|
144
|
+
const objectBody = isHivesObjectUpload(operationId) ? await hivesObjectBody(input.body) : undefined;
|
|
145
|
+
const url = this.url(operationId, input);
|
|
146
|
+
const headers = {
|
|
147
|
+
accept: 'application/json',
|
|
148
|
+
'user-agent': this.opts.userAgent ?? '@onesi/genesis/0.1.13',
|
|
149
|
+
...this.opts.headers, ...init.headers,
|
|
150
|
+
};
|
|
151
|
+
if (op.service === 'blinds' && !['GET', 'HEAD', 'OPTIONS'].includes(op.method))
|
|
152
|
+
blindsOperationKey(operationId, input, headers);
|
|
153
|
+
if (isHivesQueueFundingOperation(operationId)) {
|
|
154
|
+
const keys = Object.keys(headers).filter(name => name.toLowerCase() === 'idempotency-key');
|
|
155
|
+
if (keys.length > 1 || keys.some(name => !/^[ -~]{1,128}$/.test(headers[name])))
|
|
156
|
+
throw resourceError(0, operationId);
|
|
157
|
+
}
|
|
158
|
+
if (runnerGrant) {
|
|
159
|
+
for (const key of Object.keys(headers))
|
|
160
|
+
if (!['accept', 'user-agent'].includes(key.toLowerCase()))
|
|
161
|
+
delete headers[key];
|
|
162
|
+
headers['x-request-id'] = runnerGrant.requestId;
|
|
163
|
+
headers['idempotency-key'] = runnerGrant.operationId;
|
|
164
|
+
}
|
|
165
|
+
if (operationId === 'flocks_exchangeJoinToken') {
|
|
166
|
+
for (const key of Object.keys(headers))
|
|
167
|
+
if (!['accept', 'user-agent'].includes(key.toLowerCase()))
|
|
168
|
+
delete headers[key];
|
|
169
|
+
}
|
|
170
|
+
if (!op.public) {
|
|
171
|
+
const cred = await this.credential();
|
|
172
|
+
// Direct mode: let the upstream decide (some, like Foundry's model list, are public).
|
|
173
|
+
if (!cred && this.routing !== 'direct')
|
|
174
|
+
throw new GenesisError({ status: 401, type: 'unauthorized', message: 'No Genesis credential. Run `genesis login`, or set GENESIS_API_KEY.', operationId });
|
|
175
|
+
if (cred)
|
|
176
|
+
headers.authorization = `Bearer ${cred}`;
|
|
177
|
+
}
|
|
178
|
+
// A timed-out credential provider must not dispatch a later resource mutation.
|
|
179
|
+
if (init.signal?.aborted && isHivesResourceOperation(operationId))
|
|
180
|
+
throw resourceError(0, operationId);
|
|
181
|
+
let body;
|
|
182
|
+
if (op.hasBody && input.body !== undefined) {
|
|
183
|
+
// Fetch owns multipart boundary generation. Never serialize a File/Blob as JSON.
|
|
184
|
+
for (const key of Object.keys(headers))
|
|
185
|
+
if (key.toLowerCase() === 'content-type')
|
|
186
|
+
delete headers[key];
|
|
187
|
+
if (objectBody !== undefined && op.requestContentType === 'application/octet-stream') {
|
|
188
|
+
headers['content-type'] = hivesContentType(input.content_type);
|
|
189
|
+
headers['content-length'] = String(objectBody.byteLength);
|
|
190
|
+
body = objectBody;
|
|
191
|
+
}
|
|
192
|
+
else if (op.requestContentType === 'multipart/form-data') {
|
|
193
|
+
if (!(input.body instanceof FormData))
|
|
194
|
+
throw new GenesisError({ status: 0, type: 'invalid_request', operationId,
|
|
195
|
+
message: 'This operation requires a FormData body containing a real binary file.' });
|
|
196
|
+
body = input.body;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
if (input.body instanceof FormData || input.body instanceof Blob || input.body instanceof ArrayBuffer || ArrayBuffer.isView(input.body)) {
|
|
200
|
+
throw new GenesisError({ status: 0, type: 'invalid_request', operationId, message: 'This operation requires a JSON body.' });
|
|
201
|
+
}
|
|
202
|
+
headers['content-type'] = 'application/json';
|
|
203
|
+
body = JSON.stringify(input.body);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const environmentOperation = op.service === 'hives' && operationId.endsWith('_project_environment');
|
|
207
|
+
const secretOperation = op.service === 'hives' && (/_secrets?$/.test(operationId) || environmentOperation);
|
|
208
|
+
const functionOperation = op.service === 'hives' && (op.rawResponse || /_function(?:s|_async)?$/.test(operationId));
|
|
209
|
+
let res;
|
|
210
|
+
try {
|
|
211
|
+
res = await this.fetchImpl(url, { method: op.method, headers, body, signal: init.signal,
|
|
212
|
+
...(op.service === 'hives' || op.service === 'flocks' || runnerGrant ? { cache: 'no-store', redirect: op.rawResponse && !runnerGrant ? 'manual' : 'error' } : {}) });
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (runnerGrant)
|
|
216
|
+
throw runnerGrantTransportError();
|
|
217
|
+
if (functionOperation)
|
|
218
|
+
throw new GenesisError({ status: 0, type: 'transport_error', operationId,
|
|
219
|
+
message: 'Hives function transport failed. Keep the original invocation key and reconcile its operation before retrying.' });
|
|
220
|
+
if (!secretOperation)
|
|
221
|
+
throw error;
|
|
222
|
+
throw new GenesisError({ status: 0, type: 'transport_error', operationId,
|
|
223
|
+
message: 'Hives secret transport failed. Redirects are refused; check the service address and retry.' });
|
|
224
|
+
}
|
|
225
|
+
if (runnerGrant)
|
|
226
|
+
return runnerGrantResponse(res, runnerGrant);
|
|
227
|
+
if (op.rawResponse && /^exec_[a-f0-9]{32}$/.test(res.headers.get('x-execution-id') ?? '')) {
|
|
228
|
+
return new Response(res.body, { status: res.status, statusText: res.statusText, headers: hivesFunctionResponseHeaders(res.headers) });
|
|
229
|
+
}
|
|
230
|
+
if (op.rawResponse && op.service === 'hives' && res.headers.has('x-hives-operation-id')) {
|
|
231
|
+
const error = await hivesOperationError(res);
|
|
232
|
+
if (error)
|
|
233
|
+
throw error;
|
|
234
|
+
throw new GenesisError({ status: res.status, type: 'invalid_response', operationId,
|
|
235
|
+
message: 'Hives operation status was not confirmed. Keep the original idempotency key and reconcile its operation.' });
|
|
236
|
+
}
|
|
237
|
+
if (op.rawResponse && res.ok)
|
|
238
|
+
throw new GenesisError({ status: res.status, type: 'invalid_response', operationId,
|
|
239
|
+
message: 'Hives did not confirm a completed guest execution. Keep the original invocation key and reconcile its operation.' });
|
|
240
|
+
if (functionOperation && !res.ok) {
|
|
241
|
+
const message = res.status === 401 ? 'Genesis credential rejected. Sign in again.'
|
|
242
|
+
: res.status === 403 ? 'Missing the authorized Hives scope for this function action.'
|
|
243
|
+
: res.status === 404 ? 'Function/deployment, project or artifact not found in the authenticated organization and project.'
|
|
244
|
+
: res.status === 409 ? 'Function mutation conflicted. Refresh its metadata before retrying.'
|
|
245
|
+
: res.status === 400 || res.status === 422 ? 'Invalid function request. Check artifact, resource bounds and guest request syntax.'
|
|
246
|
+
: res.status === 413 ? 'Function request exceeds a body or serialized guest envelope limit.'
|
|
247
|
+
: res.status >= 300 && res.status < 400 ? 'Unexpected function service redirect. No redirect was followed.'
|
|
248
|
+
: `Hives function operation failed (${res.status}). Refresh and retry.`;
|
|
249
|
+
throw new GenesisError({ status: res.status, type: `http_${res.status}`, operationId, message,
|
|
250
|
+
requestId: res.headers.get('x-request-id') ?? undefined });
|
|
251
|
+
}
|
|
252
|
+
if (!res.ok && secretOperation) {
|
|
253
|
+
const message = res.status === 401 ? 'Genesis credential rejected. Sign in again.'
|
|
254
|
+
: res.status === 403 ? 'Missing the authorized Hives scope for this action (hives:read or hives:write).'
|
|
255
|
+
: res.status === 404 ? 'Secret or project not found in your authenticated organization.'
|
|
256
|
+
: res.status === 409 ? 'A secret with this name already exists. Update it instead.'
|
|
257
|
+
: res.status === 400 || res.status === 422 ? 'Invalid secret request. Check names and UTF-8 value size.'
|
|
258
|
+
: res.status === 413 ? 'Secret request too large. Use a smaller batch.'
|
|
259
|
+
: res.status === 429 ? 'Too many requests. Wait briefly before retrying.'
|
|
260
|
+
: res.status === 503 ? 'Hives customer encryption is unavailable or stored secrets require protected migration.'
|
|
261
|
+
: `Hives secret operation failed (${res.status}). Refresh metadata and retry.`;
|
|
262
|
+
// Validation/provider bodies may reflect submitted plaintext. Never retain or log them.
|
|
263
|
+
throw new GenesisError({ status: res.status, type: `http_${res.status}`, message, operationId,
|
|
264
|
+
requestId: environmentOperation ? undefined : res.headers.get('x-request-id') ?? undefined });
|
|
265
|
+
}
|
|
266
|
+
if (!res.ok && isHivesQueueFundingOperation(operationId)) {
|
|
267
|
+
const receipt = await hivesQueueOperationError(res, input.operation_id);
|
|
268
|
+
if (receipt)
|
|
269
|
+
throw receipt;
|
|
270
|
+
}
|
|
271
|
+
if (!res.ok && isHivesResourceOperation(operationId))
|
|
272
|
+
throw await resourceErrorFromResponse(res, operationId);
|
|
273
|
+
if (!res.ok)
|
|
274
|
+
throw await GenesisError.fromResponse(res, operationId);
|
|
275
|
+
return res;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type GenesisContextSelection = {
|
|
2
|
+
organizationId: string;
|
|
3
|
+
} | {
|
|
4
|
+
organizationId: string;
|
|
5
|
+
projectId: string;
|
|
6
|
+
platformId: 'pf_console';
|
|
7
|
+
workspaceId: string;
|
|
8
|
+
fleetId: null;
|
|
9
|
+
environmentId: 'sandbox';
|
|
10
|
+
};
|
|
11
|
+
export interface GenesisContextSession {
|
|
12
|
+
principal: {
|
|
13
|
+
id: string;
|
|
14
|
+
kind: 'human';
|
|
15
|
+
email?: string;
|
|
16
|
+
};
|
|
17
|
+
session: {
|
|
18
|
+
id: string;
|
|
19
|
+
principalKind: 'human';
|
|
20
|
+
accessToken: string;
|
|
21
|
+
issuer: string;
|
|
22
|
+
audience: string;
|
|
23
|
+
issuedAt: string;
|
|
24
|
+
expiresAt: string;
|
|
25
|
+
organizationId: string;
|
|
26
|
+
tenantId?: string;
|
|
27
|
+
projectId?: string | null;
|
|
28
|
+
platformId?: string | null;
|
|
29
|
+
workspaceId?: string | null;
|
|
30
|
+
fleetId?: string | null;
|
|
31
|
+
resourceEnvironment?: string | null;
|
|
32
|
+
};
|
|
33
|
+
tenant: {
|
|
34
|
+
organizationId: string;
|
|
35
|
+
projectId?: string | null;
|
|
36
|
+
platformId?: string | null;
|
|
37
|
+
workspaceId?: string | null;
|
|
38
|
+
fleetId?: string | null;
|
|
39
|
+
environmentId?: string | null;
|
|
40
|
+
};
|
|
41
|
+
refresh_token: string;
|
|
42
|
+
}
|
|
43
|
+
/** Response consistency only: Auth verifies credentials, membership and IAM. */
|
|
44
|
+
export declare function checkedGenesisContextSession(value: unknown, priorToken: string, selection: GenesisContextSelection, now?: number): GenesisContextSession;
|
|
45
|
+
/** One authoritative tenant-context remint. Persist both returned tokens together before using the selection. No org header or local grant is created. */
|
|
46
|
+
export declare function selectGenesisContext(selection: GenesisContextSelection, options: {
|
|
47
|
+
credential: () => Promise<string | undefined>;
|
|
48
|
+
fetch?: typeof fetch;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}): Promise<GenesisContextSession>;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { GenesisError } from './errors.js';
|
|
2
|
+
const AUTH = 'https://auth.life.one';
|
|
3
|
+
const object = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
|
|
4
|
+
const id = (v, prefix) => typeof v === 'string' && new RegExp('^' + prefix + '_[A-Za-z0-9_-]{1,128}$').exec(v)?.[0] === v;
|
|
5
|
+
const secret = (v) => typeof v === 'string' && v.length > 0 && v.length <= 16_384 && !/[\s,\x00-\x1f\x7f]/.test(v);
|
|
6
|
+
function fail(status = 0) { throw new GenesisError({ status, type: 'context_unconfirmed', message: 'One account did not confirm this context. No automatic retry was made.' }); }
|
|
7
|
+
function claims(token) {
|
|
8
|
+
if (!secret(token))
|
|
9
|
+
return fail();
|
|
10
|
+
try {
|
|
11
|
+
const parts = token.split('.');
|
|
12
|
+
if (parts.length !== 3 || parts.some(p => !p || /[^A-Za-z0-9_-]/.test(p)))
|
|
13
|
+
return fail();
|
|
14
|
+
const json = atob(parts[1].replaceAll('-', '+').replaceAll('_', '/'));
|
|
15
|
+
const value = object(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.from(json, c => c.charCodeAt(0)))));
|
|
16
|
+
if (!value)
|
|
17
|
+
return fail();
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return fail();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function audiences(value) {
|
|
25
|
+
const values = Array.isArray(value) ? value : [value];
|
|
26
|
+
return values.length && values.every(v => typeof v === 'string' && v.length > 0 && v.length <= 256) ? values : fail();
|
|
27
|
+
}
|
|
28
|
+
function human(token, now) {
|
|
29
|
+
const c = claims(token);
|
|
30
|
+
if (c.iss !== AUTH || c.kind !== 'human' || !id(c.sub, 'h') || c.tid !== c.sub || !id(c.sid, 'sess')
|
|
31
|
+
|| !Number.isSafeInteger(c.exp) || c.exp <= now || c.cnf !== undefined || c.cap !== undefined || c.obo !== undefined)
|
|
32
|
+
return fail(401);
|
|
33
|
+
return c;
|
|
34
|
+
}
|
|
35
|
+
/** Response consistency only: Auth verifies credentials, membership and IAM. */
|
|
36
|
+
export function checkedGenesisContextSession(value, priorToken, selection, now = Date.now() / 1000) {
|
|
37
|
+
const prior = human(priorToken, now), root = object(value), p = object(root?.principal), s = object(root?.session), t = object(root?.tenant);
|
|
38
|
+
if (!root || !p || !s || !t || p.kind !== 'human' || p.id !== prior.sub || s.principalKind !== 'human' || !id(s.id, 'sess')
|
|
39
|
+
|| s.issuer !== AUTH || !secret(s.accessToken) || !secret(root.refresh_token) || s.organizationId !== selection.organizationId
|
|
40
|
+
|| t.organizationId !== selection.organizationId || typeof s.audience !== 'string' || typeof s.issuedAt !== 'string' || typeof s.expiresAt !== 'string')
|
|
41
|
+
return fail();
|
|
42
|
+
const next = human(s.accessToken, now), aud = audiences(next.aud), previousAud = audiences(prior.aud);
|
|
43
|
+
if (s.id === prior.sid || next.sub !== prior.sub || next.sid !== s.id || next.org !== selection.organizationId || !aud.includes(s.audience) || !previousAud.includes(s.audience)
|
|
44
|
+
|| !Number.isFinite(Date.parse(s.issuedAt)) || !Number.isFinite(Date.parse(s.expiresAt)) || Date.parse(s.issuedAt) / 1000 > now + 60
|
|
45
|
+
|| Date.parse(s.expiresAt) / 1000 <= now || Date.parse(s.expiresAt) <= Date.parse(s.issuedAt)
|
|
46
|
+
|| (s.tenantId !== undefined && s.tenantId !== null && s.tenantId !== prior.sub))
|
|
47
|
+
return fail();
|
|
48
|
+
// Validate optional projections even for an organization-only remint. Never
|
|
49
|
+
// cast unvalidated provider fields into a public selected-context result.
|
|
50
|
+
const fields = [['projectId', 'pid', 'pr'], ['platformId', 'pfid', 'pf'], ['workspaceId', 'wid', 'w'], ['fleetId', 'flid', 'fl']];
|
|
51
|
+
for (const [name, claim, prefix] of fields) {
|
|
52
|
+
if ([s[name], t[name], next[claim]].some(v => v !== undefined && v !== null && !id(v, prefix))
|
|
53
|
+
|| (s[name] ?? null) !== (t[name] ?? null) || (s[name] ?? null) !== (next[claim] ?? null))
|
|
54
|
+
return fail();
|
|
55
|
+
}
|
|
56
|
+
if (![undefined, null, 'sandbox', 'live'].includes(s.resourceEnvironment)
|
|
57
|
+
|| (s.resourceEnvironment ?? null) !== (t.environmentId ?? null))
|
|
58
|
+
return fail();
|
|
59
|
+
if ('projectId' in selection) {
|
|
60
|
+
for (const [name, claim] of [['projectId', 'pid'], ['platformId', 'pfid'], ['workspaceId', 'wid'], ['fleetId', 'flid']]) {
|
|
61
|
+
if ((s[name] ?? null) !== selection[name] || (t[name] ?? null) !== selection[name] || (next[claim] ?? null) !== selection[name])
|
|
62
|
+
return fail();
|
|
63
|
+
}
|
|
64
|
+
if (s.resourceEnvironment !== selection.environmentId || t.environmentId !== selection.environmentId)
|
|
65
|
+
return fail();
|
|
66
|
+
}
|
|
67
|
+
return { principal: { id: p.id, kind: 'human', ...(typeof p.email === 'string' ? { email: p.email } : {}) },
|
|
68
|
+
session: { id: s.id, principalKind: 'human', accessToken: s.accessToken, issuer: AUTH, audience: s.audience, issuedAt: s.issuedAt, expiresAt: s.expiresAt,
|
|
69
|
+
organizationId: s.organizationId, tenantId: prior.sub, projectId: s.projectId,
|
|
70
|
+
platformId: s.platformId, workspaceId: s.workspaceId,
|
|
71
|
+
fleetId: s.fleetId, resourceEnvironment: s.resourceEnvironment },
|
|
72
|
+
tenant: { organizationId: t.organizationId, projectId: t.projectId, platformId: t.platformId,
|
|
73
|
+
workspaceId: t.workspaceId, fleetId: t.fleetId, environmentId: t.environmentId },
|
|
74
|
+
refresh_token: root.refresh_token };
|
|
75
|
+
}
|
|
76
|
+
/** One authoritative tenant-context remint. Persist both returned tokens together before using the selection. No org header or local grant is created. */
|
|
77
|
+
export async function selectGenesisContext(selection, options) {
|
|
78
|
+
const requested = { ...selection };
|
|
79
|
+
const keys = Object.keys(requested), full = 'projectId' in requested;
|
|
80
|
+
if (!id(requested.organizationId, 'o') || keys.some(k => !(full ? ['organizationId', 'projectId', 'platformId', 'workspaceId', 'fleetId', 'environmentId'] : ['organizationId']).includes(k))
|
|
81
|
+
|| (full && (!id(requested.projectId, 'pr') || !id(requested.workspaceId, 'w') || requested.platformId !== 'pf_console' || requested.fleetId !== null || requested.environmentId !== 'sandbox')))
|
|
82
|
+
return fail(400);
|
|
83
|
+
const controller = new AbortController(), cancel = () => controller.abort();
|
|
84
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
85
|
+
if (options.signal?.aborted)
|
|
86
|
+
cancel();
|
|
87
|
+
const timer = setTimeout(cancel, 30_000);
|
|
88
|
+
let reader;
|
|
89
|
+
const stopped = new Promise((_, reject) => controller.signal.addEventListener('abort', () => { void reader?.cancel().catch(() => { }); reject(new Error('cancelled')); }, { once: true }));
|
|
90
|
+
try {
|
|
91
|
+
controller.signal.throwIfAborted();
|
|
92
|
+
const token = await Promise.race([options.credential(), stopped]);
|
|
93
|
+
if (!token)
|
|
94
|
+
return fail(401);
|
|
95
|
+
human(token, Date.now() / 1000);
|
|
96
|
+
controller.signal.throwIfAborted();
|
|
97
|
+
const response = await Promise.race([(options.fetch ?? fetch)(AUTH + '/v4/auth/tenant-context', { method: 'POST', headers: { authorization: 'Bearer ' + token, accept: 'application/json', 'content-type': 'application/json' },
|
|
98
|
+
body: JSON.stringify(requested), signal: controller.signal, redirect: 'error', cache: 'no-store' }).then(value => {
|
|
99
|
+
if (controller.signal.aborted) {
|
|
100
|
+
void value.body?.cancel().catch(() => { });
|
|
101
|
+
throw new Error('cancelled');
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}), stopped]);
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
void response.body?.cancel().catch(() => { });
|
|
107
|
+
return fail(response.status);
|
|
108
|
+
}
|
|
109
|
+
if (!/^application\/json(?:;|$)/i.test(response.headers.get('content-type') ?? '')) {
|
|
110
|
+
void response.body?.cancel().catch(() => { });
|
|
111
|
+
return fail();
|
|
112
|
+
}
|
|
113
|
+
reader = response.body?.getReader();
|
|
114
|
+
if (!reader)
|
|
115
|
+
return fail();
|
|
116
|
+
const parts = [];
|
|
117
|
+
let size = 0;
|
|
118
|
+
for (;;) {
|
|
119
|
+
const next = await Promise.race([reader.read(), stopped]);
|
|
120
|
+
if (next.done)
|
|
121
|
+
break;
|
|
122
|
+
size += next.value.length;
|
|
123
|
+
if (size > 65536)
|
|
124
|
+
return fail();
|
|
125
|
+
parts.push(next.value);
|
|
126
|
+
}
|
|
127
|
+
const bytes = new Uint8Array(size);
|
|
128
|
+
let at = 0;
|
|
129
|
+
for (const part of parts) {
|
|
130
|
+
bytes.set(part, at);
|
|
131
|
+
at += part.length;
|
|
132
|
+
}
|
|
133
|
+
controller.signal.throwIfAborted();
|
|
134
|
+
const value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
135
|
+
return checkedGenesisContextSession(value, token, requested);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error instanceof GenesisError)
|
|
139
|
+
throw error;
|
|
140
|
+
return fail();
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
clearTimeout(timer);
|
|
144
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
145
|
+
if (reader) {
|
|
146
|
+
void reader.cancel().catch(() => { });
|
|
147
|
+
try {
|
|
148
|
+
reader.releaseLock();
|
|
149
|
+
}
|
|
150
|
+
catch { /* cancelled pending read */ }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|