@onesi/genesis 0.2.4 → 0.2.6

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.4
6
+ npm install @onesi/genesis@0.2.6
7
7
  ```
8
8
 
9
9
  ```ts
package/dist/client.d.ts CHANGED
@@ -41,6 +41,18 @@ export declare class GenesisClient {
41
41
  constructor(opts?: GenesisOptions);
42
42
  static operations: Readonly<Record<OperationId, OperationMeta>>;
43
43
  static services: import("./types.js").ServiceMeta[];
44
+ /** This client's cached read of the selected context's Hives binding: kept HIVES_BINDING_TTL_MS, cleared on failure and on every link or detach. */
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`). The binding is cached for 60 s; a pr_… id that does not match a cached binding reads
50
+ * the selection again before it is refused, so each call makes at most one context read. Otherwise a GenesisError
51
+ * says how to find the prj_… id.
52
+ */
53
+ hivesProjectId(project_id?: string, init?: {
54
+ signal?: AbortSignal;
55
+ }): Promise<string>;
44
56
  /** Resolve the credential: explicit apiKey > token > env GENESIS_API_KEY > GENESIS_TOKEN > provider. */
45
57
  credential(): Promise<string | undefined>;
46
58
  /** Build the URL for an operation (exposed for tests and `--dry-run`). */
package/dist/client.js CHANGED
@@ -12,6 +12,7 @@ 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';
@@ -55,6 +56,8 @@ export function scrubError(e, depth = 0) {
55
56
  fresh.stack = stack;
56
57
  return fresh;
57
58
  }
59
+ /** How long a client reuses its read of the selected context's Hives binding. */
60
+ const HIVES_BINDING_TTL_MS = 60_000;
58
61
  export class GenesisClient {
59
62
  /** The configured gateway root, kept without userinfo so it is safe to log or print. */
60
63
  get baseUrl() { return stripUserinfo(this.root); }
@@ -75,6 +78,30 @@ export class GenesisClient {
75
78
  }
76
79
  static operations = operationTable;
77
80
  static services = services;
81
+ /** This client's cached read of the selected context's Hives binding: kept HIVES_BINDING_TTL_MS, cleared on failure and on every link or detach. */
82
+ hivesBinding;
83
+ /**
84
+ * The Hives project id (prj_…) for a Hives project argument. A prj_… id is returned unchanged. Omitted, or the
85
+ * Genesis pr_… id of the selected context's project, it resolves to the Hives project bound to that selection
86
+ * (`hives_get_context_project`). The binding is cached for 60 s; a pr_… id that does not match a cached binding reads
87
+ * the selection again before it is refused, so each call makes at most one context read. Otherwise a GenesisError
88
+ * says how to find the prj_… id.
89
+ */
90
+ async hivesProjectId(project_id, init = {}) {
91
+ let readHere = false;
92
+ const read = (fresh) => {
93
+ const cached = this.hivesBinding;
94
+ if (cached && !fresh && Date.now() - cached.at < HIVES_BINDING_TTL_MS)
95
+ return cached.read;
96
+ readHere = true;
97
+ const entry = { read: this.request('hives_get_context_project', {}, { signal: init.signal }), at: Date.now() };
98
+ this.hivesBinding = entry;
99
+ entry.read.catch(() => { if (this.hivesBinding === entry)
100
+ this.hivesBinding = undefined; });
101
+ return entry.read;
102
+ };
103
+ return resolveHivesProjectId(project_id, () => read(false), () => readHere ? undefined : read(true));
104
+ }
78
105
  /** Resolve the credential: explicit apiKey > token > env GENESIS_API_KEY > GENESIS_TOKEN > provider. */
79
106
  async credential() {
80
107
  return this.opts.apiKey ?? this.opts.token ?? env.GENESIS_API_KEY ?? env.GENESIS_TOKEN ?? (await this.opts.credentials?.());
@@ -184,6 +211,19 @@ export class GenesisClient {
184
211
  }
185
212
  /** Raw Response. Completed Hives guest responses retain their actual status, including non-2xx. */
186
213
  async raw(operationId, input = {}, init = {}) {
214
+ // A Genesis pr_… id where Hives wants its own prj_… id would only be a remote 404: refuse it before any request.
215
+ if (isGenesisProjectId(input.project_id) && isHivesProjectOperation(operationTable[operationId]))
216
+ throw hivesProjectIdRefusal(input.project_id, operationId);
217
+ // Linking or detaching the selection changes its Hives binding: drop the cached read before and after, whatever the outcome.
218
+ if (operationId === 'hives_set_context_project' || operationId === 'hives_detach_context_project') {
219
+ this.hivesBinding = undefined;
220
+ try {
221
+ return await this.dispatch(operationId, input, init);
222
+ }
223
+ finally {
224
+ this.hivesBinding = undefined;
225
+ }
226
+ }
187
227
  if (!isIamDirectoryOperation(operationId))
188
228
  return this.dispatch(operationId, input, init);
189
229
  // The gateway binds the organization context itself. A refusal is only explained here; the client never changes a session or its context.
@@ -226,7 +266,7 @@ export class GenesisClient {
226
266
  const url = this.url(operationId, input);
227
267
  const headers = {
228
268
  accept: 'application/json',
229
- 'user-agent': this.opts.userAgent ?? '@onesi/genesis/0.2.4',
269
+ 'user-agent': this.opts.userAgent ?? '@onesi/genesis/0.2.6',
230
270
  ...this.opts.headers, ...init.headers,
231
271
  };
232
272
  if (op.service === 'blinds' && !['GET', 'HEAD', 'OPTIONS'].includes(op.method))
package/dist/errors.js CHANGED
@@ -90,7 +90,7 @@ function backendCode(body) {
90
90
  && typeof body.error !== 'string' && !authorizationCode(body) ? body.code : undefined;
91
91
  return envelope !== undefined && !(own && /^UPSTREAM_\d{3}$/.test(envelope)) ? envelope : own;
92
92
  }
93
- const statusType = (s) => s === 401 ? 'unauthorized' : s === 403 ? 'forbidden' : s === 404 ? 'not_found' : s === 429 ? 'rate_limited' : s >= 500 ? 'upstream_error' : 'error';
93
+ const statusType = (s) => s === 401 ? 'unauthorized' : s === 403 ? 'forbidden' : s === 404 ? 'not_found' : s === 410 ? 'gone' : s === 429 ? 'rate_limited' : s >= 500 ? 'upstream_error' : 'error';
94
94
  /** The gateway catalog fields of an error, for JSON output: `code`, `required_scope` and `docs_url` when the gateway sent them. */
95
95
  export function errorCatalog(e) {
96
96
  const body = e.body && typeof e.body === 'object' ? e.body.error : 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
  },