@configbutler/krm-stream 0.1.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.
@@ -0,0 +1,101 @@
1
+ /** The complete projected Kubernetes object, verbatim.
2
+ *
3
+ * Deliberately NOT a schema: a ConfigMap has `data` and no `spec`; a Secret has `type` and
4
+ * `stringData`; a CRD may define any field at the root. We carry whatever is there. */
5
+ export interface KRMObject {
6
+ apiVersion: string;
7
+ kind: string;
8
+ metadata: {
9
+ uid: string;
10
+ name: string;
11
+ namespace?: string;
12
+ resourceVersion?: string;
13
+ labels?: Record<string, string>;
14
+ annotations?: Record<string, string>;
15
+ [key: string]: unknown;
16
+ };
17
+ [key: string]: unknown;
18
+ }
19
+ /** A field address: an ARRAY of segments — object keys (any string) and array indices.
20
+ *
21
+ * Never a dot-joined string. `metadata.labels["app.kubernetes.io/name"]` is THREE segments, the last
22
+ * of which contains two dots and a slash; joining it produces an address that resolves to nothing,
23
+ * silently. That bug (R-ID) is why this type exists. */
24
+ export type Path = (string | number)[];
25
+ export type EventType = "reset" | "added" | "modified" | "deleted" | "synced" | "error";
26
+ export type ErrorCode = "FORBIDDEN" | "UNAUTHENTICATED" | "SCOPE_INVALID" | "UPSTREAM_UNAVAILABLE" | "RESYNC_REQUIRED" | "SLOW_CONSUMER" | "INTERNAL";
27
+ /** What the gateway removed from every object in this stream. On the wire, per cycle, because a
28
+ * consumer must be able to distinguish "absent upstream" from "the gateway took it away". */
29
+ export type Projection = "krm-raw/v1" | "krm-full/v1" | "krm-spec/v1";
30
+ /** A path whose value exists upstream but was withheld by the projection. `rev` starts at one and
31
+ * increments whenever that hidden value changes during this stream connection. */
32
+ export interface Redaction {
33
+ path: string;
34
+ rev: number;
35
+ }
36
+ export interface Scope {
37
+ target: string;
38
+ group?: string;
39
+ version: string;
40
+ resource: string;
41
+ namespace?: string;
42
+ name?: string;
43
+ labelSelector?: string;
44
+ }
45
+ /** The tombstone. `deleted` is the one event with no complete object — there may not be one — so it
46
+ * carries an identity instead. `uid` is what the consumer acts on. */
47
+ export interface Identity {
48
+ uid: string;
49
+ apiVersion: string;
50
+ kind: string;
51
+ namespace?: string;
52
+ name: string;
53
+ }
54
+ export interface StreamEvent {
55
+ /** Per-connection event sequence. A gap means the consumer must reconnect for a fresh snapshot. */
56
+ seq: number;
57
+ type: EventType;
58
+ target?: string;
59
+ scope?: Scope;
60
+ projection?: Projection;
61
+ object?: KRMObject;
62
+ /** Always present on added/modified — empty when nothing is redacted. */
63
+ redacted?: Redaction[];
64
+ identity?: Identity;
65
+ code?: ErrorCode;
66
+ message?: string;
67
+ terminal?: boolean;
68
+ retryAfterMs?: number | null;
69
+ }
70
+ /** One pending edit, derived by comparing the draft to the last server object. Never cached — a
71
+ * cached dirty set goes stale on the next watch event, which is regression R-DERIVED. */
72
+ export interface Change {
73
+ path: Path;
74
+ kind: "add" | "update" | "delete";
75
+ old: unknown;
76
+ new: unknown;
77
+ }
78
+ /** "The server moved this field while you were editing it, and to a different value than you
79
+ * typed." `theirs` is what the cluster says; the user's edit is never silently overwritten. */
80
+ export interface Conflict {
81
+ path: Path;
82
+ theirs: unknown;
83
+ }
84
+ /** Which regions of an object a user may edit. `status` is controller-owned and read-only — but
85
+ * read-only is NOT ignored: it still follows the server live and flashes what changed. That is the
86
+ * entire live-status-watch use case. */
87
+ export interface EditabilityPolicy {
88
+ isEditable(obj: KRMObject, path: Path): boolean;
89
+ /** True for a path that is not editable itself but that an editable region lives UNDER — `[]` and
90
+ * `["metadata"]` under the default policy.
91
+ *
92
+ * The merge needs both questions answered, and they are not each other's negation. `metadata` is
93
+ * not editable, but replacing it wholesale from the server would clobber the label the user is
94
+ * editing inside it; `status` is not editable either, and replacing it wholesale is exactly right.
95
+ * Without this second predicate a store cannot tell those two apart. */
96
+ containsEditable(obj: KRMObject, path: Path): boolean;
97
+ /** For an associative Kubernetes list at path, return its identity fields. Omit this to retain the
98
+ * safe atomic-array behavior. Implementations normally derive it from OpenAPI's
99
+ * `x-kubernetes-list-type: map` and `x-kubernetes-list-map-keys`. */
100
+ listMapKeys?(obj: KRMObject, path: Path): readonly string[] | undefined;
101
+ }
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ // The wire vocabulary and the client's core types. This file is the TypeScript half of the same
2
+ // contract gateway/event.go declares in Go; see ../../../spec/v1.md, which governs both.
3
+ //
4
+ // Nothing here has a runtime dependency, and nothing here knows about ConfigButler, GitOps, Dex or
5
+ // kcp. It knows KRM. That one-way rule is what makes this library adoptable by someone who has
6
+ // never heard of us.
7
+ export {};
package/dist/url.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { Projection, Scope } from "./types.ts";
2
+ /** A scope as a CALLER supplies it. `target` is optional here (a single-cluster host may never set
3
+ * one) though it is always present on the `reset` event the gateway sends back. */
4
+ export type ScopeQuery = Omit<Scope, "target"> & {
5
+ target?: string;
6
+ projection?: Projection;
7
+ };
8
+ /**
9
+ * Build the URL for a resource stream.
10
+ *
11
+ * ```ts
12
+ * connectResourceStream(resourceStreamURL("/resource-stream/v1", {
13
+ * version: "v1", resource: "configmaps", namespace: "app",
14
+ * }), store);
15
+ * ```
16
+ *
17
+ * The field order is FIXED — target, group, version, resource, namespace, name, labelSelector — so
18
+ * that the same scope always produces the same URL. That is not tidiness: a URL that varies by key
19
+ * order is one an HTTP cache, a log aggregator and a human diffing two bug reports all see as two
20
+ * different URLs.
21
+ *
22
+ * Note what this function does NOT take: an API-server address. There is nowhere to put one, which
23
+ * is spec §8's promise made structural rather than merely stated — and the gateway REFUSES such a
24
+ * parameter outright if someone appends one by hand.
25
+ */
26
+ export declare function resourceStreamURL(base: string, scope: ScopeQuery): string;
package/dist/url.js ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Build the URL for a resource stream.
3
+ *
4
+ * ```ts
5
+ * connectResourceStream(resourceStreamURL("/resource-stream/v1", {
6
+ * version: "v1", resource: "configmaps", namespace: "app",
7
+ * }), store);
8
+ * ```
9
+ *
10
+ * The field order is FIXED — target, group, version, resource, namespace, name, labelSelector — so
11
+ * that the same scope always produces the same URL. That is not tidiness: a URL that varies by key
12
+ * order is one an HTTP cache, a log aggregator and a human diffing two bug reports all see as two
13
+ * different URLs.
14
+ *
15
+ * Note what this function does NOT take: an API-server address. There is nowhere to put one, which
16
+ * is spec §8's promise made structural rather than merely stated — and the gateway REFUSES such a
17
+ * parameter outright if someone appends one by hand.
18
+ */
19
+ export function resourceStreamURL(base, scope) {
20
+ if (!scope.resource)
21
+ throw new Error("krm-stream: scope needs a `resource` (the plural, lowercase API name)");
22
+ if (!scope.version)
23
+ throw new Error("krm-stream: scope needs a `version` (there is no default: v1 and v1beta1 differ)");
24
+ const q = new URLSearchParams();
25
+ const set = (k, v) => {
26
+ if (v)
27
+ q.append(k, v);
28
+ };
29
+ set("target", scope.target);
30
+ set("group", scope.group);
31
+ set("version", scope.version);
32
+ set("resource", scope.resource);
33
+ set("namespace", scope.namespace);
34
+ set("name", scope.name);
35
+ set("labelSelector", scope.labelSelector);
36
+ set("projection", scope.projection);
37
+ return `${base}${base.includes("?") ? "&" : "?"}${q.toString()}`;
38
+ }
@@ -0,0 +1,4 @@
1
+ /** The npm package version of this build. Assert it against the copy you vendored. */
2
+ export declare const VERSION = "0.1.0";
3
+ /** The wire protocol this build speaks (spec/v1.md). The gateway sends it as `X-KRM-Stream-Protocol`. */
4
+ export declare const PROTOCOL_VERSION = 1;
@@ -0,0 +1,26 @@
1
+ // The version stamp, and it exists for one failure that is otherwise silent.
2
+ //
3
+ // This library is publishable to npm, but it is also VENDORED — copied, as built ESM, into a host
4
+ // that serves it to a browser (that is what the gateway's `--dist` flag is for). A vendored asset
5
+ // drifts. It keeps working, too: an old client and a new gateway agree about every event they both
6
+ // still understand, right up until the wire changes, and then the failure lands in someone's browser
7
+ // rather than in anyone's test suite.
8
+ //
9
+ // So the bytes carry their own provenance, and a host can assert it:
10
+ //
11
+ // import { VERSION, PROTOCOL_VERSION } from "@configbutler/krm-stream";
12
+ // // in the host's own test suite:
13
+ // assert.equal(PROTOCOL_VERSION, protocolVersionFromMyGoMod);
14
+ //
15
+ // Neither number is hand-maintained in the sense that matters: `task test` fails if VERSION drifts
16
+ // from package.json, and if PROTOCOL_VERSION drifts from the Go constant that DEFINES it (the gateway
17
+ // publishes it into conformance/gen/protocol.json, and the suite reads it back). Neither half of this
18
+ // repo can bump the protocol alone.
19
+ //
20
+ // VERSION is written by release-please, which finds it by the annotation on the line itself (see
21
+ // release-please-config.json). Do not edit it by hand: the release PR bumps package.json and this
22
+ // line together, and version.test.ts fails if they ever disagree.
23
+ /** The npm package version of this build. Assert it against the copy you vendored. */
24
+ export const VERSION = "0.1.0"; // x-release-please-version
25
+ /** The wire protocol this build speaks (spec/v1.md). The gateway sends it as `X-KRM-Stream-Protocol`. */
26
+ export const PROTOCOL_VERSION = 1;
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@configbutler/krm-stream",
3
+ "version": "0.1.0",
4
+ "description": "A live, honest window onto Kubernetes resources, in the browser: consume a KRM resource stream, three-way merge local edits, build a merge patch.",
5
+ "keywords": [
6
+ "kubernetes",
7
+ "krm",
8
+ "sse",
9
+ "watch",
10
+ "live",
11
+ "three-way-merge",
12
+ "resource-stream"
13
+ ],
14
+ "license": "Apache-2.0",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/ConfigButler/krm-stream.git",
18
+ "directory": "packages/krm-stream"
19
+ },
20
+ "type": "module",
21
+ "main": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md"
32
+ ],
33
+ "engines": {
34
+ "node": ">=22"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc",
38
+ "test": "node --test",
39
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
40
+ "lint": "biome check",
41
+ "format": "biome check --write"
42
+ },
43
+ "//": "ZERO dependencies, and that one is absolute: the engine emits plain ESM a browser imports with no bundler, and gitops-api has no bundler and is not getting one. devDependencies ship NOTHING and are kept to the three that pay for themselves: typescript (the compiler), @types/node (without it `tsc` cannot see test/ at all, because the tests import node:test — and `node --test` STRIPS types rather than checking them, so the suite that IS the contract check would be the one unverified file in the repo), and biome (lint + format in one binary, the TypeScript half of what gofmt/go vet/golangci-lint do for the gateway).",
44
+ "devDependencies": {
45
+ "@biomejs/biome": "^2.5.3",
46
+ "@types/node": "^26.1.1",
47
+ "typescript": "^5.9.0"
48
+ }
49
+ }