@onesi/genesis 0.2.3 → 0.2.5

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  The TypeScript SDK for OneOS. One typed client for every Genesis service, through the Genesis API at `https://os.one.ml/api`.
4
4
 
5
5
  ```sh
6
- npm install @onesi/genesis@0.2.3
6
+ npm install @onesi/genesis@0.2.5
7
7
  ```
8
8
 
9
9
  ```ts
package/dist/boxes.d.ts CHANGED
@@ -3,7 +3,7 @@ import type { OperationInput as In } from './types.js';
3
3
  /** Boxes (mail, docs, files, sheets, slides, calendar, identity, MCP): one method per operation, grouped as the
4
4
  * Boxes resource tree. Path parameters come first, then the body, then query options, then an optional
5
5
  * idempotency key on the operations that take one. Operations that stream or download bytes return the raw Response;
6
- * uploads take a FormData body. GET needs boxes:read, everything else boxes:write. Generated from the Boxes spec. */
6
+ * uploads take a FormData body. GET needs boxes:read, everything else boxes:write; mail domain administration needs boxes:admin. Generated from the Boxes spec. */
7
7
  export declare function boxesModule(client: GenesisClient): {
8
8
  mail: {
9
9
  addresses: {
package/dist/boxes.js CHANGED
@@ -2,7 +2,7 @@ import { idempotencyHeaders } from './idempotency.js';
2
2
  /** Boxes (mail, docs, files, sheets, slides, calendar, identity, MCP): one method per operation, grouped as the
3
3
  * Boxes resource tree. Path parameters come first, then the body, then query options, then an optional
4
4
  * idempotency key on the operations that take one. Operations that stream or download bytes return the raw Response;
5
- * uploads take a FormData body. GET needs boxes:read, everything else boxes:write. Generated from the Boxes spec. */
5
+ * uploads take a FormData body. GET needs boxes:read, everything else boxes:write; mail domain administration needs boxes:admin. Generated from the Boxes spec. */
6
6
  export function boxesModule(client) {
7
7
  return {
8
8
  mail: {
package/dist/client.d.ts CHANGED
@@ -25,8 +25,15 @@ export interface GenesisOptions {
25
25
  headers?: Record<string, string>;
26
26
  userAgent?: string;
27
27
  }
28
+ /**
29
+ * Redact URL userinfo from an error's message, stack and cause chain (bounded). If the error is frozen or any of
30
+ * those properties cannot be rewritten, return a new sanitized Error instead of the original.
31
+ */
32
+ export declare function scrubError(e: unknown, depth?: number): unknown;
28
33
  export declare class GenesisClient {
29
- readonly baseUrl: string;
34
+ /** The configured gateway root, kept without userinfo so it is safe to log or print. */
35
+ get baseUrl(): string;
36
+ private readonly root;
30
37
  readonly routing: 'gateway' | 'direct';
31
38
  private readonly opts;
32
39
  private readonly fetchImpl;
@@ -34,6 +41,16 @@ export declare class GenesisClient {
34
41
  constructor(opts?: GenesisOptions);
35
42
  static operations: Readonly<Record<OperationId, OperationMeta>>;
36
43
  static services: import("./types.js").ServiceMeta[];
44
+ /** This client's one read of the selected context's Hives binding (cleared on failure). */
45
+ private hivesBinding?;
46
+ /**
47
+ * The Hives project id (prj_…) for a Hives project argument. A prj_… id is returned unchanged. Omitted, or the
48
+ * Genesis pr_… id of the selected context's project, it resolves to the Hives project bound to that selection
49
+ * (`hives_get_context_project`, read at most once per client); otherwise a GenesisError says how to find the prj_… id.
50
+ */
51
+ hivesProjectId(project_id?: string, init?: {
52
+ signal?: AbortSignal;
53
+ }): Promise<string>;
37
54
  /** Resolve the credential: explicit apiKey > token > env GENESIS_API_KEY > GENESIS_TOKEN > provider. */
38
55
  credential(): Promise<string | undefined>;
39
56
  /** Build the URL for an operation (exposed for tests and `--dry-run`). */
package/dist/client.js CHANGED
@@ -8,10 +8,11 @@ import { blindsOperationKey, blindsOwnerTransport, isBlindsOwnerOperation } from
8
8
  import { isHivesObjectUpload, hivesObjectBody, hivesContentType, isHivesResourceOperation, resourceError, resourceErrorFromResponse } from './hives-resources-transport.js';
9
9
  import { operationTable, services } from './generated/operations.js';
10
10
  import { GenesisError } from './errors.js';
11
- import { requireSecureUrl } from './urls.js';
11
+ import { requireSecureUrl, redactUserinfo, stripUserinfo } from './urls.js';
12
12
  import { iamDirectoryFailure, isIamDirectoryOperation } from './iam-directory.js';
13
13
  import { validateHivesInvocation, hivesFunctionResponseHeaders, hivesFunctionMetadata, hivesOperationError } from './hives-functions.js';
14
14
  import { validateHivesEnvironment, validateHivesEnvironmentNames, hivesEnvironmentSetNames, hivesEnvironmentMetadata, hivesEnvironmentReveal } from './hives-environment.js';
15
+ import { hivesProjectIdRefusal, isGenesisProjectId, isHivesProjectOperation, resolveHivesProjectId } from './hives-project.js';
15
16
  import { RUNNER_SELF_GRANT_OPERATION, snapshotRunnerSelfGrant, runnerGrantResponse, runnerGrantTransportError } from './runner-grant.js';
16
17
  /** The Genesis API gateway. `https://console.life.one/api` remains an alias for the same gateway. */
17
18
  export const DEFAULT_BASE_URL = 'https://os.one.ml/api';
@@ -21,23 +22,75 @@ const ENDLESS_STREAMS = {
21
22
  hives_stream_logs: 'genesis.hives.logs.stream() or genesis.hives.logs.window()',
22
23
  hives_stream_deployment_logs: 'hivesSseWindow(await genesis.raw(...))',
23
24
  };
25
+ const SCRUB_DEPTH = 5;
26
+ /**
27
+ * Redact URL userinfo from an error's message, stack and cause chain (bounded). If the error is frozen or any of
28
+ * those properties cannot be rewritten, return a new sanitized Error instead of the original.
29
+ */
30
+ export function scrubError(e, depth = 0) {
31
+ if (typeof e === 'string')
32
+ return redactUserinfo(e);
33
+ if (!(e instanceof Error))
34
+ return e;
35
+ if (depth >= SCRUB_DEPTH)
36
+ return undefined;
37
+ const message = redactUserinfo(String(e.message));
38
+ const stack = typeof e.stack === 'string' ? redactUserinfo(e.stack) : e.stack;
39
+ const hasCause = 'cause' in e;
40
+ const cause = hasCause ? scrubError(e.cause, depth + 1) : undefined;
41
+ if (message === e.message && stack === e.stack && cause === e.cause)
42
+ return e;
43
+ try {
44
+ if (Object.isFrozen(e))
45
+ throw e;
46
+ Object.defineProperty(e, 'message', { value: message, writable: true, configurable: true, enumerable: false });
47
+ Object.defineProperty(e, 'stack', { value: stack, writable: true, configurable: true, enumerable: false });
48
+ if (hasCause)
49
+ Object.defineProperty(e, 'cause', { value: cause, writable: true, configurable: true, enumerable: false });
50
+ if (e.message === message && e.stack === stack && (!hasCause || e.cause === cause))
51
+ return e;
52
+ }
53
+ catch { /* fall through to a fresh error */ }
54
+ const fresh = new (e instanceof TypeError ? TypeError : Error)(message, hasCause ? { cause } : undefined);
55
+ fresh.name = redactUserinfo(String(e.name));
56
+ fresh.stack = stack;
57
+ return fresh;
58
+ }
24
59
  export class GenesisClient {
25
- baseUrl;
60
+ /** The configured gateway root, kept without userinfo so it is safe to log or print. */
61
+ get baseUrl() { return stripUserinfo(this.root); }
62
+ root;
26
63
  routing;
27
64
  opts;
28
65
  fetchImpl;
29
66
  get contextFetch() { return this.fetchImpl; }
30
67
  constructor(opts = {}) {
31
68
  this.opts = opts;
32
- this.baseUrl = requireSecureUrl(opts.baseUrl ?? env.GENESIS_BASE_URL ?? DEFAULT_BASE_URL, 'baseUrl').replace(/\/+$/, '');
69
+ this.root = requireSecureUrl(opts.baseUrl ?? env.GENESIS_BASE_URL ?? DEFAULT_BASE_URL, 'baseUrl').replace(/\/+$/, '');
33
70
  this.routing = opts.routing ?? (env.GENESIS_ROUTING === 'direct' ? 'direct' : 'gateway');
34
71
  const f = opts.fetch ?? globalThis.fetch;
35
72
  if (!f)
36
73
  throw new Error('No fetch available; pass options.fetch');
37
- this.fetchImpl = f.bind(globalThis);
74
+ // fetch errors can quote the request URL (and its userinfo); scrub them before they surface.
75
+ this.fetchImpl = ((input, init) => f.call(globalThis, input, init).catch((e) => { throw scrubError(e); }));
38
76
  }
39
77
  static operations = operationTable;
40
78
  static services = services;
79
+ /** This client's one read of the selected context's Hives binding (cleared on failure). */
80
+ hivesBinding;
81
+ /**
82
+ * The Hives project id (prj_…) for a Hives project argument. A prj_… id is returned unchanged. Omitted, or the
83
+ * Genesis pr_… id of the selected context's project, it resolves to the Hives project bound to that selection
84
+ * (`hives_get_context_project`, read at most once per client); otherwise a GenesisError says how to find the prj_… id.
85
+ */
86
+ async hivesProjectId(project_id, init = {}) {
87
+ return resolveHivesProjectId(project_id, () => {
88
+ const read = this.hivesBinding ??= this.request('hives_get_context_project', {}, { signal: init.signal });
89
+ read.catch(() => { if (this.hivesBinding === read)
90
+ this.hivesBinding = undefined; });
91
+ return read;
92
+ });
93
+ }
41
94
  /** Resolve the credential: explicit apiKey > token > env GENESIS_API_KEY > GENESIS_TOKEN > provider. */
42
95
  async credential() {
43
96
  return this.opts.apiKey ?? this.opts.token ?? env.GENESIS_API_KEY ?? env.GENESIS_TOKEN ?? (await this.opts.credentials?.());
@@ -68,7 +121,7 @@ export class GenesisClient {
68
121
  p = fill(op.upstream.path);
69
122
  }
70
123
  else {
71
- base = this.baseUrl;
124
+ base = this.root;
72
125
  p = fill(op.path);
73
126
  }
74
127
  const qs = new URLSearchParams();
@@ -147,6 +200,9 @@ export class GenesisClient {
147
200
  }
148
201
  /** Raw Response. Completed Hives guest responses retain their actual status, including non-2xx. */
149
202
  async raw(operationId, input = {}, init = {}) {
203
+ // A Genesis pr_… id where Hives wants its own prj_… id would only be a remote 404: refuse it before any request.
204
+ if (isGenesisProjectId(input.project_id) && isHivesProjectOperation(operationTable[operationId]))
205
+ throw hivesProjectIdRefusal(input.project_id, operationId);
150
206
  if (!isIamDirectoryOperation(operationId))
151
207
  return this.dispatch(operationId, input, init);
152
208
  // The gateway binds the organization context itself. A refusal is only explained here; the client never changes a session or its context.
@@ -189,7 +245,7 @@ export class GenesisClient {
189
245
  const url = this.url(operationId, input);
190
246
  const headers = {
191
247
  accept: 'application/json',
192
- 'user-agent': this.opts.userAgent ?? '@onesi/genesis/0.2.3',
248
+ 'user-agent': this.opts.userAgent ?? '@onesi/genesis/0.2.5',
193
249
  ...this.opts.headers, ...init.headers,
194
250
  };
195
251
  if (op.service === 'blinds' && !['GET', 'HEAD', 'OPTIONS'].includes(op.method))
package/dist/errors.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /** An error body is read to at most this many bytes, before any parsing or redaction; the rest is dropped. */
2
+ export declare const ERROR_BODY_READ_LIMIT: number;
3
+ /** Read at most `limit` bytes of a response body as text, cancelling the stream once the limit is reached. */
4
+ export declare function readCappedText(res: Response, limit?: number): Promise<string>;
1
5
  /** Normalized error for every Genesis service (gateway `{error:{type,message}}`, IAM `{error,error_description}`, Relays `{error}`, Foundry/OpenAI shape). */
2
6
  export declare class GenesisError extends Error {
3
7
  readonly status: number;
package/dist/errors.js CHANGED
@@ -1,4 +1,33 @@
1
1
  import { authorizationCode, redactErrorBody } from './redact.js';
2
+ /** An error body is read to at most this many bytes, before any parsing or redaction; the rest is dropped. */
3
+ export const ERROR_BODY_READ_LIMIT = 64 * 1024;
4
+ /** Read at most `limit` bytes of a response body as text, cancelling the stream once the limit is reached. */
5
+ export async function readCappedText(res, limit = ERROR_BODY_READ_LIMIT) {
6
+ const reader = res.body?.getReader();
7
+ if (!reader)
8
+ return (await res.text()).slice(0, limit);
9
+ const decoder = new TextDecoder();
10
+ let text = '';
11
+ let bytes = 0;
12
+ try {
13
+ for (;;) {
14
+ const { done, value } = await reader.read();
15
+ if (done)
16
+ break;
17
+ const room = limit - bytes;
18
+ text += decoder.decode(value.byteLength > room ? value.subarray(0, room) : value, { stream: true });
19
+ bytes += Math.min(value.byteLength, room);
20
+ if (bytes >= limit) {
21
+ void reader.cancel().catch(() => { });
22
+ break;
23
+ }
24
+ }
25
+ return text + decoder.decode();
26
+ }
27
+ finally {
28
+ reader.releaseLock?.();
29
+ }
30
+ }
2
31
  /** Normalized error for every Genesis service (gateway `{error:{type,message}}`, IAM `{error,error_description}`, Relays `{error}`, Foundry/OpenAI shape). */
3
32
  export class GenesisError extends Error {
4
33
  status;
@@ -16,7 +45,7 @@ export class GenesisError extends Error {
16
45
  this.body = init.body;
17
46
  }
18
47
  static async fromResponse(res, operationId) {
19
- const text = await res.text().catch(() => '');
48
+ const text = await readCappedText(res).catch(() => '');
20
49
  let body = text;
21
50
  try {
22
51
  body = text ? JSON.parse(text) : undefined;
package/dist/fleets.d.ts CHANGED
@@ -141,7 +141,10 @@ export declare function fleetsModule(client: GenesisClient): {
141
141
  model: string;
142
142
  updated_at?: number;
143
143
  }>;
144
- set: (api_key: string) => Promise<{
144
+ /** `model` (optional) must be a model Foundry lists for the key; omit it for the Fleets default. */
145
+ set: (api_key: string, options?: {
146
+ model?: string;
147
+ }) => Promise<{
145
148
  connected: boolean;
146
149
  model: string;
147
150
  updated_at?: number;
package/dist/fleets.js CHANGED
@@ -26,6 +26,11 @@ function validateKey(apiKey) {
26
26
  throw invalid('fleets_setConnection', 'Foundry key must contain 1–4096 UTF-8 bytes without CR, LF or NUL.');
27
27
  }
28
28
  }
29
+ function validateModel(model) {
30
+ if (typeof model !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(model)) {
31
+ throw invalid('fleets_setConnection', 'Model must be a Foundry model id of 1–128 characters (letters, digits, . _ : / -).');
32
+ }
33
+ }
29
34
  const instanceId = (value, operation) => {
30
35
  if (typeof value !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value)) {
31
36
  throw invalid(operation, 'Invalid Fleets instance ID.');
@@ -160,9 +165,12 @@ export function fleetsModule(client) {
160
165
  },
161
166
  connection: {
162
167
  get: () => client.request('fleets_getConnection', {}),
163
- set: async (api_key) => {
168
+ /** `model` (optional) must be a model Foundry lists for the key; omit it for the Fleets default. */
169
+ set: async (api_key, options = {}) => {
164
170
  validateKey(api_key);
165
- return client.request('fleets_setConnection', { body: { api_key } });
171
+ if (options.model !== undefined)
172
+ validateModel(options.model);
173
+ return client.request('fleets_setConnection', { body: { api_key, ...(options.model !== undefined ? { model: options.model } : {}) } });
166
174
  },
167
175
  delete: () => client.request('fleets_deleteConnection', {}),
168
176
  },
package/dist/foundry.d.ts CHANGED
@@ -266,7 +266,8 @@ export declare function foundryModule(client: GenesisClient): {
266
266
  [key: string]: unknown;
267
267
  }>;
268
268
  };
269
- /** OpenRouter-style generation lookup by request id; content and feedback need Foundry `admin` (refused today). */
269
+ /** OpenRouter-style generation lookup by request id, retained content and feedback for your own generations (not billed).
270
+ * Feedback requires an idempotency key (8-128 characters of A-Z a-z 0-9 _ . : -); reuse it to retry. */
270
271
  generation: {
271
272
  get: (id: string) => Promise<{
272
273
  data: {
@@ -286,7 +287,7 @@ export declare function foundryModule(client: GenesisClient): {
286
287
  content: (id: string) => Promise<{
287
288
  [key: string]: unknown;
288
289
  }>;
289
- feedback: (body: In<"foundry_submitGenerationFeedback">["body"]) => Promise<{
290
+ feedback: (body: In<"foundry_submitGenerationFeedback">["body"], idempotencyKey: string) => Promise<{
290
291
  [key: string]: unknown;
291
292
  }>;
292
293
  };
package/dist/foundry.js CHANGED
@@ -85,11 +85,12 @@ export function foundryModule(client) {
85
85
  status: (workload_id) => client.request('foundry_getWorkloadStatus', { workload_id }),
86
86
  terminate: (workload_id) => client.request('foundry_terminateWorkload', { workload_id }),
87
87
  },
88
- /** OpenRouter-style generation lookup by request id; content and feedback need Foundry `admin` (refused today). */
88
+ /** OpenRouter-style generation lookup by request id, retained content and feedback for your own generations (not billed).
89
+ * Feedback requires an idempotency key (8-128 characters of A-Z a-z 0-9 _ . : -); reuse it to retry. */
89
90
  generation: {
90
91
  get: (id) => client.request('foundry_getGeneration', { id }),
91
92
  content: (id) => client.request('foundry_getGenerationContent', { id }),
92
- feedback: (body) => client.request('foundry_submitGenerationFeedback', { body }),
93
+ feedback: async (body, idempotencyKey) => client.request('foundry_submitGenerationFeedback', { body }, { headers: idempotencyHeaders('foundry_submitGenerationFeedback', idempotencyKey) }),
93
94
  },
94
95
  batches: {
95
96
  create: (body) => client.request('foundry_createBatch', { body }),