@k9o/core 1.2.0 → 1.3.0
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/CHANGELOG.md +45 -0
- package/README.md +168 -17
- package/dist/{chunk-O2ZTV6YN.js → chunk-KC4EDMBF.js} +61 -3
- package/dist/chunk-KC4EDMBF.js.map +1 -0
- package/dist/{chunk-2NDFTHUB.js → chunk-UL53GNYM.js} +23 -5
- package/dist/chunk-UL53GNYM.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +2 -1
- package/dist/index.d.ts +563 -517
- package/dist/index.js +170 -421
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/url-grammar.d.ts +32 -16
- package/dist/url-grammar.js +5 -1
- package/package.json +4 -4
- package/src/audit.ts +16 -26
- package/src/compute/llms.ts +55 -33
- package/src/functions.ts +60 -27
- package/src/http.ts +5 -1
- package/src/index.ts +16 -17
- package/src/me.ts +69 -85
- package/src/migrations.ts +61 -11
- package/src/org.ts +138 -140
- package/src/orgs.ts +16 -3
- package/src/policies.ts +63 -24
- package/src/projects.ts +58 -22
- package/src/realtime.ts +21 -200
- package/src/region.ts +83 -3
- package/src/storage-buckets.ts +17 -5
- package/src/studio/workflows.ts +8 -8
- package/src/types.ts +3 -3
- package/src/url-grammar.ts +52 -25
- package/dist/chunk-2NDFTHUB.js.map +0 -1
- package/dist/chunk-O2ZTV6YN.js.map +0 -1
- package/src/metadata.ts +0 -130
package/src/studio/workflows.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Workflows substrate. Mirrors
|
|
5
5
|
* `_sys.workflows` + `_sys.workflow_runs` row shapes.
|
|
6
6
|
*
|
|
7
|
-
* Wire mapping (project-plane
|
|
7
|
+
* Wire mapping (project-plane):
|
|
8
8
|
* - list() → GET /@<slug>/workflows
|
|
9
9
|
* - create(workflow) → POST /@<slug>/workflows
|
|
10
10
|
* - read(id) → GET /@<slug>/workflows/<wfl-id>
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* - runs.read(id, runId) → GET /@<slug>/workflows/<wfl-id>/runs/<wfr-id>
|
|
16
16
|
* - runs.cancel(id, runId) → POST /@<slug>/workflows/<wfl-id>/runs/<wfr-id>/cancel
|
|
17
17
|
*
|
|
18
|
-
*
|
|
18
|
+
* RBAC: create/update/delete/trigger/cancel require
|
|
19
19
|
* PROJECT_AUTHOR; list/read/runs read against all cohort roles subject
|
|
20
|
-
* to RLS.
|
|
20
|
+
* to RLS. Surfaces 6 K9O_WORKFLOW_* error codes (see
|
|
21
21
|
* `WORKFLOW_ERROR_CODES`); callers narrow on `error.code`.
|
|
22
22
|
*/
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ import { projectPath } from "../url-grammar.ts";
|
|
|
26
26
|
import type { Envelope } from "../envelope.ts";
|
|
27
27
|
|
|
28
28
|
// ----------------------------------------------------------------------
|
|
29
|
-
// Enums (closed
|
|
29
|
+
// Enums (closed set)
|
|
30
30
|
// ----------------------------------------------------------------------
|
|
31
31
|
|
|
32
32
|
/** 5-trigger catalogue. */
|
|
@@ -73,7 +73,7 @@ export type WorkflowAction = {
|
|
|
73
73
|
// Resource shapes (mirror _sys.workflows + _sys.workflow_runs rows)
|
|
74
74
|
// ----------------------------------------------------------------------
|
|
75
75
|
|
|
76
|
-
/** Workflow record
|
|
76
|
+
/** Workflow record. */
|
|
77
77
|
export type Workflow = {
|
|
78
78
|
/** `wfl_<base62>`. */
|
|
79
79
|
id: string;
|
|
@@ -106,7 +106,7 @@ export type WorkflowRunStep = {
|
|
|
106
106
|
costSubtotal?: number;
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
/** Workflow run record
|
|
109
|
+
/** Workflow run record. */
|
|
110
110
|
export type WorkflowRun = {
|
|
111
111
|
/** `wfr_<base62>`. */
|
|
112
112
|
id: string;
|
|
@@ -251,14 +251,14 @@ export function makeWorkflowsNamespace(config: HttpClientConfig) {
|
|
|
251
251
|
/**
|
|
252
252
|
* POST /@<slug>/workflows/<wfl-id>/runs — manual-trigger a run.
|
|
253
253
|
* Valid only for `triggerType: 'manual'` workflows.
|
|
254
|
-
* Requires PROJECT_AUTHOR. Returns the materialised WorkflowRun.
|
|
254
|
+
* Requires PROJECT_AUTHOR. The argument is sent as `{ triggerContext }`. Returns the materialised WorkflowRun.
|
|
255
255
|
*/
|
|
256
256
|
async trigger(workflowId: string, triggerContext: Record<string, unknown> = {}): Promise<Envelope<WorkflowRun>> {
|
|
257
257
|
const slug = requireProjectSlug(config);
|
|
258
258
|
return request(config, {
|
|
259
259
|
method: "POST",
|
|
260
260
|
path: projectPath(slug, `workflows/${encodeURIComponent(workflowId)}/runs`),
|
|
261
|
-
body: triggerContext,
|
|
261
|
+
body: { triggerContext },
|
|
262
262
|
});
|
|
263
263
|
},
|
|
264
264
|
|
package/src/types.ts
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* canonical org subdomain `https://<orgSlug>.api.k9o.io`. Pass an explicit host to override — an explicit `<orgSlug>` subdomain,
|
|
29
29
|
* or a self-hosted / Sovereign-Control host, or a dev host — used as-is.
|
|
30
30
|
* - `fetch` — custom fetch implementation (defaults to global)
|
|
31
|
-
* - `retryConfig` — retry/backoff config
|
|
31
|
+
* - `retryConfig` — retry/backoff config for idempotent requests
|
|
32
32
|
* - `defaultHeaders` — headers merged into every request
|
|
33
33
|
*/
|
|
34
34
|
export type CreateClientOptions = {
|
|
@@ -42,8 +42,8 @@ export type CreateClientOptions = {
|
|
|
42
42
|
};
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Retry config.
|
|
46
|
-
*
|
|
45
|
+
* Retry config. Only GET, HEAD, OPTIONS and DELETE are retried, on network failure and on
|
|
46
|
+
* HTTP 429, 502, 503 and 504.
|
|
47
47
|
*/
|
|
48
48
|
export type RetryConfig = {
|
|
49
49
|
maxRetries: number;
|
package/src/url-grammar.ts
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* URL grammar.
|
|
3
3
|
*
|
|
4
4
|
* Canonical URL shapes:
|
|
5
|
-
* - Member-plane (project-scoped): `<orgSlug>.api
|
|
6
|
-
* - Org-plane (org-scoped): `<orgSlug>.api
|
|
7
|
-
* - Platform-plane (cross-org): `api
|
|
5
|
+
* - Member-plane (project-scoped): `<orgSlug>.api.<apex>/@<projectSlug>/<sub-resource>/...`
|
|
6
|
+
* - Org-plane (org-scoped): `<orgSlug>.api.<apex>/<resource>/...`
|
|
7
|
+
* - Platform-plane (cross-org): `api.<apex>/discovery/...` + `api.<apex>/auth/...` etc.
|
|
8
|
+
*
|
|
9
|
+
* `<apex>` is whatever the deployment uses (public cloud, internal, air-gapped, localhost);
|
|
10
|
+
* the SDK takes the host from the baseUrl it is given and never matches it against a list.
|
|
8
11
|
*
|
|
9
12
|
* The `baseUrl` passed to `createClient` is the gateway host root —
|
|
10
|
-
* scheme + host (+ optional port)
|
|
13
|
+
* scheme + host (+ optional port) Path-bearing baseUrls
|
|
11
14
|
* are rejected with `K9O_INVALID_BASE_URL`.
|
|
12
15
|
*
|
|
13
16
|
* The SDK constructs URLs by composing `baseUrl + path-from-builder`.
|
|
@@ -79,35 +82,59 @@ export function validateBaseUrl(baseUrl: string): string {
|
|
|
79
82
|
return normalised;
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
/** The convenience default when the caller passes no baseUrl. A default value only: never used to parse a host. */
|
|
86
|
+
export const DEFAULT_BASE_URL = "https://api.k9o.io";
|
|
87
|
+
|
|
88
|
+
/** The structure of a gateway API host, read from the host itself. */
|
|
89
|
+
export type ApiHost = {
|
|
90
|
+
/** First label of an org host (`acme` in `acme.api.corp.internal`); null for a platform host (`api.corp.internal`). */
|
|
91
|
+
orgSlug: string | null;
|
|
92
|
+
/** `api` or `api-staging`: the label that marks a gateway host. */
|
|
93
|
+
family: "api" | "api-staging";
|
|
94
|
+
/** 4-character region code when the host is regional (`api-de01`), else null (region-free). */
|
|
95
|
+
region: string | null;
|
|
96
|
+
/** Everything after the gateway label (`corp.internal`, `k9o.io`): the apex, never matched against a list. */
|
|
97
|
+
apex: string;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const API_LABEL = /^(api|api-staging)(?:-([a-z]{2}\d{2}))?$/;
|
|
101
|
+
|
|
82
102
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* both). A THIRD apex here needs the same confirmation before joining this
|
|
88
|
-
* set — never inferred from a hostname merely looking apex-shaped.
|
|
103
|
+
* Read the org slug and region out of a gateway host without knowing its apex. The gateway label is `api` or
|
|
104
|
+
* `api-staging`, optionally followed by `-<rgn>`; the label before it is the org slug and whatever follows it is the
|
|
105
|
+
* apex. Returns null for any other shape (localhost, an IP, a single-label host, a host with no gateway label), which
|
|
106
|
+
* callers use exactly as given.
|
|
89
107
|
*/
|
|
90
|
-
|
|
108
|
+
export function parseApiHost(host: string): ApiHost | null {
|
|
109
|
+
const labels = host.toLowerCase().split(".");
|
|
110
|
+
if (labels.some((l) => l.length === 0)) return null;
|
|
111
|
+
const mk = (gatewayLabel: string, orgSlug: string | null, rest: string[]): ApiHost | null => {
|
|
112
|
+
const m = API_LABEL.exec(gatewayLabel);
|
|
113
|
+
if (m === null || rest.length === 0) return null;
|
|
114
|
+
return { orgSlug, family: m[1] as "api" | "api-staging", region: m[2] ?? null, apex: rest.join(".") };
|
|
115
|
+
};
|
|
116
|
+
// Org host first: `<org>.<api[-rgn]>.<apex>`.
|
|
117
|
+
if (labels.length >= 3 && SLUG_RE.test(labels[0]!) && !labels[0]!.endsWith("-") && API_LABEL.test(labels[1]!)) {
|
|
118
|
+
return mk(labels[1]!, labels[0]!, labels.slice(2));
|
|
119
|
+
}
|
|
120
|
+
// Platform host: `api.<apex>`.
|
|
121
|
+
if (labels.length >= 2 && API_LABEL.test(labels[0]!)) return mk(labels[0]!, null, labels.slice(1));
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
91
124
|
|
|
92
125
|
/**
|
|
93
|
-
* Resolve the effective baseUrl. `baseUrl` is
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* `https://<orgSlug>.<apex>`. An explicit `<orgSlug>` subdomain, a
|
|
98
|
-
* self-hosted / Sovereign host, or a dev host (anything not in that set)
|
|
99
|
-
* passes through unchanged — `orgSlug` is intentionally NOT validated or
|
|
100
|
-
* composed onto a non-apex host (fixed 2026-08-30: this used to silently
|
|
101
|
-
* ignore `orgSlug` for `api-staging.k9o.io` too, not just genuine
|
|
102
|
-
* self-hosted/custom hosts, since only the literal production apex string
|
|
103
|
-
* was checked).
|
|
126
|
+
* Resolve the effective baseUrl. `baseUrl` is OPTIONAL: omitted, it defaults to {@link DEFAULT_BASE_URL} (the public
|
|
127
|
+
* cloud, a convenience only). When the (validated) host is a platform host (`api.<apex>` or `api-staging.<apex>`, any
|
|
128
|
+
* apex) and `orgSlug` is supplied, the canonical org host `<orgSlug>.<host>` is composed. Any other host (an org host
|
|
129
|
+
* already, an internal or air-gapped host, localhost) passes through unchanged; `orgSlug` is then only sent as a header.
|
|
104
130
|
*/
|
|
105
131
|
export function resolveBaseUrl(baseUrl: string | undefined, orgSlug?: string): string {
|
|
106
|
-
const validated = validateBaseUrl(baseUrl ??
|
|
132
|
+
const validated = validateBaseUrl(baseUrl ?? DEFAULT_BASE_URL);
|
|
107
133
|
const parsed = new URL(validated);
|
|
108
|
-
|
|
134
|
+
const host = parseApiHost(parsed.hostname);
|
|
135
|
+
if (host !== null && host.orgSlug === null && host.region === null && orgSlug) {
|
|
109
136
|
validateSlug(orgSlug, "org");
|
|
110
|
-
return `${parsed.protocol}//${orgSlug}.${parsed.
|
|
137
|
+
return `${parsed.protocol}//${orgSlug}.${parsed.host}`;
|
|
111
138
|
}
|
|
112
139
|
return validated;
|
|
113
140
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/url-grammar.ts"],"sourcesContent":["/**\n * URL grammar.\n *\n * Canonical URL shapes:\n * - Member-plane (project-scoped): `<orgSlug>.api.k9o.io/@<projectSlug>/<sub-resource>/...`\n * - Org-plane (org-scoped): `<orgSlug>.api.k9o.io/<resource>/...`\n * - Platform-plane (cross-org): `api.k9o.io/discovery/...` + `api.k9o.io/auth/...` etc.\n *\n * The `baseUrl` passed to `createClient` is the gateway host root —\n * scheme + host (+ optional port). Path-bearing baseUrls\n * are rejected with `K9O_INVALID_BASE_URL`.\n *\n * The SDK constructs URLs by composing `baseUrl + path-from-builder`.\n * Per-resource SDK methods use the builders below to assemble their\n * `path` argument before handing to the HTTP client.\n */\n\nimport { KError } from \"./kerror.ts\";\n\n/**\n * Validate a `baseUrl`. Returns the normalised form\n * (trailing-slash stripped) on success; throws `K9O_INVALID_BASE_URL`\n * on any violation.\n *\n * Accepted shapes:\n * - `https://api.k9o.io`\n * - `https://acme.api.k9o.io`\n * - `http://localhost:8080`\n * - `https://api.k9o.io:443`\n *\n * Rejected shapes:\n * - path segments (`https://api.k9o.io/v2`)\n * - missing scheme (`api.k9o.io`)\n * - non-http(s) scheme (`wss://api.k9o.io`)\n * - query / fragment (`https://api.k9o.io?x=1`)\n */\nexport function validateBaseUrl(baseUrl: string): string {\n let parsed: URL;\n try {\n parsed = new URL(baseUrl);\n } catch {\n throw new KError({\n code: \"K9O_INVALID_BASE_URL\",\n message: `baseUrl \"${baseUrl}\" is not a valid URL`,\n context: { baseUrl },\n recommendation: \"Pass a scheme + host like `https://api.k9o.io` or `https://<orgSlug>.api.k9o.io`\",\n status: null,\n });\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new KError({\n code: \"K9O_INVALID_BASE_URL\",\n message: `baseUrl \"${baseUrl}\" uses unsupported scheme \"${parsed.protocol}\"; expected http: or https:`,\n context: { baseUrl, scheme: parsed.protocol },\n recommendation: \"Use http:// or https://. WebSocket subscriptions are wired internally over SSE on the same host.\",\n status: null,\n });\n }\n if (parsed.pathname !== \"/\" && parsed.pathname !== \"\") {\n throw new KError({\n code: \"K9O_INVALID_BASE_URL\",\n message: `baseUrl \"${baseUrl}\" must not contain a path segment; got \"${parsed.pathname}\"`,\n context: { baseUrl, path: parsed.pathname },\n recommendation: \"Strip the path segment — pass host only. URL paths are composed by the SDK.\",\n status: null,\n });\n }\n if (parsed.search.length > 0 || parsed.hash.length > 0) {\n throw new KError({\n code: \"K9O_INVALID_BASE_URL\",\n message: `baseUrl \"${baseUrl}\" must not contain query or fragment`,\n context: { baseUrl, query: parsed.search, fragment: parsed.hash },\n recommendation: \"Pass host only.\",\n status: null,\n });\n }\n // Normalise — strip trailing slash so concatenation is predictable.\n const normalised = `${parsed.protocol}//${parsed.host}`;\n return normalised;\n}\n\n/**\n * The bare platform apex hosts org-subdomain derivation applies to. `api.k9o.io`\n * (production) and `api-staging.k9o.io` (staging) share the same\n * `<orgSlug>.<apex>` wire-routing scheme — confirmed live (conformance,\n * `k9o login` examples, staging demo configs all address orgs this way on\n * both). A THIRD apex here needs the same confirmation before joining this\n * set — never inferred from a hostname merely looking apex-shaped.\n */\nconst K9O_ORG_DERIVABLE_APEXES = new Set([\"api.k9o.io\", \"api-staging.k9o.io\"]);\n\n/**\n * Resolve the effective baseUrl. `baseUrl` is\n * OPTIONAL — it defaults to the platform apex `https://api.k9o.io`. When the\n * (validated) host is a bare apex in `K9O_ORG_DERIVABLE_APEXES` and `orgSlug`\n * is supplied, the SDK composes the canonical org-subdomain wire root\n * `https://<orgSlug>.<apex>`. An explicit `<orgSlug>` subdomain, a\n * self-hosted / Sovereign host, or a dev host (anything not in that set)\n * passes through unchanged — `orgSlug` is intentionally NOT validated or\n * composed onto a non-apex host (fixed 2026-08-30: this used to silently\n * ignore `orgSlug` for `api-staging.k9o.io` too, not just genuine\n * self-hosted/custom hosts, since only the literal production apex string\n * was checked).\n */\nexport function resolveBaseUrl(baseUrl: string | undefined, orgSlug?: string): string {\n const validated = validateBaseUrl(baseUrl ?? \"https://api.k9o.io\");\n const parsed = new URL(validated);\n if (K9O_ORG_DERIVABLE_APEXES.has(parsed.hostname) && orgSlug) {\n validateSlug(orgSlug, \"org\");\n return `${parsed.protocol}//${orgSlug}.${parsed.hostname}`;\n }\n return validated;\n}\n\n/**\n * Slug format validator:\n * `[a-z][a-z0-9-]{1,62}` and must not end with `-`.\n */\nconst SLUG_RE = /^[a-z][a-z0-9-]{1,62}$/;\n\nexport function validateSlug(slug: string, kind: \"org\" | \"project\"): void {\n if (!SLUG_RE.test(slug) || slug.endsWith(\"-\")) {\n throw new KError({\n code: \"K9O_INVALID_FIELD_VALUE\",\n message: `${kind}Slug \"${slug}\" does not match required format ^[a-z][a-z0-9-]{1,62}$ (must not end with '-')`,\n context: { slug, kind },\n recommendation: `Pass a slug matching the format — lowercase letters / digits / hyphens; 2-63 chars; starts with a letter; does not end with hyphen.`,\n status: null,\n });\n }\n}\n\n/**\n * Build a project-scoped (member-plane) URL path per the URL grammar\n * E.g. `projectPath('member-portal', 'collections/posts/documents')`\n * → `/@member-portal/collections/posts/documents`.\n *\n * The caller is responsible for normalising sub-resource components\n * (URL-encoding goes through the caller's path construction; this\n * helper handles only the prefix + slug shape).\n */\nexport function projectPath(projectSlug: string, subResource: string): string {\n validateSlug(projectSlug, \"project\");\n const cleaned = subResource.startsWith(\"/\") ? subResource.slice(1) : subResource;\n return `/@${projectSlug}/${cleaned}`;\n}\n\n/**\n * Build an org-plane path. E.g. `orgPath('org')` → `/org`,\n * `orgPath('projects')` → `/projects`.\n */\nexport function orgPath(resource: string): string {\n const cleaned = resource.startsWith(\"/\") ? resource.slice(1) : resource;\n return `/${cleaned}`;\n}\n\n/**\n * Build a platform-plane path (cross-org). E.g.\n * `platformPath('discovery/projects')` → `/discovery/projects`,\n * `platformPath('auth/sign-in')` → `/auth/sign-in`.\n *\n * Composes with the platform baseUrl (`https://api.k9o.io`) — not the\n * org-subdomain baseUrl.\n */\nexport function platformPath(resource: string): string {\n const cleaned = resource.startsWith(\"/\") ? resource.slice(1) : resource;\n return `/${cleaned}`;\n}\n"],"mappings":";;;;;AAoCO,SAAS,gBAAgB,SAAyB;AACvD,MAAI;AACJ,MAAI;AACF,aAAS,IAAI,IAAI,OAAO;AAAA,EAC1B,QAAQ;AACN,UAAM,IAAI,OAAO;AAAA,MACf,MAAM;AAAA,MACN,SAAS,YAAY,OAAO;AAAA,MAC5B,SAAS,EAAE,QAAQ;AAAA,MACnB,gBAAgB;AAAA,MAChB,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACA,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,UAAU;AAC/D,UAAM,IAAI,OAAO;AAAA,MACf,MAAM;AAAA,MACN,SAAS,YAAY,OAAO,8BAA8B,OAAO,QAAQ;AAAA,MACzE,SAAS,EAAE,SAAS,QAAQ,OAAO,SAAS;AAAA,MAC5C,gBAAgB;AAAA,MAChB,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACA,MAAI,OAAO,aAAa,OAAO,OAAO,aAAa,IAAI;AACrD,UAAM,IAAI,OAAO;AAAA,MACf,MAAM;AAAA,MACN,SAAS,YAAY,OAAO,2CAA2C,OAAO,QAAQ;AAAA,MACtF,SAAS,EAAE,SAAS,MAAM,OAAO,SAAS;AAAA,MAC1C,gBAAgB;AAAA,MAChB,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACA,MAAI,OAAO,OAAO,SAAS,KAAK,OAAO,KAAK,SAAS,GAAG;AACtD,UAAM,IAAI,OAAO;AAAA,MACf,MAAM;AAAA,MACN,SAAS,YAAY,OAAO;AAAA,MAC5B,SAAS,EAAE,SAAS,OAAO,OAAO,QAAQ,UAAU,OAAO,KAAK;AAAA,MAChE,gBAAgB;AAAA,MAChB,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAEA,QAAM,aAAa,GAAG,OAAO,QAAQ,KAAK,OAAO,IAAI;AACrD,SAAO;AACT;AAUA,IAAM,2BAA2B,oBAAI,IAAI,CAAC,cAAc,oBAAoB,CAAC;AAetE,SAAS,eAAe,SAA6B,SAA0B;AACpF,QAAM,YAAY,gBAAgB,WAAW,oBAAoB;AACjE,QAAM,SAAS,IAAI,IAAI,SAAS;AAChC,MAAI,yBAAyB,IAAI,OAAO,QAAQ,KAAK,SAAS;AAC5D,iBAAa,SAAS,KAAK;AAC3B,WAAO,GAAG,OAAO,QAAQ,KAAK,OAAO,IAAI,OAAO,QAAQ;AAAA,EAC1D;AACA,SAAO;AACT;AAMA,IAAM,UAAU;AAET,SAAS,aAAa,MAAc,MAA+B;AACxE,MAAI,CAAC,QAAQ,KAAK,IAAI,KAAK,KAAK,SAAS,GAAG,GAAG;AAC7C,UAAM,IAAI,OAAO;AAAA,MACf,MAAM;AAAA,MACN,SAAS,GAAG,IAAI,SAAS,IAAI;AAAA,MAC7B,SAAS,EAAE,MAAM,KAAK;AAAA,MACtB,gBAAgB;AAAA,MAChB,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACF;AAWO,SAAS,YAAY,aAAqB,aAA6B;AAC5E,eAAa,aAAa,SAAS;AACnC,QAAM,UAAU,YAAY,WAAW,GAAG,IAAI,YAAY,MAAM,CAAC,IAAI;AACrE,SAAO,KAAK,WAAW,IAAI,OAAO;AACpC;AAMO,SAAS,QAAQ,UAA0B;AAChD,QAAM,UAAU,SAAS,WAAW,GAAG,IAAI,SAAS,MAAM,CAAC,IAAI;AAC/D,SAAO,IAAI,OAAO;AACpB;AAUO,SAAS,aAAa,UAA0B;AACrD,QAAM,UAAU,SAAS,WAAW,GAAG,IAAI,SAAS,MAAM,CAAC,IAAI;AAC/D,SAAO,IAAI,OAAO;AACpB;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/retry.ts","../src/region.ts","../src/http.ts"],"sourcesContent":["/**\n * Retry + backoff for the HTTP client.\n *\n * Configured via `retryConfig: { maxRetries, backoff }`. Retries are\n * scoped to transient failures only — never retry on KError-carrying\n * non-2xx responses (those are semantic; the caller must handle them).\n *\n * Retryable conditions (transient; safe to retry idempotently):\n * - Transport error (`K9O_UPSTREAM_UNAVAILABLE` from `request()`) — network down,\n * DNS fail, connection refused, etc.\n * - HTTP 502 / 503 / 504 (gateway-up but downstream unavailable)\n * - HTTP 429 with `Retry-After` header (rate-limit; backoff by header value)\n *\n * Non-retryable (semantic; caller must handle):\n * - HTTP 4xx other than 429 (caller bug; retry won't help)\n * - HTTP 5xx other than 502/503/504 (gateway-side bug; surface immediately)\n * - KError on success-shape (rare; malformed envelope)\n *\n * Retry method discipline: only GET / HEAD / OPTIONS / DELETE are\n * retried by default — these are RFC-7231 idempotent. POST / PATCH /\n * PUT are NOT retried (caller can opt in via `idempotencyKey` header\n * when they assert idempotency).\n *\n * Backoff:\n * - `exponential` (default) — `initialDelayMs * 2^attempt`, capped at maxDelayMs; jittered ±25%\n * - `linear` — `initialDelayMs * (attempt + 1)`, capped at maxDelayMs; jittered ±25%\n * - `none` — no delay between attempts\n */\n\nimport { KError } from \"./kerror.ts\";\nimport type { RetryConfig } from \"./types.ts\";\n\nconst RETRYABLE_METHODS = new Set([\"GET\", \"HEAD\", \"OPTIONS\", \"DELETE\"]);\nconst RETRYABLE_STATUS = new Set([502, 503, 504]);\n\nexport const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {\n maxRetries: 3,\n backoff: \"exponential\",\n initialDelayMs: 100,\n maxDelayMs: 5000,\n};\n\n/**\n * Decide if a thrown error is retryable.\n * Only KError instances are inspected; other thrown values propagate\n * (programmer error / unrecoverable state).\n */\nexport function isRetryable(error: unknown, method: string): boolean {\n if (!RETRYABLE_METHODS.has(method.toUpperCase())) return false;\n if (!(error instanceof KError)) return false;\n if (error.code === \"K9O_UPSTREAM_UNAVAILABLE\") return true;\n if (error.status !== null && RETRYABLE_STATUS.has(error.status)) return true;\n if (error.status === 429) return true; // rate-limit; respect Retry-After if surfaced in context\n return false;\n}\n\n/**\n * Compute the backoff delay for a given attempt index (0-based).\n */\nexport function computeDelay(config: Required<RetryConfig>, attempt: number): number {\n let base: number;\n if (config.backoff === \"none\") return 0;\n if (config.backoff === \"linear\") base = config.initialDelayMs * (attempt + 1);\n else base = config.initialDelayMs * Math.pow(2, attempt);\n const capped = Math.min(base, config.maxDelayMs);\n // ±25% jitter\n const jitter = capped * 0.25 * (Math.random() * 2 - 1);\n return Math.max(0, Math.floor(capped + jitter));\n}\n\n/**\n * Sleep helper. Pulled out so tests can stub it via injection.\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/**\n * Execute a function with retry. The function is called up to\n * `maxRetries + 1` times; non-retryable failures throw immediately.\n *\n * @param fn - The function to execute (typically the HTTP request)\n * @param method - HTTP method (used to gate retry per RETRYABLE_METHODS)\n * @param config - Retry config; defaults applied if partial\n * @param sleepImpl - Sleep implementation (tests inject a no-op or fast variant)\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n method: string,\n config: RetryConfig | undefined,\n sleepImpl: (ms: number) => Promise<void> = sleep,\n): Promise<T> {\n const c: Required<RetryConfig> = {\n ...DEFAULT_RETRY_CONFIG,\n ...(config ?? {}),\n };\n let lastError: unknown;\n for (let attempt = 0; attempt <= c.maxRetries; attempt++) {\n try {\n return await fn();\n } catch (e) {\n lastError = e;\n if (attempt === c.maxRetries) break;\n if (!isRetryable(e, method)) break;\n const delay = computeDelay(c, attempt);\n if (delay > 0) await sleepImpl(delay);\n }\n }\n throw lastError;\n}\n","/**\n * Regional host resolution lives in `@k9o/gate` (which `@k9o/core` composes), so the Gate client and the core client\n * share one implementation of the probe and of the `Location` validation. See `packages/gate/src/region.ts`.\n */\nexport { resolveRegionalOrigin, regionalOriginFromLocation, type RegionConfig } from \"@k9o/gate\";\n","/**\n * HTTP client abstraction for `@k9o/core`.\n *\n * Lifted + generalised from `@k9o/gate/client.ts` (the auth-pillar SDK's\n * proven pattern). This client is per-instance — `createClient` constructs\n * one bound to its config; per-call options merge into the bound defaults.\n *\n * Responsibilities:\n * - URL construction against bound `baseUrl` + per-call `path` + query\n * - Envelope unwrap on 2xx (rejects malformed envelopes as INTERNAL_ERROR)\n * - KError rehydration on non-2xx\n * - Bearer token injection (from per-call override OR bound default)\n * - Project context header (`x-k9o-project-slug`) injection when bound\n * - Retry with exponential backoff per RetryConfig (see retry.ts) — not available\n * yet; requests are currently a single attempt\n *\n * The client is intentionally minimal — per-resource SDK methods compose\n * the path + body + bearer themselves and hand to `request()`. This keeps\n * the HTTP layer dumb and the per-resource logic auditable.\n */\n\nimport { KError, kerrorFromWire, type KErrorWire } from \"./kerror.ts\";\nimport type { Envelope } from \"./envelope.ts\";\nimport { withRetry } from \"./retry.ts\";\nimport type { RetryConfig } from \"./types.ts\";\nimport { resolveRegionalOrigin } from \"./region.ts\";\n\n/**\n * Per-instance HTTP client config. Constructed by `createClient`; passed\n * to `request()` per call.\n */\nexport type HttpClientConfig = {\n /** Normalised baseUrl (scheme + host); set via createClient. */\n baseUrl: string;\n /** Default bearer token (per-call override wins). */\n apiKey?: string | undefined;\n /** Active project slug — injected as `x-k9o-project-slug` header on every request. */\n projectSlug?: string | undefined;\n /** Active org slug — injected as `x-k9o-org-slug` header on every request. */\n orgSlug?: string | undefined;\n /** Custom fetch impl (defaults to globalThis.fetch); use for tests + non-standard runtimes. */\n fetch?: typeof globalThis.fetch | undefined;\n /** Default headers merged into every request. */\n defaultHeaders?: Record<string, string> | undefined;\n /** Retry config; idempotent methods only (GET/HEAD/OPTIONS/DELETE) — applied when transport fails or upstream returns 502/503/504/429. */\n retryConfig?: RetryConfig | undefined;\n};\n\n/**\n * Per-call request options.\n */\nexport type RequestInit = {\n method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n path: string;\n body?: unknown;\n /** Per-call bearer override (wins over config.apiKey). */\n bearerToken?: string | undefined;\n /** Per-call additional headers (merged with config.defaultHeaders + auth). */\n headers?: Record<string, string> | undefined;\n /** Per-call query params. */\n query?: Record<string, string | number | boolean | undefined> | undefined;\n};\n\n/**\n * Issue a request against the gateway. Returns the unwrapped Envelope\n * on 2xx; throws KError on non-2xx, transport failures, or malformed\n * responses.\n */\nexport async function request<T>(\n config: HttpClientConfig,\n init: RequestInit,\n): Promise<Envelope<T>> {\n return withRetry(() => requestOnce<T>(config, init), init.method, config.retryConfig);\n}\n\n/**\n * Single-attempt request implementation. `request` wraps this in\n * retry-with-backoff per `config.retryConfig`. Exported for tests that\n * want to bypass retry semantics.\n */\nexport async function requestOnce<T>(\n config: HttpClientConfig,\n init: RequestInit,\n): Promise<Envelope<T>> {\n const fetchImpl = config.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new KError({\n code: \"K9O_INTERNAL_ERROR\",\n message:\n \"No fetch implementation available — pass {fetch} in createClient options for runtimes without a global fetch\",\n context: {},\n recommendation: \"Use Node 18+, Bun 1.1+, or a modern browser; otherwise supply a fetch polyfill via the fetch option.\",\n });\n }\n\n const url = buildUrl(await resolveRegionalOrigin(config), init.path, init.query);\n const headers: Record<string, string> = {\n accept: \"application/json\",\n ...(config.defaultHeaders ?? {}),\n ...(config.projectSlug ? { \"x-k9o-project-slug\": config.projectSlug } : {}),\n ...(config.orgSlug ? { \"x-k9o-org-slug\": config.orgSlug } : {}),\n ...(init.headers ?? {}),\n };\n const bearer = init.bearerToken ?? config.apiKey;\n if (typeof bearer === \"string\" && bearer.length > 0) {\n headers[\"authorization\"] = `Bearer ${bearer}`;\n }\n const requestInit: Record<string, unknown> = {\n method: init.method,\n headers,\n };\n if (init.body !== undefined) {\n if (typeof FormData !== \"undefined\" && init.body instanceof FormData) {\n // Multipart: the runtime sets the content type (with its boundary); never JSON-encode it.\n requestInit[\"body\"] = init.body;\n } else {\n headers[\"content-type\"] = \"application/json\";\n requestInit[\"body\"] = JSON.stringify(init.body);\n }\n }\n\n let response: Response;\n try {\n response = await fetchImpl(url, requestInit as Parameters<typeof fetchImpl>[1]);\n } catch (e) {\n throw new KError({\n code: \"K9O_UPSTREAM_UNAVAILABLE\",\n message: `Network error reaching ${url}: ${e instanceof Error ? e.message : String(e)}`,\n context: { url },\n recommendation: \"Check connectivity to the gateway; retry with backoff if transient.\",\n });\n }\n\n // 204 No Content — synthesize an empty envelope (some DELETE / PUT endpoints).\n if (response.status === 204) {\n return { data: undefined as unknown as T, meta: {} };\n }\n\n let parsed: unknown;\n try {\n parsed = await response.json();\n } catch (e) {\n throw new KError({\n code: \"K9O_INTERNAL_ERROR\",\n message: `Failed to parse JSON response from ${url}: ${e instanceof Error ? e.message : String(e)}`,\n context: { url, status: response.status },\n status: response.status,\n });\n }\n\n if (response.status >= 200 && response.status < 300) {\n if (!isEnvelope<T>(parsed)) {\n throw new KError({\n code: \"K9O_INTERNAL_ERROR\",\n message: `Malformed envelope from ${url}: expected {data, meta}`,\n context: { url, status: response.status },\n status: response.status,\n });\n }\n return parsed;\n }\n\n if (isKErrorWire(parsed)) {\n throw kerrorFromWire(parsed, response.status);\n }\n throw new KError({\n code: \"K9O_INTERNAL_ERROR\",\n message: `Upstream returned ${response.status} with non-KError body`,\n context: { url, status: response.status, body: parsed },\n status: response.status,\n });\n}\n\n/**\n * Open a streaming request against the gateway. Returns the raw\n * `Response` on 2xx (caller consumes `response.body` as an SSE / chunked\n * stream); throws `KError` on non-2xx, transport failures, or a missing\n * body. Used by streaming surfaces — `client.compute.llms.invokeStream`\n * + `client.realtime.subscribe` SSE transport.\n *\n * Unlike `request()`, this does NOT unwrap the JSON envelope or apply\n * retry — streaming responses are consumed incrementally and retry on a\n * half-consumed stream is unsafe.\n */\nexport async function requestStream(\n config: HttpClientConfig,\n init: RequestInit & { accept?: string },\n): Promise<Response> {\n const fetchImpl = config.fetch ?? globalThis.fetch;\n if (typeof fetchImpl !== \"function\") {\n throw new KError({\n code: \"K9O_INTERNAL_ERROR\",\n message: \"No fetch implementation available — pass {fetch} in createClient options\",\n context: {},\n recommendation: \"Use Node 18+, Bun 1.1+, or a modern browser; otherwise supply a fetch polyfill via the fetch option.\",\n });\n }\n\n const url = buildUrl(await resolveRegionalOrigin(config), init.path, init.query);\n const headers: Record<string, string> = {\n accept: init.accept ?? \"text/event-stream\",\n ...(config.defaultHeaders ?? {}),\n ...(config.projectSlug ? { \"x-k9o-project-slug\": config.projectSlug } : {}),\n ...(config.orgSlug ? { \"x-k9o-org-slug\": config.orgSlug } : {}),\n ...(init.headers ?? {}),\n };\n const bearer = init.bearerToken ?? config.apiKey;\n if (typeof bearer === \"string\" && bearer.length > 0) {\n headers[\"authorization\"] = `Bearer ${bearer}`;\n }\n const requestInit: Record<string, unknown> = { method: init.method, headers };\n if (init.body !== undefined) {\n headers[\"content-type\"] = \"application/json\";\n requestInit[\"body\"] = JSON.stringify(init.body);\n }\n\n let response: Response;\n try {\n response = await fetchImpl(url, requestInit as Parameters<typeof fetchImpl>[1]);\n } catch (e) {\n throw new KError({\n code: \"K9O_UPSTREAM_UNAVAILABLE\",\n message: `Network error reaching ${url}: ${e instanceof Error ? e.message : String(e)}`,\n context: { url },\n recommendation: \"Check connectivity to the gateway; retry with backoff if transient.\",\n });\n }\n\n if (response.status < 200 || response.status >= 300) {\n let parsed: unknown = null;\n try {\n parsed = await response.json();\n } catch {\n /* non-JSON error body */\n }\n if (isKErrorWire(parsed)) throw kerrorFromWire(parsed, response.status);\n throw new KError({\n code: \"K9O_INTERNAL_ERROR\",\n message: `Upstream returned ${response.status} opening stream`,\n context: { url, status: response.status, body: parsed },\n status: response.status,\n });\n }\n\n if (response.body === null) {\n throw new KError({\n code: \"K9O_INTERNAL_ERROR\",\n message: `Stream response from ${url} has no body`,\n context: { url, status: response.status },\n status: response.status,\n });\n }\n\n return response;\n}\n\nfunction buildUrl(\n baseUrl: string,\n path: string,\n query: RequestInit[\"query\"],\n): string {\n const trimmedBase = baseUrl.endsWith(\"/\") ? baseUrl.slice(0, -1) : baseUrl;\n const trimmedPath = path.startsWith(\"/\") ? path : `/${path}`;\n let url = `${trimmedBase}${trimmedPath}`;\n if (query !== undefined) {\n const params = new URLSearchParams();\n for (const [k, v] of Object.entries(query)) {\n if (v !== undefined) params.set(k, String(v));\n }\n const qs = params.toString();\n if (qs.length > 0) url += `?${qs}`;\n }\n return url;\n}\n\nfunction isEnvelope<T>(value: unknown): value is Envelope<T> {\n if (value === null || typeof value !== \"object\") return false;\n const v = value as { data?: unknown; meta?: unknown };\n return \"data\" in v && \"meta\" in v && typeof v.meta === \"object\" && v.meta !== null;\n}\n\nfunction isKErrorWire(value: unknown): value is KErrorWire {\n if (value === null || typeof value !== \"object\") return false;\n const v = value as { code?: unknown; message?: unknown };\n return typeof v.code === \"string\" && typeof v.message === \"string\";\n}\n"],"mappings":"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","names":[]}
|
package/src/metadata.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `client.metadata.*` — cross-surface metadata query (`k9o.metadata.*`).
|
|
3
|
-
*
|
|
4
|
-
* Client-side surface for `POST /@:projectSlug/metadata/query`. The wire shape is fixed here ahead of the
|
|
5
|
-
* server route: until the gateway ships that route, calling `.execute()` returns a 404 rather than a result.
|
|
6
|
-
* Once it is available, only `telemetry:*` surfaces return data at first; collection and storage metadata
|
|
7
|
-
* follow as their server-side schemas land.
|
|
8
|
-
*
|
|
9
|
-
* `textSearch`/`vectorSearch` are explicitly a later increment (Inc 5) — not implemented
|
|
10
|
-
* here; adding stub methods for an unspecified future mode would be speculative, not spec-grounded.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { request, type HttpClientConfig } from "./http.ts";
|
|
14
|
-
import { projectPath } from "./url-grammar.ts";
|
|
15
|
-
import { KError } from "./kerror.ts";
|
|
16
|
-
import type { Envelope } from "./envelope.ts";
|
|
17
|
-
|
|
18
|
-
/** The three cross-surface slices. */
|
|
19
|
-
export type MetadataSlice = "collection" | "storage" | "telemetry";
|
|
20
|
-
|
|
21
|
-
/** operator vocabulary (= JSON-policy set), mirrored verbatim from the gateway's
|
|
22
|
-
* `metadata-query-parse.ts` so a typo can't silently pass client-side and fail only server-side. */
|
|
23
|
-
export const METADATA_OPERATORS = [
|
|
24
|
-
"eq", "neq", "gt", "gte", "lt", "lte",
|
|
25
|
-
"contains", "startsWith", "endsWith", "in", "notIn",
|
|
26
|
-
"and", "or", "not",
|
|
27
|
-
] as const;
|
|
28
|
-
export type MetadataOperator = (typeof METADATA_OPERATORS)[number];
|
|
29
|
-
|
|
30
|
-
/** A surface label `<slice>:<scope>`, e.g. `collection:posts`, `storage:images`,
|
|
31
|
-
* `telemetry:auth.session.created`. */
|
|
32
|
-
export type MetadataSurface = `${MetadataSlice}:${string}`;
|
|
33
|
-
|
|
34
|
-
export type MetadataWhereClause = {
|
|
35
|
-
readonly field: string;
|
|
36
|
-
readonly op: MetadataOperator;
|
|
37
|
-
readonly value: unknown;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* One result row: "each record carries its surface + scope + the queried metadata
|
|
42
|
-
* fields plus a back-reference id" — `recordId` (collection), `objectPath` (storage), or `eventId`
|
|
43
|
-
* (telemetry), depending on which surface the row came from. Exact field names are provisional
|
|
44
|
-
* pending Inc 2 landing a concrete response schema; kept loose (`metadata: Record<string, unknown>`)
|
|
45
|
-
* rather than guessing a shape that isn't coined in code anywhere yet.
|
|
46
|
-
*/
|
|
47
|
-
export type MetadataQueryRecord = {
|
|
48
|
-
surface: MetadataSurface;
|
|
49
|
-
scope: string;
|
|
50
|
-
recordId?: string;
|
|
51
|
-
objectPath?: string;
|
|
52
|
-
eventId?: string;
|
|
53
|
-
metadata: Record<string, unknown>;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export type MetadataQueryResult = {
|
|
57
|
-
records: readonly MetadataQueryRecord[];
|
|
58
|
-
nextCursor: string | null;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
/** The `k9o.metadata.from(...).where(...).limit(...).execute()` builder. Immutable —
|
|
62
|
-
* every chained call returns a new builder, so a partially-built query can be safely reused/branched. */
|
|
63
|
-
export type MetadataQueryBuilder = {
|
|
64
|
-
where(field: string, op: MetadataOperator, value: unknown): MetadataQueryBuilder;
|
|
65
|
-
limit(n: number): MetadataQueryBuilder;
|
|
66
|
-
cursor(c: string | null): MetadataQueryBuilder;
|
|
67
|
-
execute(): Promise<Envelope<MetadataQueryResult>>;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
function requireProjectSlug(config: HttpClientConfig): string {
|
|
71
|
-
if (config.projectSlug === undefined || config.projectSlug.length === 0) {
|
|
72
|
-
throw new KError({
|
|
73
|
-
code: "K9O_MISSING_PROJECT_CONTEXT",
|
|
74
|
-
message: "client.metadata requires a bound projectSlug (project data-plane resource,)",
|
|
75
|
-
context: {},
|
|
76
|
-
recommendation: "Pass projectSlug when constructing the client.",
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return config.projectSlug;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function makeBuilder(
|
|
83
|
-
config: HttpClientConfig,
|
|
84
|
-
surfaces: readonly MetadataSurface[],
|
|
85
|
-
where: readonly MetadataWhereClause[],
|
|
86
|
-
limit: number | undefined,
|
|
87
|
-
cursorValue: string | null,
|
|
88
|
-
): MetadataQueryBuilder {
|
|
89
|
-
return {
|
|
90
|
-
where(field, op, value) {
|
|
91
|
-
return makeBuilder(config, surfaces, [...where, { field, op, value }], limit, cursorValue);
|
|
92
|
-
},
|
|
93
|
-
limit(n) {
|
|
94
|
-
return makeBuilder(config, surfaces, where, n, cursorValue);
|
|
95
|
-
},
|
|
96
|
-
cursor(c) {
|
|
97
|
-
return makeBuilder(config, surfaces, where, limit, c);
|
|
98
|
-
},
|
|
99
|
-
async execute() {
|
|
100
|
-
return request<MetadataQueryResult>(config, {
|
|
101
|
-
method: "POST",
|
|
102
|
-
path: projectPath(requireProjectSlug(config), "metadata/query"),
|
|
103
|
-
body: {
|
|
104
|
-
surfaces,
|
|
105
|
-
where,
|
|
106
|
-
...(limit !== undefined ? { limit } : {}),
|
|
107
|
-
cursor: cursorValue,
|
|
108
|
-
},
|
|
109
|
-
});
|
|
110
|
-
},
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function makeMetadataNamespace(config: HttpClientConfig) {
|
|
115
|
-
return {
|
|
116
|
-
/** Start a cross-surface metadata query against one or more surfaces. */
|
|
117
|
-
from(surface: MetadataSurface | readonly MetadataSurface[]): MetadataQueryBuilder {
|
|
118
|
-
const surfaces = Array.isArray(surface) ? surface : [surface as MetadataSurface];
|
|
119
|
-
if (surfaces.length === 0) {
|
|
120
|
-
throw new KError({
|
|
121
|
-
code: "K9O_UNKNOWN_SURFACE",
|
|
122
|
-
message: "client.metadata.from(...) requires at least one surface label.",
|
|
123
|
-
context: {},
|
|
124
|
-
recommendation: "Pass a surface like 'collection:posts', 'storage:images', or 'telemetry:auth.session.created'.",
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
return makeBuilder(config, surfaces, [], undefined, null);
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|