@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.
@@ -0,0 +1,18 @@
1
+ import { GenesisError } from './errors.js';
2
+ import type { OperationMeta } from './types.js';
3
+ /** The one description of a Hives `project_id` (spec, CLI help and MCP schemas). */
4
+ export declare const HIVES_PROJECT_ID_DESCRIPTION = "Hives project id (prj_\u2026)";
5
+ /** Appended where an omitted project_id resolves from the selected Genesis context. */
6
+ export declare const HIVES_PROJECT_ID_DEFAULT = "omit it to use the Hives project bound to your selected Genesis context";
7
+ /** A Genesis project id (`pr_…`). A Hives project id is `prj_…`, so the two never collide. */
8
+ export declare const isGenesisProjectId: (value: unknown) => value is string;
9
+ /** Hives operations addressed by a Hives project in their path. */
10
+ export declare const isHivesProjectOperation: (op: OperationMeta | undefined) => boolean;
11
+ /** A Genesis pr_… id given where Hives wants its own prj_… id: refused before any request. */
12
+ export declare function hivesProjectIdRefusal(value: string, operationId?: string): GenesisError;
13
+ /**
14
+ * Map a Hives project argument to a Hives project id. A prj_… (or any non-Genesis) value is returned unchanged.
15
+ * Omitted, or a Genesis pr_… id that is the selected context's project, resolves to that selection's Hives binding
16
+ * read from `hives_get_context_project`; anything else is refused with how to find the prj_… id.
17
+ */
18
+ export declare function resolveHivesProjectId(value: unknown, readContext: () => Promise<unknown>): Promise<string>;
@@ -0,0 +1,51 @@
1
+ import { GenesisError } from './errors.js';
2
+ /** The one description of a Hives `project_id` (spec, CLI help and MCP schemas). */
3
+ export const HIVES_PROJECT_ID_DESCRIPTION = 'Hives project id (prj_…)';
4
+ /** Appended where an omitted project_id resolves from the selected Genesis context. */
5
+ export const HIVES_PROJECT_ID_DEFAULT = 'omit it to use the Hives project bound to your selected Genesis context';
6
+ /** A Genesis project id (`pr_…`). A Hives project id is `prj_…`, so the two never collide. */
7
+ export const isGenesisProjectId = (value) => typeof value === 'string' && /^pr_[A-Za-z0-9]{1,124}$/.test(value);
8
+ const HIVES_PROJECT = /^prj_[A-Za-z0-9]{1,124}$/;
9
+ /** Hives operations addressed by a Hives project in their path. */
10
+ export const isHivesProjectOperation = (op) => op?.service === 'hives' && op.pathParams.includes('project_id');
11
+ const FIND = 'Find the Hives project bound to your selected Genesis context with `genesis hives context get` (SDK: genesis.hives.resolveProject()), or list Hives projects with `genesis hives projects list`.';
12
+ /** A Genesis pr_… id given where Hives wants its own prj_… id: refused before any request. */
13
+ export function hivesProjectIdRefusal(value, operationId) {
14
+ return new GenesisError({ status: 0, type: 'invalid_request', code: 'HIVES_PROJECT_ID_REQUIRED', operationId,
15
+ message: `${value} is a Genesis project id. Hives operations take a Hives project id (prj_…). ${FIND}` });
16
+ }
17
+ const notBound = (message, from) => new GenesisError({ status: from?.status ?? 0, type: 'invalid_request', code: 'HIVES_PROJECT_NOT_BOUND',
18
+ message, requestId: from?.requestId, operationId: 'hives_get_context_project' });
19
+ /**
20
+ * Map a Hives project argument to a Hives project id. A prj_… (or any non-Genesis) value is returned unchanged.
21
+ * Omitted, or a Genesis pr_… id that is the selected context's project, resolves to that selection's Hives binding
22
+ * read from `hives_get_context_project`; anything else is refused with how to find the prj_… id.
23
+ */
24
+ export async function resolveHivesProjectId(value, readContext) {
25
+ if (value !== undefined && value !== null && value !== '' && !isGenesisProjectId(value)) {
26
+ if (typeof value !== 'string')
27
+ throw new GenesisError({ status: 0, type: 'invalid_request', code: 'HIVES_PROJECT_ID_REQUIRED', message: `project_id must be a ${HIVES_PROJECT_ID_DESCRIPTION}.` });
28
+ return value;
29
+ }
30
+ let context;
31
+ try {
32
+ context = await readContext();
33
+ }
34
+ catch (e) {
35
+ if (!(e instanceof GenesisError) || ![404, 409].includes(e.status))
36
+ throw e;
37
+ const named = isGenesisProjectId(value) ? `${value} is a Genesis project id and ` : '';
38
+ throw notBound(e.status === 404
39
+ ? `${named}No Hives project is bound to your selected Genesis context. Bind one with \`genesis hives context create <name>\` or \`genesis hives context link <prj_…>\`, or pass a Hives project id (prj_…) from \`genesis hives projects list\`.`
40
+ : `${named}No Genesis project is selected. Select an organization, workspace and project with \`genesis context select\`, or pass a Hives project id (prj_…) from \`genesis hives projects list\`.`, e);
41
+ }
42
+ const row = context && typeof context === 'object' ? context : {};
43
+ const selected = row.selectedContext?.projectId, bound = row.binding?.hivesProjectId;
44
+ if (typeof bound !== 'string' || !HIVES_PROJECT.test(bound) || typeof selected !== 'string') {
45
+ throw new GenesisError({ status: 0, type: 'invalid_response', operationId: 'hives_get_context_project', message: 'Hives returned an invalid context binding.' });
46
+ }
47
+ if (isGenesisProjectId(value) && value !== selected) {
48
+ throw notBound(`${value} is a Genesis project id, and it is not your selected Genesis project (${selected}, bound to Hives project ${bound}). Select it with \`genesis context select\`, or pass its Hives project id (prj_…) from \`genesis hives projects list\`.`);
49
+ }
50
+ return bound;
51
+ }
@@ -2,7 +2,7 @@ import { GenesisError } from './errors.js';
2
2
  import { operationTable } from './generated/operations.js';
3
3
  const REQUIRED = /^[\x21-\x7e]{8,128}$/;
4
4
  const OPTIONAL = /^[\x21-\x7e]{1,256}$/;
5
- /** Auto's Idempotency-Key grammar (l1felabs/auto src/operating.ts IDEMPOTENCY_KEY). */
5
+ /** Auto's Idempotency-Key grammar (l1felabs/auto src/operating.ts IDEMPOTENCY_KEY); Foundry feedback uses the same. */
6
6
  const AUTO_KEY = /^[A-Za-z0-9_.:-]{8,128}$/;
7
7
  /** The Idempotency-Key header an operation's spec declares. A required key is refused locally when missing
8
8
  * or malformed, before any request is sent; reuse the same key to replay instead of acting twice. */
@@ -25,6 +25,11 @@ export function idempotencyHeaders(operationId, key) {
25
25
  throw new GenesisError({ status: 0, type: 'invalid_request', operationId,
26
26
  message: `${operationId} idempotency key must be 8-128 characters of A-Z a-z 0-9 _ . : -` });
27
27
  }
28
+ // Foundry generation feedback takes [A-Za-z0-9_.:-] only (openrouter_generations.rs validate_feedback).
29
+ if (operationId === 'foundry_submitGenerationFeedback' && !AUTO_KEY.test(key)) {
30
+ throw new GenesisError({ status: 0, type: 'invalid_request', operationId,
31
+ message: `${operationId} idempotency key must be 8-128 characters of A-Z a-z 0-9 _ . : -` });
32
+ }
28
33
  return { 'idempotency-key': key };
29
34
  }
30
35
  /** The If-Match header an operation's spec requires (Auto's optimistic concurrency): the resource `version`
package/dist/index.d.ts CHANGED
@@ -4,13 +4,14 @@ export { iamDirectoryOperations, isIamDirectoryOperation, iamDirectoryFailure }
4
4
  export { iamModule } from './iam.js';
5
5
  export { GenesisError, errorCatalog, errorBody, errorToken, errorOutcome } from './errors.js';
6
6
  export { ERROR_BODY_LIMITS, cardNumber, redactErrorBody, redactErrorText, secretField } from './redact.js';
7
- export { requireSecureUrl } from './urls.js';
7
+ export { requireSecureUrl, redactUserinfo, stripUserinfo } from './urls.js';
8
8
  export { operationTable, services, type OperationId } from './generated/operations.js';
9
9
  export type { OperationInput, OperationOutput, OperationMeta, ServiceMeta } from './types.js';
10
10
  export type { paths, operations, components } from './generated/schema.js';
11
11
  export { hivesUploadBody, MAX_WASM_BYTES, type HivesUploadOptions } from './hives-upload.js';
12
12
  export { validateHivesInvocation, hivesFunctionResponseHeaders, HivesOperationError, MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_JSON_BYTES, type HivesInvokeRequest, type HivesInvokeOptions, type HivesOperationStatus } from './hives-functions.js';
13
13
  export { hivesResponseBytes } from './hives-response.js';
14
+ export { HIVES_PROJECT_ID_DESCRIPTION, HIVES_PROJECT_ID_DEFAULT, isGenesisProjectId, isHivesProjectOperation, hivesProjectIdRefusal } from './hives-project.js';
14
15
  export { runnerModule, type RunnerStartInput, type RunnerSnapshot, type RunnerCatalog, type RunnerEvent, type RunnerProviderResult, type RunnerRequestOptions } from './runner.js';
15
16
  export type { RunnerSelfExecutionGrantInput, RunnerSelfExecutionGrant } from './runner-grant.js';
16
17
  export { beaconsModule, BEACONS_RESOURCE_FAMILIES, BEACONS_CLIENT_PLATFORMS, type BeaconsResourceFamily } from './beacons.js';
package/dist/index.js CHANGED
@@ -4,11 +4,12 @@ export { iamDirectoryOperations, isIamDirectoryOperation, iamDirectoryFailure }
4
4
  export { iamModule } from './iam.js';
5
5
  export { GenesisError, errorCatalog, errorBody, errorToken, errorOutcome } from './errors.js';
6
6
  export { ERROR_BODY_LIMITS, cardNumber, redactErrorBody, redactErrorText, secretField } from './redact.js';
7
- export { requireSecureUrl } from './urls.js';
7
+ export { requireSecureUrl, redactUserinfo, stripUserinfo } from './urls.js';
8
8
  export { operationTable, services } from './generated/operations.js';
9
9
  export { hivesUploadBody, MAX_WASM_BYTES } from './hives-upload.js';
10
10
  export { validateHivesInvocation, hivesFunctionResponseHeaders, HivesOperationError, MAX_FUNCTION_BODY_BYTES, MAX_FUNCTION_JSON_BYTES } from './hives-functions.js';
11
11
  export { hivesResponseBytes } from './hives-response.js';
12
+ export { HIVES_PROJECT_ID_DESCRIPTION, HIVES_PROJECT_ID_DEFAULT, isGenesisProjectId, isHivesProjectOperation, hivesProjectIdRefusal } from './hives-project.js';
12
13
  export { runnerModule } from './runner.js';
13
14
  export { beaconsModule, BEACONS_RESOURCE_FAMILIES, BEACONS_CLIENT_PLATFORMS } from './beacons.js';
14
15
  export { rigsModule, panesModule, shellsModule } from './instances.js';
package/dist/redact.d.ts CHANGED
@@ -1,9 +1,3 @@
1
- /**
2
- * Redaction for service error bodies and messages before the CLI or MCP prints them. The design is
3
- * conservative: when a key or value might be a secret, it is replaced with `[redacted]`. Field names are
4
- * matched after normalisation (camelCase split, `-`/`.`/space to `_`, lower-cased), so `x-api-key`,
5
- * `privateKey`, `Set-Cookie` and `client_secret` are all the same kind of name.
6
- */
7
1
  export declare const REDACTED = "[redacted]";
8
2
  /** Bounds of the printed body: serialized bytes, per string, per array/object, nesting and the oversize preview. */
9
3
  export declare const ERROR_BODY_LIMITS: {
package/dist/redact.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * matched after normalisation (camelCase split, `-`/`.`/space to `_`, lower-cased), so `x-api-key`,
5
5
  * `privateKey`, `Set-Cookie` and `client_secret` are all the same kind of name.
6
6
  */
7
+ import { redactUserinfo } from './urls.js';
7
8
  export const REDACTED = '[redacted]';
8
9
  /** Bounds of the printed body: serialized bytes, per string, per array/object, nesting and the oversize preview. */
9
10
  export const ERROR_BODY_LIMITS = { bytes: 16384, string: 2048, items: 100, depth: 8, preview: 4096 };
@@ -174,8 +175,8 @@ const SECRET_TEXT = [
174
175
  [/(?<![A-Za-z0-9])ek_(?:live|test)_[A-Za-z0-9_-]{8,}/g, REDACTED],
175
176
  // PEM private keys (whole block, or to the end when cut).
176
177
  [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, REDACTED],
177
- // URL userinfo: scheme://user:pass@host.
178
- [/\b([a-z][a-z0-9+.-]{0,31}:\/\/)[^\s/?#@]+@/gi, `$1${REDACTED}@`],
178
+ // URL userinfo: scheme://user:pass@host, and the scheme-less, protocol-relative, percent-encoded and JSON-escaped forms.
179
+ [/^[\s\S]*$/, (t) => redactUserinfo(t, REDACTED)],
179
180
  // An Authorization / Proxy-Authorization header in text, any scheme.
180
181
  [/\b((?:proxy[-_ ]?)?authorization["']?\s*[:=]\s*["']?)(?!\[redacted\])(?:[A-Za-z][A-Za-z0-9_-]*\s+)?[^\s"',;]{8,}/gi, `$1${REDACTED}`],
181
182
  // Auth schemes followed by a credential.
package/dist/urls.d.ts CHANGED
@@ -4,3 +4,12 @@
4
4
  * Credentials ride on every request, so a plain-http endpoint off the local machine is refused.
5
5
  */
6
6
  export declare function requireSecureUrl(value: string, what: string): string;
7
+ /**
8
+ * Replace the userinfo of every URL in `text` with `mask` (default `***`): `https://user:pass@host` becomes
9
+ * `https://***@host` and `user:pass@host` becomes `***@host`, including scheme-less, protocol-relative,
10
+ * percent-encoded (also double-encoded) and JSON-escaped forms, and passwords holding an unencoded `@`.
11
+ * Linear time on any input. Use on error and status text only, never on data the user asked to see.
12
+ */
13
+ export declare function redactUserinfo(text: string, mask?: string): string;
14
+ /** `url` without its userinfo (`https://u:p@host/x` becomes `https://host/x`); text that is not an absolute URL passes through redactUserinfo. */
15
+ export declare function stripUserinfo(url: string): string;
package/dist/urls.js CHANGED
@@ -29,3 +29,86 @@ export function requireSecureUrl(value, what) {
29
29
  return value;
30
30
  throw new Error(`${what} must use https (http is allowed only on loopback hosts)`);
31
31
  }
32
+ // The scheme before the slashes is left as it is, so it is never matched. Every quantifier is bounded (userinfo 512, bare user 64 / password 128) and none is nested or overlaps
33
+ // another, so one start position costs O(1). On top of that, only the stretches within WINDOW characters before an
34
+ // `@` (or `%40`) are scanned at all: a userinfo cannot end anywhere else. Redaction is therefore linear in the text
35
+ // length times a constant, and text with no `@`, fullwidth `@` or `%40` is returned as it is.
36
+ // scheme://userinfo@, //userinfo@, https:/\userinfo@ and JSON-escaped :\/\/userinfo@ (greedy to the last @ before the path).
37
+ const USERINFO = /([/\\]{2}|\\\/\\\/)[^\s/?#\\"'<>]{0,512}[@@]/gi;
38
+ // The same, percent-encoded (also double-encoded): https%3A%2F%2Fuser%3Apass%40host, https%253A%252F%252Fuser%2540host.
39
+ const ENCODED_USERINFO = /(%(?:25){0,2}2F%(?:25){0,2}2F)(?:(?!%(?:25){0,2}2F)[^\s/?#&"'<>]){0,512}%(?:25){0,2}40/gi;
40
+ // Scheme-less user:pass@host (a colon is required, so plain addresses like user@host are left alone).
41
+ const BARE_USERINFO = /(^|[\s"'(<=,;[{])[^\s/?#@@:"'<>\\]{1,64}:[^\s/?#"'<>\\]{0,128}[@@](?=[\w[])/g;
42
+ /** Every place a userinfo can end: `@`, fullwidth `@`, or an encoded `%40` / `%2540`. */
43
+ const AT_HITS = /[@@]|%(?:25){0,2}40/gi;
44
+ /** The scheme-less form can only end at an `@` or `@` that is followed by a word character or `[` (see BARE_USERINFO). */
45
+ const BARE_HITS = /[@@](?=[\w[])/g;
46
+ /** The longest URL-form match (slashes and userinfo, encoded) is under 640 characters; the bare form is under 200. */
47
+ const URL_WINDOW = 700;
48
+ const BARE_WINDOW = 200;
49
+ /** With at least one hit per DENSE characters the windows would cover the text anyway, so it is scanned whole. The regexes are linear on their own; the windows only skip stretches that cannot hold a userinfo. */
50
+ const DENSE = 64;
51
+ /** Start and end of every hit of `re` in `text`, flattened; `null` when they are dense. */
52
+ function hitSpans(text, re) {
53
+ const limit = Math.floor(text.length / DENSE);
54
+ const spans = [];
55
+ re.lastIndex = 0;
56
+ for (let hit = re.exec(text); hit; hit = re.exec(text)) {
57
+ if (spans.length >= 2 * limit)
58
+ return null;
59
+ spans.push(hit.index, hit.index + hit[0].length);
60
+ }
61
+ return spans;
62
+ }
63
+ /**
64
+ * Apply `fn` to the stretches of `text` within `window` characters before a hit, and copy the rest as it is.
65
+ * `spans` of `null` means dense: `fn` runs over the whole text. A stretch without the character `needs` cannot hold a match.
66
+ */
67
+ function overHits(text, spans, window, fn, needs = '') {
68
+ if (spans === null)
69
+ return fn(text);
70
+ if (spans.length === 0)
71
+ return text;
72
+ let out = '';
73
+ let copied = 0;
74
+ let start = Math.max(0, spans[0] - window);
75
+ let end = spans[1] + 1;
76
+ const flush = () => {
77
+ const seg = text.slice(start, end);
78
+ // A sentinel before a window that does not start the text keeps `^` (start of text) from matching mid-text.
79
+ const piece = needs && !seg.includes(needs) ? seg : start > 0 ? fn(`x${seg}`).slice(1) : fn(seg);
80
+ out += text.slice(copied, start) + piece;
81
+ copied = Math.min(end, text.length);
82
+ };
83
+ for (let i = 2; i < spans.length; i += 2) {
84
+ const s = Math.max(0, spans[i] - window);
85
+ if (s <= end) {
86
+ end = spans[i + 1] + 1;
87
+ continue;
88
+ }
89
+ flush();
90
+ start = s;
91
+ end = spans[i + 1] + 1;
92
+ }
93
+ flush();
94
+ return out + text.slice(copied);
95
+ }
96
+ /**
97
+ * Replace the userinfo of every URL in `text` with `mask` (default `***`): `https://user:pass@host` becomes
98
+ * `https://***@host` and `user:pass@host` becomes `***@host`, including scheme-less, protocol-relative,
99
+ * percent-encoded (also double-encoded) and JSON-escaped forms, and passwords holding an unencoded `@`.
100
+ * Linear time on any input. Use on error and status text only, never on data the user asked to see.
101
+ */
102
+ export function redactUserinfo(text, mask = '***') {
103
+ // Pre-check: no `@`, `@` or `%40` means no userinfo in any form.
104
+ if (!/[@@]|%(?:25){0,2}40/i.test(text))
105
+ return text;
106
+ const urlForms = overHits(text, hitSpans(text, AT_HITS), URL_WINDOW, (t) => t.replace(ENCODED_USERINFO, `$1${mask}%40`).replace(USERINFO, `$1${mask}@`));
107
+ if (!urlForms.includes(':'))
108
+ return urlForms; // the scheme-less form needs its colon
109
+ return overHits(urlForms, hitSpans(urlForms, BARE_HITS), BARE_WINDOW, (t) => t.replace(BARE_USERINFO, `$1${mask}@`), ':');
110
+ }
111
+ /** `url` without its userinfo (`https://u:p@host/x` becomes `https://host/x`); text that is not an absolute URL passes through redactUserinfo. */
112
+ export function stripUserinfo(url) {
113
+ return /^[a-z][a-z0-9+.-]*:\/\//i.test(url) ? url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/?#]*@/i, '$1') : redactUserinfo(url);
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onesi/genesis",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/l1felabs/genesis-api.git",