@lifesi/genesis 0.1.0 → 0.1.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/dist/beacons.d.ts +46 -0
- package/dist/beacons.js +20 -0
- package/dist/client.d.ts +7179 -1234
- package/dist/client.js +139 -5
- package/dist/dials.d.ts +254 -6
- package/dist/dials.js +82 -1
- package/dist/fleets.d.ts +53 -1
- package/dist/fleets.js +35 -1
- package/dist/flocks.d.ts +73 -0
- package/dist/flocks.js +22 -0
- package/dist/generated/operations.d.ts +7178 -1233
- package/dist/generated/operations.js +8991 -1579
- package/dist/generated/schema.d.ts +15611 -5129
- package/dist/genesis.d.ts +990 -29
- package/dist/genesis.js +46 -4
- package/dist/genesis.openapi.json +22450 -6816
- 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 +15 -0
- package/dist/hives-functions.js +94 -0
- package/dist/hives-node.d.ts +9 -0
- package/dist/hives-node.js +152 -0
- package/dist/hives-operations.d.ts +304 -0
- package/dist/hives-operations.js +39 -0
- package/dist/hives-queues.d.ts +121 -0
- package/dist/hives-queues.js +106 -0
- package/dist/hives-resources-transport.d.ts +18 -0
- package/dist/hives-resources-transport.js +165 -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-upload.d.ts +9 -0
- package/dist/hives-upload.js +23 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.js +7 -1
- package/dist/runner-grant.d.ts +10 -0
- package/dist/runner-grant.js +133 -0
- package/dist/runner.d.ts +16 -0
- package/dist/runner.js +4 -0
- package/dist/types.d.ts +11 -1
- package/package.json +14 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { GenesisClient } from './client.js';
|
|
2
|
+
import { type HivesResourceOptions } from './hives-resources-transport.js';
|
|
3
|
+
export interface HivesCorsRule {
|
|
4
|
+
allowed_origins: string[];
|
|
5
|
+
allowed_methods: string[];
|
|
6
|
+
allowed_headers: string[];
|
|
7
|
+
exposed_headers: string[];
|
|
8
|
+
max_age_secs: number;
|
|
9
|
+
}
|
|
10
|
+
export interface HivesBucket {
|
|
11
|
+
id: string;
|
|
12
|
+
tenant_id: string;
|
|
13
|
+
project_id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
region: string;
|
|
16
|
+
visibility: string;
|
|
17
|
+
status: string;
|
|
18
|
+
object_count: number | null;
|
|
19
|
+
size_bytes: number | null;
|
|
20
|
+
statistics_scope: 'unmeasured_provider_inventory' | 'retained_legacy_metadata';
|
|
21
|
+
created_at: string;
|
|
22
|
+
provider_binding: {
|
|
23
|
+
provider: 'gcs';
|
|
24
|
+
region: string;
|
|
25
|
+
state: string;
|
|
26
|
+
requires_reconciliation: boolean;
|
|
27
|
+
} | null;
|
|
28
|
+
operation_state: string | null;
|
|
29
|
+
}
|
|
30
|
+
export interface HivesObject {
|
|
31
|
+
key: string;
|
|
32
|
+
bucket_id: string;
|
|
33
|
+
size_bytes: number;
|
|
34
|
+
etag: string;
|
|
35
|
+
last_modified: string;
|
|
36
|
+
generation: string;
|
|
37
|
+
content_type: string | null;
|
|
38
|
+
metadata: Record<string, string> | null;
|
|
39
|
+
metadata_scope: string;
|
|
40
|
+
}
|
|
41
|
+
export interface HivesObjectAcknowledgement {
|
|
42
|
+
bucket_id: string;
|
|
43
|
+
key: string;
|
|
44
|
+
size_bytes: number;
|
|
45
|
+
etag: string;
|
|
46
|
+
generation: string;
|
|
47
|
+
content_type: string | null;
|
|
48
|
+
}
|
|
49
|
+
export interface HivesObjectList {
|
|
50
|
+
objects: HivesObject[];
|
|
51
|
+
common_prefixes: string[];
|
|
52
|
+
is_truncated: boolean;
|
|
53
|
+
next_continuation_token: string | null;
|
|
54
|
+
total: number | null;
|
|
55
|
+
}
|
|
56
|
+
export interface HivesBucketList {
|
|
57
|
+
data: HivesBucket[];
|
|
58
|
+
pagination: {
|
|
59
|
+
page: number;
|
|
60
|
+
per_page: number;
|
|
61
|
+
total: number;
|
|
62
|
+
total_pages: number;
|
|
63
|
+
has_next: boolean;
|
|
64
|
+
has_prev: boolean;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export interface HivesCreateBucket {
|
|
68
|
+
name: string;
|
|
69
|
+
region?: 'us-central1';
|
|
70
|
+
visibility?: 'private' | 'publicread';
|
|
71
|
+
cors_rules?: HivesCorsRule[];
|
|
72
|
+
}
|
|
73
|
+
export interface HivesPresign {
|
|
74
|
+
key: string;
|
|
75
|
+
method?: 'GET' | 'PUT';
|
|
76
|
+
expires_in_secs?: number;
|
|
77
|
+
content_type?: string;
|
|
78
|
+
content_length?: number;
|
|
79
|
+
}
|
|
80
|
+
export interface HivesPresignedUrl {
|
|
81
|
+
url: string;
|
|
82
|
+
expires_at: string;
|
|
83
|
+
method: 'GET' | 'PUT';
|
|
84
|
+
required_headers?: Record<string, string>;
|
|
85
|
+
}
|
|
86
|
+
export declare function hivesObjectKey(key: string): string;
|
|
87
|
+
export declare function hivesCells(client: GenesisClient): {
|
|
88
|
+
list(project: string, query?: {
|
|
89
|
+
page?: number;
|
|
90
|
+
per_page?: number;
|
|
91
|
+
}, options?: HivesResourceOptions): Promise<HivesBucketList>;
|
|
92
|
+
get(project: string, id: string, options?: HivesResourceOptions): Promise<HivesBucket>;
|
|
93
|
+
/** A pending provider_binding is an uncertain outcome, never a completed-create claim. */
|
|
94
|
+
create(project: string, body: HivesCreateBucket, options?: HivesResourceOptions): Promise<HivesBucket>;
|
|
95
|
+
remove(project: string, id: string, options?: HivesResourceOptions): Promise<{
|
|
96
|
+
id: string;
|
|
97
|
+
success: boolean;
|
|
98
|
+
message: string;
|
|
99
|
+
}>;
|
|
100
|
+
reconcile(project: string, id: string, options?: HivesResourceOptions): Promise<{
|
|
101
|
+
bucket: HivesBucket;
|
|
102
|
+
outcome: string;
|
|
103
|
+
}>;
|
|
104
|
+
cors: {
|
|
105
|
+
get: (project: string, id: string, options?: HivesResourceOptions) => Promise<HivesCorsRule[]>;
|
|
106
|
+
set: (project: string, id: string, rules: HivesCorsRule[], options?: HivesResourceOptions) => Promise<HivesCorsRule[]>;
|
|
107
|
+
};
|
|
108
|
+
objects: {
|
|
109
|
+
list(project: string, id: string, query?: {
|
|
110
|
+
prefix?: string;
|
|
111
|
+
delimiter?: string;
|
|
112
|
+
continuation_token?: string;
|
|
113
|
+
max_keys?: number;
|
|
114
|
+
}, options?: HivesResourceOptions): Promise<HivesObjectList>;
|
|
115
|
+
put(project: string, id: string, key: string, bytes: Blob | ArrayBuffer | ArrayBufferView, options?: HivesResourceOptions & {
|
|
116
|
+
content_type?: string;
|
|
117
|
+
}): Promise<HivesObjectAcknowledgement>;
|
|
118
|
+
remove: (project: string, id: string, key: string, options?: HivesResourceOptions) => Promise<void>;
|
|
119
|
+
download: (project: string, id: string, key: string, options?: HivesResourceOptions) => Promise<Response>;
|
|
120
|
+
/** Existing flat-key path compatibility; nested keys use canonical collection methods above. */
|
|
121
|
+
putFlat(project: string, id: string, key: string, bytes: Blob | ArrayBuffer | ArrayBufferView, options?: HivesResourceOptions & {
|
|
122
|
+
content_type?: string;
|
|
123
|
+
}): Promise<HivesObjectAcknowledgement>;
|
|
124
|
+
removeFlat: (project: string, id: string, key: string, options?: HivesResourceOptions) => Promise<void>;
|
|
125
|
+
};
|
|
126
|
+
presign(project: string, id: string, body: HivesPresign, options?: HivesResourceOptions): Promise<HivesPresignedUrl>;
|
|
127
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { HIVES_OBJECT_BYTES, hivesResourceCall, hivesResourceId, hivesObjectBody, hivesContentType, resourceError } from './hives-resources-transport.js';
|
|
2
|
+
export function hivesObjectKey(key) {
|
|
3
|
+
const bytes = new TextEncoder().encode(key);
|
|
4
|
+
if (typeof key !== 'string' || !key || bytes.length > 1024 || new TextDecoder('utf-8', { fatal: true }).decode(bytes) !== key
|
|
5
|
+
|| key.startsWith('/') || key.endsWith('/') || /[\u0000-\u001f\u007f-\u009f\\]/.test(key) || key.split('/').some(p => !p || p === '.' || p === '..'))
|
|
6
|
+
throw resourceError(0);
|
|
7
|
+
let binary = '';
|
|
8
|
+
for (const byte of bytes)
|
|
9
|
+
binary += String.fromCharCode(byte);
|
|
10
|
+
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
11
|
+
}
|
|
12
|
+
function cors(rules) {
|
|
13
|
+
if (!Array.isArray(rules) || rules.length > 100)
|
|
14
|
+
throw resourceError(0);
|
|
15
|
+
return rules.map(rule => {
|
|
16
|
+
if (!rule || typeof rule !== 'object' || Object.keys(rule).some(k => !['allowed_origins', 'allowed_methods', 'allowed_headers', 'exposed_headers', 'max_age_secs'].includes(k))
|
|
17
|
+
|| !Array.isArray(rule.allowed_origins) || !rule.allowed_origins.length || rule.allowed_origins.length > 100
|
|
18
|
+
|| !Array.isArray(rule.allowed_methods) || !rule.allowed_methods.length || rule.allowed_methods.length > 6
|
|
19
|
+
|| !Array.isArray(rule.allowed_headers) || rule.allowed_headers.length !== 1 || rule.allowed_headers[0] !== '*'
|
|
20
|
+
|| !Array.isArray(rule.exposed_headers) || rule.exposed_headers.length > 100
|
|
21
|
+
|| !Number.isInteger(rule.max_age_secs) || rule.max_age_secs < 0 || rule.max_age_secs > 86400)
|
|
22
|
+
throw resourceError(0);
|
|
23
|
+
for (const origin of rule.allowed_origins) {
|
|
24
|
+
if (origin === '*')
|
|
25
|
+
continue;
|
|
26
|
+
if (typeof origin !== 'string')
|
|
27
|
+
throw resourceError(0);
|
|
28
|
+
let url;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(origin);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw resourceError(0);
|
|
34
|
+
}
|
|
35
|
+
if (!['http:', 'https:'].includes(url.protocol) || !url.hostname || url.username || url.password || url.pathname !== '/' || url.search || url.hash)
|
|
36
|
+
throw resourceError(0);
|
|
37
|
+
}
|
|
38
|
+
if (rule.allowed_methods.some(m => !['GET', 'HEAD', 'PUT', 'POST', 'DELETE'].includes(m))
|
|
39
|
+
|| rule.exposed_headers.some(h => typeof h !== 'string' || h.length > 256 || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(h)))
|
|
40
|
+
throw resourceError(0);
|
|
41
|
+
return { allowed_origins: [...rule.allowed_origins], allowed_methods: [...rule.allowed_methods], allowed_headers: ['*'], exposed_headers: [...rule.exposed_headers], max_age_secs: rule.max_age_secs };
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function bucket(value, project, id) {
|
|
45
|
+
if (!value || typeof value !== 'object' || value.project_id !== project || typeof value.id !== 'string' || id !== undefined && value.id !== id
|
|
46
|
+
|| !['unmeasured_provider_inventory', 'retained_legacy_metadata'].includes(value.statistics_scope)
|
|
47
|
+
|| [value.object_count, value.size_bytes].some(v => v !== null && (!Number.isSafeInteger(v) || v < 0))
|
|
48
|
+
|| (value.statistics_scope === 'unmeasured_provider_inventory' && (value.object_count !== null || value.size_bytes !== null)))
|
|
49
|
+
throw resourceError(0);
|
|
50
|
+
if (value.statistics_scope === 'unmeasured_provider_inventory' && !value.provider_binding)
|
|
51
|
+
throw resourceError(0);
|
|
52
|
+
if (value.provider_binding !== null && (!value.provider_binding || value.provider_binding.provider !== 'gcs' || typeof value.provider_binding.state !== 'string' || typeof value.provider_binding.requires_reconciliation !== 'boolean'))
|
|
53
|
+
throw resourceError(0);
|
|
54
|
+
return Object.fromEntries(['id', 'tenant_id', 'project_id', 'name', 'region', 'visibility', 'status', 'object_count', 'size_bytes', 'statistics_scope', 'created_at', 'provider_binding', 'operation_state'].map(k => [k, value[k]]));
|
|
55
|
+
}
|
|
56
|
+
function object(value, id) {
|
|
57
|
+
if (!value || value.bucket_id !== id || typeof value.key !== 'string' || !Number.isSafeInteger(value.size_bytes) || value.size_bytes < 0 || typeof value.generation !== 'string')
|
|
58
|
+
throw resourceError(0);
|
|
59
|
+
hivesObjectKey(value.key);
|
|
60
|
+
return Object.fromEntries(['key', 'bucket_id', 'size_bytes', 'etag', 'last_modified', 'generation', 'content_type', 'metadata', 'metadata_scope'].map(k => [k, value[k]]));
|
|
61
|
+
}
|
|
62
|
+
function acknowledgement(value, key, id, size) {
|
|
63
|
+
if (!value || value.bucket_id !== id || value.key !== key || value.size_bytes !== size || !Number.isSafeInteger(value.size_bytes) || value.size_bytes < 0 || typeof value.etag !== 'string' || typeof value.generation !== 'string' || value.content_type !== null && typeof value.content_type !== 'string')
|
|
64
|
+
throw resourceError(0);
|
|
65
|
+
return { key: value.key, bucket_id: value.bucket_id, size_bytes: value.size_bytes, etag: value.etag, generation: value.generation, content_type: value.content_type };
|
|
66
|
+
}
|
|
67
|
+
export function hivesCells(client) {
|
|
68
|
+
const ids = (project_id, bucket_id) => ({ project_id: hivesResourceId(project_id), ...(bucket_id === undefined ? {} : { bucket_id: hivesResourceId(bucket_id) }) });
|
|
69
|
+
const call = (id, input, options, mode = 'json', limit = 2 * 1024 * 1024) => hivesResourceCall(client, id, input, options, mode, limit, 100_000);
|
|
70
|
+
return {
|
|
71
|
+
async list(project, query = {}, options) {
|
|
72
|
+
if (Object.keys(query).some(k => !['page', 'per_page'].includes(k)) || query.page !== undefined && (!Number.isInteger(query.page) || query.page < 1 || query.page > 10000) || query.per_page !== undefined && (!Number.isInteger(query.per_page) || query.per_page < 1 || query.per_page > 100))
|
|
73
|
+
throw resourceError(0);
|
|
74
|
+
const result = await call('hives_list_buckets', { ...query, ...ids(project) }, options);
|
|
75
|
+
if (!result || !Array.isArray(result.data) || !result.pagination)
|
|
76
|
+
throw resourceError(0);
|
|
77
|
+
return { ...result, data: result.data.map(row => bucket(row, project)) };
|
|
78
|
+
},
|
|
79
|
+
async get(project, id, options) { return bucket(await call('hives_get_bucket', ids(project, id), options), project, id); },
|
|
80
|
+
/** A pending provider_binding is an uncertain outcome, never a completed-create claim. */
|
|
81
|
+
async create(project, body, options) {
|
|
82
|
+
if (!body || typeof body.name !== 'string' || !/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(body.name) || body.region !== undefined && body.region !== 'us-central1'
|
|
83
|
+
|| body.visibility !== undefined && !['private', 'publicread'].includes(body.visibility) || Object.keys(body).some(k => !['name', 'region', 'visibility', 'cors_rules'].includes(k)))
|
|
84
|
+
throw resourceError(0);
|
|
85
|
+
const checked = { ...body, ...(body.cors_rules === undefined ? {} : { cors_rules: cors(body.cors_rules) }) };
|
|
86
|
+
return bucket(await call('hives_create_bucket', { ...ids(project), body: checked }, options), project);
|
|
87
|
+
},
|
|
88
|
+
async remove(project, id, options) {
|
|
89
|
+
const result = await call('hives_delete_bucket', ids(project, id), options);
|
|
90
|
+
if (!result || result.id !== id || result.success !== true)
|
|
91
|
+
throw resourceError(0);
|
|
92
|
+
return result;
|
|
93
|
+
},
|
|
94
|
+
async reconcile(project, id, options) {
|
|
95
|
+
const result = await call('hives_reconcile', ids(project, id), options);
|
|
96
|
+
if (!result || typeof result.outcome !== 'string' || result.bucket?.id !== id)
|
|
97
|
+
throw resourceError(0);
|
|
98
|
+
return { ...result, bucket: bucket(result.bucket, project) };
|
|
99
|
+
},
|
|
100
|
+
cors: {
|
|
101
|
+
get: async (project, id, options) => cors(await call('hives_get_cors_config', ids(project, id), options)),
|
|
102
|
+
set: async (project, id, rules, options) => cors(await call('hives_set_cors_config', { ...ids(project, id), body: { rules: cors(rules) } }, options)),
|
|
103
|
+
},
|
|
104
|
+
objects: {
|
|
105
|
+
async list(project, id, query = {}, options) {
|
|
106
|
+
if (query.max_keys !== undefined && (!Number.isInteger(query.max_keys) || query.max_keys < 1 || query.max_keys > 1000))
|
|
107
|
+
throw resourceError(0);
|
|
108
|
+
const result = await call('hives_list_objects', { ...query, ...ids(project, id) }, options);
|
|
109
|
+
if (!result || !Array.isArray(result.objects) || !Array.isArray(result.common_prefixes) || typeof result.is_truncated !== 'boolean' || result.total !== null)
|
|
110
|
+
throw resourceError(0);
|
|
111
|
+
return { ...result, objects: result.objects.map(row => object(row, id)) };
|
|
112
|
+
},
|
|
113
|
+
async put(project, id, key, bytes, options = {}) {
|
|
114
|
+
const body = await hivesObjectBody(bytes);
|
|
115
|
+
const content_type = hivesContentType(options.content_type);
|
|
116
|
+
return acknowledgement(await call('hives_upload_object_by_key', { ...ids(project, id), key_base64url: hivesObjectKey(key), body, content_type }, options), key, id, body.byteLength);
|
|
117
|
+
},
|
|
118
|
+
remove: (project, id, key, options) => call('hives_delete_object_by_key', { ...ids(project, id), key_base64url: hivesObjectKey(key) }, options, 'empty'),
|
|
119
|
+
download: (project, id, key, options) => call('hives_download_object_by_key', { ...ids(project, id), key_base64url: hivesObjectKey(key) }, options, 'binary', HIVES_OBJECT_BYTES),
|
|
120
|
+
/** Existing flat-key path compatibility; nested keys use canonical collection methods above. */
|
|
121
|
+
async putFlat(project, id, key, bytes, options = {}) {
|
|
122
|
+
hivesObjectKey(key);
|
|
123
|
+
if (!/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/.test(key))
|
|
124
|
+
throw resourceError(0);
|
|
125
|
+
const body = await hivesObjectBody(bytes);
|
|
126
|
+
return acknowledgement(await call('hives_upload_object', { ...ids(project, id), key, body, content_type: hivesContentType(options.content_type) }, options), key, id, body.byteLength);
|
|
127
|
+
},
|
|
128
|
+
removeFlat: (project, id, key, options) => { hivesObjectKey(key); if (!/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/.test(key))
|
|
129
|
+
throw resourceError(0); return call('hives_delete_object', { ...ids(project, id), key }, options, 'empty'); },
|
|
130
|
+
},
|
|
131
|
+
async presign(project, id, body, options) {
|
|
132
|
+
hivesObjectKey(body.key);
|
|
133
|
+
const method = body.method ?? 'GET';
|
|
134
|
+
if (!['GET', 'PUT'].includes(method) || body.expires_in_secs !== undefined && (!Number.isInteger(body.expires_in_secs) || body.expires_in_secs < 60 || body.expires_in_secs > 900)
|
|
135
|
+
|| method === 'PUT' && (body.content_length === undefined || !Number.isInteger(body.content_length) || body.content_length < 0 || body.content_length > HIVES_OBJECT_BYTES))
|
|
136
|
+
throw resourceError(0);
|
|
137
|
+
if (body.content_type !== undefined)
|
|
138
|
+
hivesContentType(body.content_type);
|
|
139
|
+
const result = await call('hives_generate_presigned_url', { ...ids(project, id), body }, options);
|
|
140
|
+
if (!result || typeof result.url !== 'string' || result.method !== method || typeof result.expires_at !== 'string')
|
|
141
|
+
throw resourceError(0);
|
|
142
|
+
const url = new URL(result.url);
|
|
143
|
+
if (url.protocol !== 'https:' || url.hostname !== 'storage.googleapis.com' || url.username || url.password || url.hash)
|
|
144
|
+
throw resourceError(0);
|
|
145
|
+
const headers = result.required_headers;
|
|
146
|
+
if (headers !== undefined && (!headers || typeof headers !== 'object' || Array.isArray(headers) || Object.entries(headers).some(([k, v]) => !['content-type', 'content-length', 'x-goog-if-generation-match'].includes(k.toLowerCase()) || typeof v !== 'string' || /[\u0000-\u001f\u007f]/.test(v))))
|
|
147
|
+
throw resourceError(0);
|
|
148
|
+
return { url: result.url, expires_at: result.expires_at, method: result.method, ...(headers ? { required_headers: { ...headers } } : {}) };
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Environment metadata never carries stored values. Reveals are explicit. */
|
|
2
|
+
export declare function validateHivesEnvironment(input: unknown): Record<string, string>;
|
|
3
|
+
export declare function hivesEnvironmentMetadata(value: unknown, project: unknown, updated: boolean): {
|
|
4
|
+
project_id: string;
|
|
5
|
+
updated: {
|
|
6
|
+
name: string;
|
|
7
|
+
}[];
|
|
8
|
+
data?: undefined;
|
|
9
|
+
pagination?: undefined;
|
|
10
|
+
} | {
|
|
11
|
+
project_id: string;
|
|
12
|
+
data: {
|
|
13
|
+
name: string;
|
|
14
|
+
}[];
|
|
15
|
+
pagination: {
|
|
16
|
+
page: unknown;
|
|
17
|
+
per_page: unknown;
|
|
18
|
+
total: unknown;
|
|
19
|
+
total_pages: unknown;
|
|
20
|
+
has_next: boolean;
|
|
21
|
+
has_prev: boolean;
|
|
22
|
+
};
|
|
23
|
+
updated?: undefined;
|
|
24
|
+
};
|
|
25
|
+
export declare function hivesEnvironmentReveal(value: unknown, key: unknown): {
|
|
26
|
+
name: string;
|
|
27
|
+
value: string;
|
|
28
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Environment metadata never carries stored values. Reveals are explicit. */
|
|
2
|
+
export function validateHivesEnvironment(input) {
|
|
3
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
4
|
+
throw new Error('Environment input must be a flat JSON map of names to string values.');
|
|
5
|
+
const entries = Object.entries(input);
|
|
6
|
+
if (entries.length < 1 || entries.length > 100)
|
|
7
|
+
throw new Error('Environment updates require 1–100 names.');
|
|
8
|
+
const result = Object.create(null);
|
|
9
|
+
for (const [name, value] of entries) {
|
|
10
|
+
if (name.length > 255 || !/^[A-Za-z][A-Za-z0-9_]*$/.test(name))
|
|
11
|
+
throw new Error('Environment names must start with a letter and contain only letters, numbers and underscores (up to 255 characters).');
|
|
12
|
+
if (typeof value !== 'string' || new TextEncoder().encode(value).length > 65536)
|
|
13
|
+
throw new Error('Environment values must be strings of at most 65,536 UTF-8 bytes. Empty values are allowed.');
|
|
14
|
+
result[name] = value;
|
|
15
|
+
}
|
|
16
|
+
if (new TextEncoder().encode(JSON.stringify(result)).length > 2 * 1024 * 1024)
|
|
17
|
+
throw new Error('Environment input exceeds 2 MiB. Split it into smaller batches.');
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function record(value) {
|
|
21
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
22
|
+
throw new Error('Hives returned invalid environment metadata.');
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function names(value) {
|
|
26
|
+
if (!Array.isArray(value))
|
|
27
|
+
throw new Error('Hives returned invalid environment metadata.');
|
|
28
|
+
return value.map((value) => {
|
|
29
|
+
const row = record(value);
|
|
30
|
+
if (typeof row.name !== 'string')
|
|
31
|
+
throw new Error('Hives returned invalid environment metadata.');
|
|
32
|
+
return { name: row.name };
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function hivesEnvironmentMetadata(value, project, updated) {
|
|
36
|
+
const data = record(value);
|
|
37
|
+
if (typeof project !== 'string' || data.project_id !== project)
|
|
38
|
+
throw new Error('Hives returned environment metadata for a different project.');
|
|
39
|
+
if (updated)
|
|
40
|
+
return { project_id: project, updated: names(data.updated) };
|
|
41
|
+
const pagination = record(data.pagination);
|
|
42
|
+
for (const key of ['page', 'per_page', 'total', 'total_pages']) {
|
|
43
|
+
if (!Number.isSafeInteger(pagination[key]) || pagination[key] < (key === 'page' || key === 'per_page' ? 1 : 0))
|
|
44
|
+
throw new Error('Hives returned invalid environment pagination.');
|
|
45
|
+
}
|
|
46
|
+
if (typeof pagination.has_next !== 'boolean' || typeof pagination.has_prev !== 'boolean')
|
|
47
|
+
throw new Error('Hives returned invalid environment pagination.');
|
|
48
|
+
return { project_id: project, data: names(data.data), pagination: {
|
|
49
|
+
page: pagination.page, per_page: pagination.per_page, total: pagination.total, total_pages: pagination.total_pages,
|
|
50
|
+
has_next: pagination.has_next, has_prev: pagination.has_prev,
|
|
51
|
+
} };
|
|
52
|
+
}
|
|
53
|
+
export function hivesEnvironmentReveal(value, key) {
|
|
54
|
+
const data = record(value);
|
|
55
|
+
if (typeof key !== 'string' || data.name !== key || typeof data.value !== 'string' || new TextEncoder().encode(data.value).length > 65536)
|
|
56
|
+
throw new Error('Hives returned invalid environment reveal data.');
|
|
57
|
+
return { name: data.name, value: data.value };
|
|
58
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** The service separately bounds its serialized guest stdin envelope at 10 MiB. */
|
|
2
|
+
export declare const MAX_FUNCTION_BODY_BYTES: number;
|
|
3
|
+
export declare const MAX_FUNCTION_JSON_BYTES: number;
|
|
4
|
+
export interface HivesInvokeRequest {
|
|
5
|
+
method?: string;
|
|
6
|
+
path?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
body_base64?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function validateHivesInvocation(input: unknown): asserts input is HivesInvokeRequest;
|
|
12
|
+
/** Safe display/transport headers; customer headers never establish identity. */
|
|
13
|
+
export declare function hivesFunctionResponseHeaders(headers: Headers): Headers;
|
|
14
|
+
/** Metadata contracts cannot accidentally expose an upstream env/ciphertext field. */
|
|
15
|
+
export declare function hivesFunctionMetadata(input: unknown, project: unknown): Record<string, unknown>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** The service separately bounds its serialized guest stdin envelope at 10 MiB. */
|
|
2
|
+
export const MAX_FUNCTION_BODY_BYTES = 10 * 1024 * 1024;
|
|
3
|
+
export const MAX_FUNCTION_JSON_BYTES = 16 * 1024 * 1024;
|
|
4
|
+
const reservedHeaders = new Set(['host', 'connection', 'content-length', 'transfer-encoding', 'keep-alive',
|
|
5
|
+
'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'upgrade', 'set-cookie', 'clear-site-data',
|
|
6
|
+
'authorization', 'cookie', 'forwarded', 'origin', 'referer', 'x-org-id', 'x-workspace-id', 'x-project-id',
|
|
7
|
+
'x-admin-token', 'x-tenant-id', 'x-cold-start']);
|
|
8
|
+
const platformHeader = (name) => /^x-(?:auth|genesis|life|l1fe|hives|forwarded|execution)-/.test(name);
|
|
9
|
+
const bytes = (value) => new TextEncoder().encode(value).length;
|
|
10
|
+
const fail = () => { throw new Error('Invalid function request. Check method, path, headers, exclusive body encoding and size limits.'); };
|
|
11
|
+
export function validateHivesInvocation(input) {
|
|
12
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
13
|
+
fail();
|
|
14
|
+
const r = input;
|
|
15
|
+
if (Object.keys(r).some((k) => !['method', 'path', 'headers', 'body', 'body_base64'].includes(k)))
|
|
16
|
+
fail();
|
|
17
|
+
if (r.method !== undefined && (typeof r.method !== 'string' || bytes(r.method) > 64 || !/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(r.method)))
|
|
18
|
+
fail();
|
|
19
|
+
if (r.path !== undefined && (typeof r.path !== 'string' || !r.path.startsWith('/') || r.path.startsWith('//') || r.path.includes('#') || bytes(r.path) > 8192 || /[\s\u0000-\u001f\u007f]/.test(r.path)))
|
|
20
|
+
fail();
|
|
21
|
+
let headerBytes = 0;
|
|
22
|
+
if (r.headers !== undefined) {
|
|
23
|
+
if (!r.headers || typeof r.headers !== 'object' || Array.isArray(r.headers) || Object.keys(r.headers).length > 128)
|
|
24
|
+
fail();
|
|
25
|
+
for (const [key, value] of Object.entries(r.headers)) {
|
|
26
|
+
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(key) || typeof value !== 'string' || /[\u0000-\u0008\u000a-\u001f\u007f]/.test(value)
|
|
27
|
+
|| reservedHeaders.has(key.toLowerCase()) || platformHeader(key.toLowerCase()))
|
|
28
|
+
fail();
|
|
29
|
+
headerBytes += bytes(key) + bytes(value);
|
|
30
|
+
if (headerBytes > 64 * 1024)
|
|
31
|
+
fail();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
let size = 0;
|
|
35
|
+
if (r.body_base64 !== undefined) {
|
|
36
|
+
const value = r.body_base64;
|
|
37
|
+
if (r.body != null || typeof value !== 'string' || value.length > Math.ceil(MAX_FUNCTION_BODY_BYTES / 3) * 4
|
|
38
|
+
|| value.length % 4 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value))
|
|
39
|
+
fail();
|
|
40
|
+
try {
|
|
41
|
+
const bytes = atob(value);
|
|
42
|
+
if (btoa(bytes) !== value)
|
|
43
|
+
fail();
|
|
44
|
+
size = bytes.length;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
fail();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (r.body != null) {
|
|
51
|
+
let value;
|
|
52
|
+
try {
|
|
53
|
+
value = typeof r.body === 'string' ? r.body : JSON.stringify(r.body);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
fail();
|
|
57
|
+
}
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
fail();
|
|
60
|
+
size = new TextEncoder().encode(value).length;
|
|
61
|
+
}
|
|
62
|
+
let serialized;
|
|
63
|
+
try {
|
|
64
|
+
serialized = JSON.stringify(r);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
fail();
|
|
68
|
+
}
|
|
69
|
+
if (size + headerBytes > MAX_FUNCTION_BODY_BYTES || !serialized || new TextEncoder().encode(serialized).length > MAX_FUNCTION_JSON_BYTES)
|
|
70
|
+
fail();
|
|
71
|
+
}
|
|
72
|
+
/** Safe display/transport headers; customer headers never establish identity. */
|
|
73
|
+
export function hivesFunctionResponseHeaders(headers) {
|
|
74
|
+
const blocked = new Set(['connection', 'keep-alive', 'transfer-encoding', 'upgrade', 'proxy-authenticate', 'proxy-authorization',
|
|
75
|
+
'te', 'trailer', 'content-length', 'content-encoding', 'set-cookie', 'clear-site-data', 'authorization', 'cookie', 'forwarded', 'host', 'x-org-id', 'x-workspace-id', 'x-project-id', 'x-admin-token', 'x-tenant-id', 'x-hives-guest-status']);
|
|
76
|
+
for (const value of (headers.get('connection') ?? '').split(','))
|
|
77
|
+
blocked.add(value.trim().toLowerCase());
|
|
78
|
+
const safe = new Headers();
|
|
79
|
+
headers.forEach((value, name) => {
|
|
80
|
+
if (!blocked.has(name) && (!platformHeader(name) || ['x-execution-id', 'x-execution-duration-ms'].includes(name)))
|
|
81
|
+
safe.set(name, value);
|
|
82
|
+
});
|
|
83
|
+
safe.set('cache-control', 'no-store');
|
|
84
|
+
return safe;
|
|
85
|
+
}
|
|
86
|
+
/** Metadata contracts cannot accidentally expose an upstream env/ciphertext field. */
|
|
87
|
+
export function hivesFunctionMetadata(input, project) {
|
|
88
|
+
if (!input || typeof input !== 'object' || input.project_id !== project) {
|
|
89
|
+
throw new Error('Hives returned invalid or wrong-project function metadata.');
|
|
90
|
+
}
|
|
91
|
+
const row = input;
|
|
92
|
+
return Object.fromEntries(['id', 'tenant_id', 'project_id', 'name', 'description', 'handler', 'route', 'memory_mb', 'timeout_ms',
|
|
93
|
+
'status', 'invocations', 'avg_duration_ms', 'created_at', 'updated_at'].map((key) => [key, row[key]]));
|
|
94
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Genesis } from './genesis.js';
|
|
2
|
+
import type { GenesisOptions } from './client.js';
|
|
3
|
+
export declare function closeHivesNodeTransport(): Promise<void>;
|
|
4
|
+
export declare function hivesOperationBudget(id: string): number;
|
|
5
|
+
/** Hives leaf transport. Caller fetch overrides stay authoritative; only native
|
|
6
|
+
* global fetch gets our Agent. Every operation retains an overall body deadline. */
|
|
7
|
+
export declare function createHivesNodeTransport(opts?: GenesisOptions, callerSignal?: AbortSignal): typeof fetch;
|
|
8
|
+
/** Node opt-in for Hives calls. Other services retain the caller transport. */
|
|
9
|
+
export declare function createHivesNodeClient(opts?: GenesisOptions, callerSignal?: AbortSignal): Genesis;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Agent } from 'undici';
|
|
2
|
+
import { Genesis } from './genesis.js';
|
|
3
|
+
import { operationTable } from './generated/operations.js';
|
|
4
|
+
// Node's native fetch accepts a compatible per-request dispatcher. Never change
|
|
5
|
+
// the global dispatcher or the portable SDK: other products retain their transport.
|
|
6
|
+
const agentOptions = { headersTimeout: 340_000, bodyTimeout: 340_000, connect: { timeout: 5_000 }, connections: 4, pipelining: 1 };
|
|
7
|
+
let agent;
|
|
8
|
+
const nativeAgent = () => agent ??= new Agent(agentOptions);
|
|
9
|
+
export async function closeHivesNodeTransport() {
|
|
10
|
+
const current = agent;
|
|
11
|
+
agent = undefined;
|
|
12
|
+
await current?.destroy();
|
|
13
|
+
}
|
|
14
|
+
export function hivesOperationBudget(id) {
|
|
15
|
+
if (['hives_list_buckets', 'hives_create_bucket', 'hives_get_bucket', 'hives_delete_bucket', 'hives_list_objects', 'hives_upload_object', 'hives_upload_object_by_key', 'hives_delete_object', 'hives_delete_object_by_key', 'hives_download_object_by_key', 'hives_get_cors_config', 'hives_set_cors_config', 'hives_generate_presigned_url', 'hives_reconcile'].includes(id))
|
|
16
|
+
return 100_000;
|
|
17
|
+
if (id === 'hives_cancel_deployment' || id === 'hives_delete_deployment')
|
|
18
|
+
return 330_000;
|
|
19
|
+
if (['hives_create_deployment', 'hives_upload_deployment', 'hives_invoke_deployment', 'hives_invoke_function', 'hives_invoke_function_async'].includes(id))
|
|
20
|
+
return 40_000;
|
|
21
|
+
if (['hives_create_function', 'hives_update_function', 'hives_delete_function'].includes(id))
|
|
22
|
+
return 100_000;
|
|
23
|
+
return 20_000;
|
|
24
|
+
}
|
|
25
|
+
const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
26
|
+
const templatePattern = (value) => new RegExp('^' + value.split(/(\{[^}]+\})/).map((part) => part.startsWith('{') ? '[^/]+' : escape(part)).join('') + '$');
|
|
27
|
+
/** Hives leaf transport. Caller fetch overrides stay authoritative; only native
|
|
28
|
+
* global fetch gets our Agent. Every operation retains an overall body deadline. */
|
|
29
|
+
export function createHivesNodeTransport(opts = {}, callerSignal) {
|
|
30
|
+
const supplied = opts.fetch ?? globalThis.fetch;
|
|
31
|
+
const underlying = supplied.bind(globalThis);
|
|
32
|
+
const useNative = supplied === globalThis.fetch;
|
|
33
|
+
const resolver = new Genesis(opts);
|
|
34
|
+
const routes = Object.entries(operationTable)
|
|
35
|
+
.filter(([, op]) => op.service === 'hives' && !op.internal)
|
|
36
|
+
.flatMap(([id, op]) => {
|
|
37
|
+
const direct = resolver.routing === 'direct';
|
|
38
|
+
let base;
|
|
39
|
+
try {
|
|
40
|
+
base = new URL(direct ? (opts.upstreams?.hives ?? op.upstream.base) : resolver.baseUrl);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error('Invalid Hives service address.');
|
|
44
|
+
}
|
|
45
|
+
const path = direct ? op.upstream.path : op.path;
|
|
46
|
+
const prefix = base.pathname.replace(/\/+$/, '');
|
|
47
|
+
const paths = [path];
|
|
48
|
+
if (!direct && path.startsWith('/v1/hives/'))
|
|
49
|
+
paths.push(path.replace('/v1/hives/', '/hives/v1/'));
|
|
50
|
+
return paths.map((p) => ({ id, origin: base.origin, method: direct ? op.upstream.method : op.method, pattern: templatePattern(prefix + p) }));
|
|
51
|
+
});
|
|
52
|
+
return (async (input, init = {}) => {
|
|
53
|
+
let url;
|
|
54
|
+
try {
|
|
55
|
+
url = new URL(input instanceof Request ? input.url : String(input));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
throw new Error('Invalid Hives request address.');
|
|
59
|
+
}
|
|
60
|
+
const method = (init.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();
|
|
61
|
+
const route = routes.find((entry) => entry.origin === url.origin && entry.method === method && entry.pattern.test(url.pathname));
|
|
62
|
+
if (!route)
|
|
63
|
+
return underlying(input, init);
|
|
64
|
+
const controller = new AbortController();
|
|
65
|
+
let message = 'Hives transport failed. Refresh the resource and retry.';
|
|
66
|
+
const safeError = () => new Error(message);
|
|
67
|
+
const signals = [...new Set([callerSignal, init.signal, input instanceof Request ? input.signal : undefined].filter((signal) => !!signal))];
|
|
68
|
+
const canceled = () => {
|
|
69
|
+
message = 'Hives request canceled before completion. Refresh the resource before retrying.';
|
|
70
|
+
controller.abort();
|
|
71
|
+
};
|
|
72
|
+
for (const signal of signals)
|
|
73
|
+
signal.addEventListener('abort', canceled, { once: true });
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
message = 'Hives request deadline exceeded. Refresh the resource before retrying.';
|
|
76
|
+
controller.abort();
|
|
77
|
+
}, hivesOperationBudget(route.id));
|
|
78
|
+
timer.unref();
|
|
79
|
+
let abortReject = () => { };
|
|
80
|
+
let reader;
|
|
81
|
+
let bodyController;
|
|
82
|
+
const aborted = () => {
|
|
83
|
+
abortReject(safeError());
|
|
84
|
+
bodyController?.error(safeError());
|
|
85
|
+
void reader?.cancel().catch(() => { });
|
|
86
|
+
cleanup();
|
|
87
|
+
};
|
|
88
|
+
controller.signal.addEventListener('abort', aborted, { once: true });
|
|
89
|
+
const abortPromise = new Promise((_, reject) => { abortReject = reject; });
|
|
90
|
+
void abortPromise.catch(() => { });
|
|
91
|
+
const cleanup = () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
for (const signal of signals)
|
|
94
|
+
signal.removeEventListener('abort', canceled);
|
|
95
|
+
controller.signal.removeEventListener('abort', aborted);
|
|
96
|
+
};
|
|
97
|
+
if (signals.some((signal) => signal.aborted))
|
|
98
|
+
canceled();
|
|
99
|
+
try {
|
|
100
|
+
if (controller.signal.aborted)
|
|
101
|
+
throw safeError();
|
|
102
|
+
const requestInit = {
|
|
103
|
+
...init, signal: controller.signal, cache: 'no-store',
|
|
104
|
+
redirect: init.redirect === 'manual' ? 'manual' : 'error',
|
|
105
|
+
...(useNative ? { dispatcher: nativeAgent() } : {}),
|
|
106
|
+
};
|
|
107
|
+
const pending = underlying(input, requestInit);
|
|
108
|
+
// A caller transport may ignore AbortSignal. Reject on our budget anyway,
|
|
109
|
+
// and discard any late response rather than presenting an acknowledgement.
|
|
110
|
+
void pending.then((response) => { if (controller.signal.aborted)
|
|
111
|
+
void response.body?.cancel().catch(() => { }); }, () => { });
|
|
112
|
+
const response = await Promise.race([pending, abortPromise]);
|
|
113
|
+
if (controller.signal.aborted)
|
|
114
|
+
throw safeError();
|
|
115
|
+
if (!response.body) {
|
|
116
|
+
cleanup();
|
|
117
|
+
return response;
|
|
118
|
+
}
|
|
119
|
+
reader = response.body.getReader();
|
|
120
|
+
const body = new ReadableStream({
|
|
121
|
+
start(value) { bodyController = value; },
|
|
122
|
+
async pull(value) {
|
|
123
|
+
try {
|
|
124
|
+
const next = await reader.read();
|
|
125
|
+
if (controller.signal.aborted)
|
|
126
|
+
return;
|
|
127
|
+
if (next.done) {
|
|
128
|
+
cleanup();
|
|
129
|
+
value.close();
|
|
130
|
+
}
|
|
131
|
+
else
|
|
132
|
+
value.enqueue(next.value);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
cleanup();
|
|
136
|
+
value.error(safeError());
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
async cancel() { cleanup(); await reader.cancel().catch(() => { }); },
|
|
140
|
+
});
|
|
141
|
+
return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
cleanup();
|
|
145
|
+
throw safeError();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/** Node opt-in for Hives calls. Other services retain the caller transport. */
|
|
150
|
+
export function createHivesNodeClient(opts = {}, callerSignal) {
|
|
151
|
+
return new Genesis({ ...opts, fetch: createHivesNodeTransport(opts, callerSignal) });
|
|
152
|
+
}
|