@nanobpm/urban 0.63.1 → 0.65.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/dist/context/binding/backend.d.ts +58 -0
- package/dist/context/binding/backend.js +15 -0
- package/dist/context/binding/descriptor.d.ts +51 -0
- package/dist/context/binding/descriptor.js +62 -0
- package/dist/context/binding/git-backend.d.ts +65 -0
- package/dist/context/binding/git-backend.js +318 -0
- package/dist/context/binding/identity.d.ts +42 -0
- package/dist/context/binding/identity.js +159 -0
- package/dist/context/binding/index.d.ts +5 -1
- package/dist/context/binding/index.js +14 -1
- package/dist/context/binding/resolver.d.ts +61 -0
- package/dist/context/binding/resolver.js +125 -0
- package/dist/runtime/core/modules/runtime.gen.js +1 -1
- package/package.json +1 -1
- package/src/context/binding/backend.ts +79 -0
- package/src/context/binding/descriptor.test.ts +51 -0
- package/src/context/binding/descriptor.ts +108 -0
- package/src/context/binding/git-backend.ts +368 -0
- package/src/context/binding/identity.test.ts +165 -0
- package/src/context/binding/identity.ts +206 -0
- package/src/context/binding/index.ts +46 -2
- package/src/context/binding/resolver.test.ts +713 -0
- package/src/context/binding/resolver.ts +176 -0
- package/src/runtime/browser/runtime.browser.js +63 -30
- package/src/runtime/core/modules/pages.test.ts +20 -1
- package/src/runtime/core/modules/runtime.gen.ts +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolve as resolvePath } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
contextIdentityKey,
|
|
6
|
+
resolveContextIdentity,
|
|
7
|
+
sameContext,
|
|
8
|
+
} from "./identity.ts";
|
|
9
|
+
|
|
10
|
+
test("same repo+ref ⇒ same identity key (shared-on-same-name)", () => {
|
|
11
|
+
const a = { repo: "owner/name", ref: "main" };
|
|
12
|
+
const b = { repo: "owner/name", ref: "main" };
|
|
13
|
+
assert.equal(contextIdentityKey(a), contextIdentityKey(b));
|
|
14
|
+
assert.equal(sameContext(a, b), true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("a different ref is a distinct, private context", () => {
|
|
18
|
+
const a = { repo: "owner/name", ref: "main" };
|
|
19
|
+
const b = { repo: "owner/name", ref: "release" };
|
|
20
|
+
assert.notEqual(contextIdentityKey(a), contextIdentityKey(b));
|
|
21
|
+
assert.equal(sameContext(a, b), false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("a different repo is a distinct, private context", () => {
|
|
25
|
+
const a = { repo: "owner/name", ref: "main" };
|
|
26
|
+
const b = { repo: "owner/other", ref: "main" };
|
|
27
|
+
assert.equal(sameContext(a, b), false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("shorthand and equivalent HTTPS URL normalise to the same identity", () => {
|
|
31
|
+
const shorthand = resolveContextIdentity({ repo: "Owner/Name", ref: "main" });
|
|
32
|
+
const url = resolveContextIdentity({
|
|
33
|
+
repo: "https://github.com/owner/name.git",
|
|
34
|
+
ref: "main",
|
|
35
|
+
});
|
|
36
|
+
assert.equal(shorthand.key, url.key);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("a trailing slash after .git does not fork identity (canonical drift guard)", () => {
|
|
40
|
+
// `stripGitSuffix` must remove `.git` even when a trailing slash follows it,
|
|
41
|
+
// or `…/name.git/` canonicalises to a different substrate than `…/name`.
|
|
42
|
+
const plain = resolveContextIdentity({ repo: "https://github.com/owner/name", ref: "main" });
|
|
43
|
+
const dotGitSlash = resolveContextIdentity({
|
|
44
|
+
repo: "https://github.com/owner/name.git/",
|
|
45
|
+
ref: "main",
|
|
46
|
+
});
|
|
47
|
+
const scpSlash = resolveContextIdentity({ repo: "git@github.com:owner/name.git/", ref: "main" });
|
|
48
|
+
assert.equal(dotGitSlash.key, plain.key);
|
|
49
|
+
assert.equal(scpSlash.key, plain.key);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("shorthand resolves to a concrete GitHub HTTPS clone URL", () => {
|
|
53
|
+
const identity = resolveContextIdentity({ repo: "owner/name", ref: "main" });
|
|
54
|
+
assert.equal(identity.repo, "https://github.com/owner/name.git");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("scp-like and https URLs for the same repo share identity", () => {
|
|
58
|
+
const scp = resolveContextIdentity({ repo: "git@github.com:owner/name.git", ref: "v2" });
|
|
59
|
+
const https = resolveContextIdentity({ repo: "https://github.com/owner/name", ref: "v2" });
|
|
60
|
+
assert.equal(scp.key, https.key);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("ssh://, scp-like and https URLs for one repo all share identity", () => {
|
|
64
|
+
const ssh = resolveContextIdentity({ repo: "ssh://git@github.com/owner/name.git", ref: "v2" });
|
|
65
|
+
const scp = resolveContextIdentity({ repo: "git@github.com:owner/name.git", ref: "v2" });
|
|
66
|
+
const https = resolveContextIdentity({ repo: "https://github.com/owner/name", ref: "v2" });
|
|
67
|
+
assert.equal(ssh.key, scp.key);
|
|
68
|
+
assert.equal(ssh.key, https.key);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("embedded credentials and explicit default port do not affect identity", () => {
|
|
72
|
+
const plain = resolveContextIdentity({ repo: "https://github.com/owner/name.git", ref: "main" });
|
|
73
|
+
const creds = resolveContextIdentity({
|
|
74
|
+
repo: "https://user:token@github.com/owner/name.git",
|
|
75
|
+
ref: "main",
|
|
76
|
+
});
|
|
77
|
+
const port = resolveContextIdentity({
|
|
78
|
+
repo: "https://github.com:443/owner/name.git",
|
|
79
|
+
ref: "main",
|
|
80
|
+
});
|
|
81
|
+
assert.equal(creds.key, plain.key);
|
|
82
|
+
assert.equal(port.key, plain.key);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("ssh:// explicit default port (22) does not affect identity", () => {
|
|
86
|
+
const plain = resolveContextIdentity({ repo: "ssh://git@github.com/owner/name.git", ref: "main" });
|
|
87
|
+
const port = resolveContextIdentity({
|
|
88
|
+
repo: "ssh://git@github.com:22/owner/name.git",
|
|
89
|
+
ref: "main",
|
|
90
|
+
});
|
|
91
|
+
assert.equal(port.key, plain.key);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("non-default ports produce DISTINCT identities (no cross-repo collision)", () => {
|
|
95
|
+
const plain = resolveContextIdentity({ repo: "https://host.example/owner/name.git", ref: "main" });
|
|
96
|
+
const alt = resolveContextIdentity({
|
|
97
|
+
repo: "https://host.example:8443/owner/name.git",
|
|
98
|
+
ref: "main",
|
|
99
|
+
});
|
|
100
|
+
const other = resolveContextIdentity({
|
|
101
|
+
repo: "https://host.example:9443/owner/name.git",
|
|
102
|
+
ref: "main",
|
|
103
|
+
});
|
|
104
|
+
assert.notEqual(alt.key, plain.key);
|
|
105
|
+
assert.notEqual(alt.key, other.key);
|
|
106
|
+
// ssh non-default ports must distinguish too.
|
|
107
|
+
const ssh = resolveContextIdentity({ repo: "ssh://git@host.example/owner/name.git", ref: "main" });
|
|
108
|
+
const sshPort = resolveContextIdentity({
|
|
109
|
+
repo: "ssh://git@host.example:2222/owner/name.git",
|
|
110
|
+
ref: "main",
|
|
111
|
+
});
|
|
112
|
+
assert.notEqual(ssh.key, sshPort.key);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("local file paths canonicalise by absolute path", () => {
|
|
116
|
+
const relative = resolveContextIdentity({ repo: "./sub/../sub/repo", ref: "main" });
|
|
117
|
+
const absolute = resolveContextIdentity({
|
|
118
|
+
repo: resolvePath("sub/repo"),
|
|
119
|
+
ref: "main",
|
|
120
|
+
});
|
|
121
|
+
const fileUrl = resolveContextIdentity({
|
|
122
|
+
repo: `file://${resolvePath("sub/repo")}`,
|
|
123
|
+
ref: "main",
|
|
124
|
+
});
|
|
125
|
+
assert.equal(relative.key, absolute.key);
|
|
126
|
+
assert.equal(absolute.key, fileUrl.key);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("file:// host component and percent-encoding canonicalise to one identity", () => {
|
|
130
|
+
const abs = resolvePath("sub/repo");
|
|
131
|
+
const plain = resolveContextIdentity({ repo: `file://${abs}`, ref: "main" });
|
|
132
|
+
// `file://localhost/…` (explicit localhost host) is equivalent to `file:///…`.
|
|
133
|
+
const localhost = resolveContextIdentity({ repo: `file://localhost${abs}`, ref: "main" });
|
|
134
|
+
assert.equal(plain.key, localhost.key);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("percent-encoded file:// path canonicalises to the decoded path identity", () => {
|
|
138
|
+
const dir = resolvePath("with space/repo");
|
|
139
|
+
const decoded = resolveContextIdentity({ repo: `file://${dir}`, ref: "main" });
|
|
140
|
+
const encoded = resolveContextIdentity({
|
|
141
|
+
repo: `file://${dir.replace(/ /g, "%20")}`,
|
|
142
|
+
ref: "main",
|
|
143
|
+
});
|
|
144
|
+
assert.equal(decoded.key, encoded.key);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("slug is filesystem-safe and identity-stable", () => {
|
|
148
|
+
const a = resolveContextIdentity({ repo: "owner/name", ref: "feature/x" });
|
|
149
|
+
const b = resolveContextIdentity({ repo: "owner/name", ref: "feature/x" });
|
|
150
|
+
assert.equal(a.slug, b.slug);
|
|
151
|
+
assert.match(a.slug, /^[a-z0-9-]+$/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("slug suffix is the full identity key ⇒ distinct keys never share a localPath", () => {
|
|
155
|
+
// The on-disk working copy is `join(cacheRoot, slug)`, so the slug's uniqueness
|
|
156
|
+
// must be exactly as strong as the identity key — a truncated hash suffix would
|
|
157
|
+
// let two distinct keys collide onto one directory. Guard that the slug embeds
|
|
158
|
+
// the whole sha256 key (64 hex chars), not a truncated prefix.
|
|
159
|
+
const identity = resolveContextIdentity({ repo: "owner/name", ref: "main" });
|
|
160
|
+
assert.match(identity.key, /^[0-9a-f]{64}$/);
|
|
161
|
+
assert.ok(
|
|
162
|
+
identity.slug.endsWith(`-${identity.key}`),
|
|
163
|
+
"slug must be suffixed with the full identity key",
|
|
164
|
+
);
|
|
165
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// Slice S1 (binding) — context *identity*: the private-per-app /
|
|
2
|
+
// shared-on-same-name semantics.
|
|
3
|
+
//
|
|
4
|
+
// A context is named by its `(repo, ref)` pair. Two apps (or two Nano
|
|
5
|
+
// instances) that name the SAME context — the same normalised repo and the same
|
|
6
|
+
// ref — resolve to the SAME shared substrate; a distinct name stays private.
|
|
7
|
+
// This module turns a binding into a stable, canonical **identity key** that
|
|
8
|
+
// the resolver uses both to memoise in-process and to derive an on-disk
|
|
9
|
+
// location, so "same name ⇒ same substrate" holds within a process AND across
|
|
10
|
+
// processes/instances that share a cache root.
|
|
11
|
+
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
import { isAbsolute, resolve as resolvePath } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import type { ContextBinding } from "./descriptor.ts";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The resolved, canonical identity of a context binding.
|
|
19
|
+
*
|
|
20
|
+
* `key` is a stable, collision-resistant string: two bindings share a substrate
|
|
21
|
+
* **iff** their `key`s are equal. `repo` is the concrete clone URL derived from
|
|
22
|
+
* the binding's (possibly shorthand) `repo`, and `ref` is the pinned ref.
|
|
23
|
+
*/
|
|
24
|
+
export interface ContextIdentity {
|
|
25
|
+
/**
|
|
26
|
+
* Stable canonical identity key. Equal keys ⇒ same shared substrate; distinct
|
|
27
|
+
* keys ⇒ private substrates. Safe to use as a map key.
|
|
28
|
+
*/
|
|
29
|
+
readonly key: string;
|
|
30
|
+
/** The concrete clone URL (or local/`file://` path) for the substrate. */
|
|
31
|
+
readonly repo: string;
|
|
32
|
+
/** The pinned ref (branch / tag / SHA). */
|
|
33
|
+
readonly ref: string;
|
|
34
|
+
/**
|
|
35
|
+
* Filesystem-safe directory name derived deterministically from {@link key}.
|
|
36
|
+
* Two identities with the same key yield the same directory name, so a shared
|
|
37
|
+
* cache root gives them the same on-disk working copy.
|
|
38
|
+
*/
|
|
39
|
+
readonly slug: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const SHORTHAND = /^[\w.-]+\/[\w.-]+$/;
|
|
43
|
+
const SCHEME_URL = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
44
|
+
const SCP_LIKE = /^[\w.-]+@([\w.-]+):(.+)$/; // git@github.com:owner/name.git
|
|
45
|
+
|
|
46
|
+
interface ClassifiedRepo {
|
|
47
|
+
/** URL/path git can clone from. */
|
|
48
|
+
readonly url: string;
|
|
49
|
+
/** Normalised canonical string used for identity (never the clone URL). */
|
|
50
|
+
readonly canonical: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function stripGitSuffix(value: string): string {
|
|
54
|
+
// Trim trailing slashes BEFORE stripping `.git` so a trailing-slash spelling
|
|
55
|
+
// (`owner/name.git/`, a URL pathname ending `.git/`) still has its `.git`
|
|
56
|
+
// suffix removed — otherwise `.git$` would not match and equivalent repo
|
|
57
|
+
// spellings would canonicalise to different identities (different substrates).
|
|
58
|
+
// Trim once more afterwards to drop any slash exposed by removing the suffix.
|
|
59
|
+
return value.replace(/\/+$/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A URL's scheme-default port is equivalent to omitting the port, so it must not
|
|
63
|
+
// split identity. `URL` already strips the default port for the web schemes it
|
|
64
|
+
// knows (http/https/ws/…), but NOT for git transports like ssh:// and git://, so
|
|
65
|
+
// their defaults are normalised here explicitly.
|
|
66
|
+
const DEFAULT_PORTS: Record<string, string> = {
|
|
67
|
+
"http:": "80",
|
|
68
|
+
"https:": "443",
|
|
69
|
+
"ssh:": "22",
|
|
70
|
+
"git:": "9418",
|
|
71
|
+
"ftp:": "21",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The canonical authority (`host` or `host:port`) for a scheme URL identity.
|
|
76
|
+
* Excludes any `user:pass@` userinfo (uses `hostname`, never `host`) so
|
|
77
|
+
* credentials never affect identity, and drops a scheme's DEFAULT port while
|
|
78
|
+
* retaining any NON-default port as a distinguishing factor.
|
|
79
|
+
*/
|
|
80
|
+
function canonicalAuthority(parsed: URL): string {
|
|
81
|
+
const host = parsed.hostname.toLowerCase();
|
|
82
|
+
const defaultPort = DEFAULT_PORTS[parsed.protocol.toLowerCase()];
|
|
83
|
+
if (parsed.port && parsed.port !== defaultPort) {
|
|
84
|
+
return `${host}:${parsed.port}`;
|
|
85
|
+
}
|
|
86
|
+
return host;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve a `file://` URL to an absolute filesystem path, honouring the host
|
|
91
|
+
* component and percent-encoding. Falls back to naive `file://` stripping for a
|
|
92
|
+
* malformed URL so identity stays stable rather than throwing.
|
|
93
|
+
*/
|
|
94
|
+
function fileUrlToAbsPath(raw: string): string {
|
|
95
|
+
try {
|
|
96
|
+
return resolvePath(fileURLToPath(raw));
|
|
97
|
+
} catch {
|
|
98
|
+
return resolvePath(raw.slice("file://".length));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Classify a binding `repo` field into a concrete clone URL plus a normalised
|
|
104
|
+
* canonical form for identity. Public git is NOT hard-coded: local/`file://`
|
|
105
|
+
* paths and arbitrary git hosts are first-class, leaving a seam for a future
|
|
106
|
+
* PII/mutable or self-hosted substrate backend.
|
|
107
|
+
*/
|
|
108
|
+
function classifyRepo(repo: string): ClassifiedRepo {
|
|
109
|
+
const raw = repo.trim();
|
|
110
|
+
|
|
111
|
+
// Local filesystem path or file:// URL — used for tests and self-hosted
|
|
112
|
+
// substrates. Canonicalised by absolute path so two spellings of one path
|
|
113
|
+
// share identity. `file://` URLs are parsed via `fileURLToPath` so the host
|
|
114
|
+
// component (e.g. `file://localhost/…`) and percent-encoding are handled and
|
|
115
|
+
// equivalent spellings map to one identity.
|
|
116
|
+
if (raw.startsWith("file://")) {
|
|
117
|
+
const abs = fileUrlToAbsPath(raw);
|
|
118
|
+
return { url: raw, canonical: `file:${abs}` };
|
|
119
|
+
}
|
|
120
|
+
if (raw.startsWith(".") || isAbsolute(raw)) {
|
|
121
|
+
const abs = resolvePath(raw);
|
|
122
|
+
return { url: abs, canonical: `file:${abs}` };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// owner/name shorthand ⇒ GitHub HTTPS clone URL.
|
|
126
|
+
if (SHORTHAND.test(raw)) {
|
|
127
|
+
const canonical = `github.com/${stripGitSuffix(raw).toLowerCase()}`;
|
|
128
|
+
return { url: `https://github.com/${stripGitSuffix(raw)}.git`, canonical };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// scp-like git URL: git@host:owner/name(.git)
|
|
132
|
+
const scp = SCP_LIKE.exec(raw);
|
|
133
|
+
if (scp) {
|
|
134
|
+
const host = scp[1].toLowerCase();
|
|
135
|
+
const path = stripGitSuffix(scp[2]).toLowerCase();
|
|
136
|
+
return { url: raw, canonical: `${host}/${path}` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// scheme URL: https://, ssh://, git://, http://
|
|
140
|
+
if (SCHEME_URL.test(raw)) {
|
|
141
|
+
try {
|
|
142
|
+
const parsed = new URL(raw);
|
|
143
|
+
// Build the authority from `hostname` (never `host`) so any `user:pass@`
|
|
144
|
+
// userinfo is excluded — credentials never affect identity. The port is
|
|
145
|
+
// *normalised*, not dropped: a scheme's DEFAULT port collapses to the
|
|
146
|
+
// portless spelling (so `https://h:443/o/n` ≡ `https://h/o/n` and
|
|
147
|
+
// `ssh://h:22/o/n` ≡ `ssh://h/o/n`), but a NON-default port is retained as
|
|
148
|
+
// a distinguishing factor — two remotes on the same host but different
|
|
149
|
+
// ports are genuinely different repos and MUST NOT share a substrate.
|
|
150
|
+
const host = canonicalAuthority(parsed);
|
|
151
|
+
const path = stripGitSuffix(parsed.pathname).replace(/^\/+/, "").toLowerCase();
|
|
152
|
+
return { url: raw, canonical: `${host}/${path}` };
|
|
153
|
+
} catch {
|
|
154
|
+
// Fall through to the opaque case below.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Anything else: treat opaquely but still stable.
|
|
159
|
+
return { url: raw, canonical: stripGitSuffix(raw).toLowerCase() };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function toSlug(canonical: string, ref: string, hash: string): string {
|
|
163
|
+
const readable = `${canonical}@${ref}`
|
|
164
|
+
.toLowerCase()
|
|
165
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
166
|
+
.replace(/^-+|-+$/g, "")
|
|
167
|
+
.slice(0, 48);
|
|
168
|
+
// Suffix with the FULL identity key (sha256 hex), not a truncated prefix: the
|
|
169
|
+
// on-disk location is derived from the slug, so a truncated suffix would make
|
|
170
|
+
// two distinct keys collide onto one `localPath` (breaking "distinct names
|
|
171
|
+
// stay private") far sooner than the key itself collides. Deriving the suffix
|
|
172
|
+
// from the whole key keeps on-disk uniqueness exactly as strong as the
|
|
173
|
+
// in-process sharing key — a single source of truth, distinct keys ⇒ distinct
|
|
174
|
+
// slugs.
|
|
175
|
+
return `${readable || "context"}-${hash}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Compute the canonical {@link ContextIdentity} for a binding. Deterministic and
|
|
180
|
+
* pure: equal `(repo, ref)` names (after normalisation) always produce an equal
|
|
181
|
+
* `key` and `slug`. Distinct names are collision-resistant, not collision-proof:
|
|
182
|
+
* both `key` and `slug` are sha256-derived, so two distinct names collide only on
|
|
183
|
+
* a full sha256 collision (the `slug` embeds the whole `key`, so it is exactly as
|
|
184
|
+
* strong as the `key`).
|
|
185
|
+
*/
|
|
186
|
+
export function resolveContextIdentity(binding: ContextBinding): ContextIdentity {
|
|
187
|
+
const { url, canonical } = classifyRepo(binding.repo);
|
|
188
|
+
const ref = binding.ref.trim();
|
|
189
|
+
const canonicalName = `${canonical}#${ref}`;
|
|
190
|
+
const key = createHash("sha256").update(canonicalName, "utf8").digest("hex");
|
|
191
|
+
return { key, repo: url, ref, slug: toSlug(canonical, ref, key) };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The stable identity key of a binding — the sharing primitive. Two bindings
|
|
196
|
+
* resolve to the same shared substrate iff this returns the same value for
|
|
197
|
+
* both.
|
|
198
|
+
*/
|
|
199
|
+
export function contextIdentityKey(binding: ContextBinding): string {
|
|
200
|
+
return resolveContextIdentity(binding).key;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** `true` iff two bindings name the same shared context (same substrate). */
|
|
204
|
+
export function sameContext(a: ContextBinding, b: ContextBinding): boolean {
|
|
205
|
+
return contextIdentityKey(a) === contextIdentityKey(b);
|
|
206
|
+
}
|
|
@@ -1,2 +1,46 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// @nanobpm/urban/context/binding — slice S1.
|
|
2
|
+
//
|
|
3
|
+
// The bindable `context` resource: the binding descriptor (`uses: context:
|
|
4
|
+
// { repo, ref }`) plus local↔remote resolution to a resolved handle, with
|
|
5
|
+
// private-per-app / shared-on-same-name identity semantics. This is one half of
|
|
6
|
+
// the published contract nanobpm/nano-workforce#291 builds against.
|
|
7
|
+
//
|
|
8
|
+
// Downstream: S3 (git substrate/governance) and S4 (retrieval) consume the
|
|
9
|
+
// ResolvedContextHandle; the SubstrateBackend interface is the seam for a
|
|
10
|
+
// future PII/mutable or self-hosted backend.
|
|
11
|
+
|
|
12
|
+
export {
|
|
13
|
+
type ContextBinding,
|
|
14
|
+
ContextBindingError,
|
|
15
|
+
isContextBinding,
|
|
16
|
+
parseContextBinding,
|
|
17
|
+
} from "./descriptor.ts";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
type ContextIdentity,
|
|
21
|
+
contextIdentityKey,
|
|
22
|
+
resolveContextIdentity,
|
|
23
|
+
sameContext,
|
|
24
|
+
} from "./identity.ts";
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
type ResolvedContextHandle,
|
|
28
|
+
type SubstrateBackend,
|
|
29
|
+
type SubstrateResolveOptions,
|
|
30
|
+
} from "./backend.ts";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
GitSubstrateBackend,
|
|
34
|
+
type GitRunner,
|
|
35
|
+
GIT_SAFETY_CONFIG,
|
|
36
|
+
hardenedGitArgs,
|
|
37
|
+
SubstrateResolveError,
|
|
38
|
+
} from "./git-backend.ts";
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
ContextResolver,
|
|
42
|
+
type ContextResolverOptions,
|
|
43
|
+
type ResolveOptions,
|
|
44
|
+
defaultContextCacheRoot,
|
|
45
|
+
resolveContextBinding,
|
|
46
|
+
} from "./resolver.ts";
|