superbee 0.2.1-pre.2 → 0.3.0-pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -6
- package/SKILL.md +1 -1
- package/dist/publication/bridge.d.ts +9 -0
- package/dist/publication/capture.d.ts +5 -1
- package/dist/publication/index.d.ts +1 -0
- package/dist/publication-bridge.mjs +542 -216
- package/dist/publication.mjs +597 -232
- package/dist/superbee.mjs +59738 -57909
- package/package.json +11 -8
- package/references/recipes/review-workflow/references/view-authoring-v0.md +101 -206
- package/references/views/references/view-authoring-v0.md +101 -206
|
@@ -2963,21 +2963,28 @@ function normalizeFrontmatter(data, okfVersion) {
|
|
|
2963
2963
|
function parseMarkdown(raw, context, options = {}) {
|
|
2964
2964
|
const split = splitLeadingFrontmatter(raw, context);
|
|
2965
2965
|
if (!("yamlSource" in split)) return { frontmatter: {}, body: split.body };
|
|
2966
|
+
return { frontmatter: parseFrontmatter(split.yamlSource, context, options), body: split.body };
|
|
2967
|
+
}
|
|
2968
|
+
function parseFrontmatter(source, context, options = {}) {
|
|
2966
2969
|
let parsed;
|
|
2967
2970
|
try {
|
|
2968
|
-
parsed = yamlEngine.parse(
|
|
2971
|
+
parsed = yamlEngine.parse(source);
|
|
2969
2972
|
} catch (err) {
|
|
2970
2973
|
throw new MalformedDocumentError(context, err);
|
|
2971
2974
|
}
|
|
2972
|
-
|
|
2973
|
-
|
|
2975
|
+
return normalizeFrontmatter(parsed, options.okfVersion);
|
|
2976
|
+
}
|
|
2977
|
+
function stringifyFrontmatter(data) {
|
|
2978
|
+
return import_js_yaml.default.safeDump(data).trim();
|
|
2974
2979
|
}
|
|
2975
2980
|
function normalizeDocumentBodyForStorage(body) {
|
|
2976
2981
|
return body.endsWith("\n") ? body : `${body}
|
|
2977
2982
|
`;
|
|
2978
2983
|
}
|
|
2979
2984
|
function stringifyWithData(data, body) {
|
|
2980
|
-
|
|
2985
|
+
return stringifyWithSerializedFrontmatter(stringifyFrontmatter(data), body);
|
|
2986
|
+
}
|
|
2987
|
+
function stringifyWithSerializedFrontmatter(dumped, body) {
|
|
2981
2988
|
const content = body ?? "";
|
|
2982
2989
|
const newline = (value) => value.endsWith("\n") ? value : `${value}
|
|
2983
2990
|
`;
|
|
@@ -3038,6 +3045,29 @@ function mutationActorFromFrontmatter(frontmatter2) {
|
|
|
3038
3045
|
|
|
3039
3046
|
// ../core/src/paths.ts
|
|
3040
3047
|
var RESERVED_FILENAMES = ["index.md", "log.md"];
|
|
3048
|
+
function compareStorageKeys(a, b) {
|
|
3049
|
+
const primary = a.localeCompare(b);
|
|
3050
|
+
if (primary !== 0 || a === b) return primary;
|
|
3051
|
+
const left = Array.from(a, (character) => character.codePointAt(0));
|
|
3052
|
+
const right = Array.from(b, (character) => character.codePointAt(0));
|
|
3053
|
+
for (let i = 0; i < Math.min(left.length, right.length); i++) {
|
|
3054
|
+
if (left[i] !== right[i]) return left[i] - right[i];
|
|
3055
|
+
}
|
|
3056
|
+
return left.length - right.length;
|
|
3057
|
+
}
|
|
3058
|
+
function assertPortablePath(value, label) {
|
|
3059
|
+
if (/[\u0000-\u001f\u007f]/u.test(value) || /^[a-z]:/i.test(value)) {
|
|
3060
|
+
throw new InvalidInputError(`${label} must not contain controls or start with a drive prefix.`);
|
|
3061
|
+
}
|
|
3062
|
+
if (value.includes("\\") || value.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
3063
|
+
throw new InvalidInputError(`${label} must be canonical and bundle-relative.`);
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
function assertSafeReservedFilename(name) {
|
|
3067
|
+
if (!RESERVED_FILENAMES.includes(name)) {
|
|
3068
|
+
throw new InvalidInputError("Reserved filename must be index.md or log.md.");
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3041
3071
|
function toPosix(p) {
|
|
3042
3072
|
return p.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
|
|
3043
3073
|
}
|
|
@@ -3057,21 +3087,8 @@ function assertSafeConceptId(id) {
|
|
|
3057
3087
|
if (typeof id !== "string" || id.trim() === "") {
|
|
3058
3088
|
throw new InvalidInputError("Concept id must be a non-empty string.");
|
|
3059
3089
|
}
|
|
3060
|
-
|
|
3061
|
-
throw new InvalidInputError(`Concept id must be bundle-relative, got absolute '${id}'.`);
|
|
3062
|
-
}
|
|
3063
|
-
if (id.includes("\\")) {
|
|
3064
|
-
throw new InvalidInputError(`Concept id must use forward slashes: '${id}'.`);
|
|
3065
|
-
}
|
|
3090
|
+
assertPortablePath(id, "Concept id");
|
|
3066
3091
|
const segments = id.split("/");
|
|
3067
|
-
if (segments.some((seg) => seg === "..")) {
|
|
3068
|
-
throw new InvalidInputError(`Concept id must not contain '..' segments: '${id}'.`);
|
|
3069
|
-
}
|
|
3070
|
-
if (segments.some((seg) => seg === "." || seg === "")) {
|
|
3071
|
-
throw new InvalidInputError(
|
|
3072
|
-
`Concept id must be canonical (no '.', duplicate-slash, leading './', or trailing-slash segments): '${id}'.`
|
|
3073
|
-
);
|
|
3074
|
-
}
|
|
3075
3092
|
if (segments.slice(0, -1).some((seg) => seg.toLowerCase().endsWith(".md"))) {
|
|
3076
3093
|
throw new InvalidInputError(
|
|
3077
3094
|
`Concept id must not contain a non-final segment ending in '.md': '${id}' (it collides with a concept file at that path).`
|
|
@@ -3082,21 +3099,11 @@ function assertSafeBlobKey(key) {
|
|
|
3082
3099
|
if (typeof key !== "string" || key.trim() === "") {
|
|
3083
3100
|
throw new InvalidInputError("Blob key must be a non-empty string.");
|
|
3084
3101
|
}
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
throw new InvalidInputError(`Blob key must be bundle-relative, got absolute '${key}'.`);
|
|
3088
|
-
}
|
|
3089
|
-
const segments = norm.split("/");
|
|
3090
|
-
if (segments.some((seg) => seg === "..")) {
|
|
3091
|
-
throw new InvalidInputError(`Blob key must not contain '..' segments: '${key}'.`);
|
|
3092
|
-
}
|
|
3102
|
+
assertPortablePath(key, "Blob key");
|
|
3103
|
+
const segments = key.split("/");
|
|
3093
3104
|
if (segments.some((seg) => seg.startsWith("."))) {
|
|
3094
3105
|
throw new InvalidInputError(`Blob key must not contain dot-prefixed segments: '${key}'.`);
|
|
3095
3106
|
}
|
|
3096
|
-
const last = segments[segments.length - 1] ?? "";
|
|
3097
|
-
if (last === "") {
|
|
3098
|
-
throw new InvalidInputError(`Blob key must name a file, not end with '/': '${key}'.`);
|
|
3099
|
-
}
|
|
3100
3107
|
if (segments.some((seg) => seg.toLowerCase().endsWith(".md"))) {
|
|
3101
3108
|
throw new InvalidInputError(
|
|
3102
3109
|
`Blob key '${key}' has a path segment ending in '.md' (checked case-insensitively, at any depth), which collides with the concept-document namespace \u2014 write it as a doc instead.`
|
|
@@ -3107,13 +3114,7 @@ function assertSafeReservedDir(dir) {
|
|
|
3107
3114
|
if (typeof dir !== "string") {
|
|
3108
3115
|
throw new InvalidInputError("Reserved-file directory must be a string.");
|
|
3109
3116
|
}
|
|
3110
|
-
|
|
3111
|
-
if (norm.startsWith("/")) {
|
|
3112
|
-
throw new InvalidInputError(`Reserved-file directory must be bundle-relative, got absolute '${dir}'.`);
|
|
3113
|
-
}
|
|
3114
|
-
if (norm.split("/").some((seg) => seg === "..")) {
|
|
3115
|
-
throw new InvalidInputError(`Reserved-file directory must not contain '..' segments: '${dir}'.`);
|
|
3116
|
-
}
|
|
3117
|
+
if (dir !== "") assertPortablePath(dir, "Reserved-file directory");
|
|
3117
3118
|
}
|
|
3118
3119
|
|
|
3119
3120
|
// ../core/src/links.ts
|
|
@@ -3418,7 +3419,52 @@ import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypt
|
|
|
3418
3419
|
|
|
3419
3420
|
// ../core/src/filesystem-lock.ts
|
|
3420
3421
|
import { promises as fs, realpathSync } from "node:fs";
|
|
3421
|
-
import { homedir, hostname
|
|
3422
|
+
import { homedir, hostname } from "node:os";
|
|
3423
|
+
|
|
3424
|
+
// ../core/src/host-policy-snapshot.ts
|
|
3425
|
+
function snapshotHostPolicy(input, seen = /* @__PURE__ */ new WeakMap()) {
|
|
3426
|
+
const existing = seen.get(input);
|
|
3427
|
+
if (existing) return existing;
|
|
3428
|
+
const result = Array.isArray(input) ? [] : {};
|
|
3429
|
+
seen.set(input, result);
|
|
3430
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3431
|
+
for (let owner = input; owner && owner !== Object.prototype && owner !== Array.prototype; owner = Object.getPrototypeOf(owner)) {
|
|
3432
|
+
for (const key of Reflect.ownKeys(owner)) {
|
|
3433
|
+
if (key !== "constructor" && !(Array.isArray(input) && key === "length")) keys.add(key);
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
for (const key of keys) {
|
|
3437
|
+
const value = input[key];
|
|
3438
|
+
Object.defineProperty(result, key, {
|
|
3439
|
+
value: typeof value === "function" ? value.bind(result) : value && typeof value === "object" ? snapshotHostPolicy(value, seen) : value,
|
|
3440
|
+
enumerable: true
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
if (Array.isArray(input)) result.length = input.length;
|
|
3444
|
+
return Object.freeze(result);
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
// ../core/src/filesystem-host.ts
|
|
3448
|
+
function defaultFilesystemHostPolicy() {
|
|
3449
|
+
if (process.platform !== "darwin" && process.platform !== "linux") {
|
|
3450
|
+
throw new InvalidInputError(
|
|
3451
|
+
"This host requires an explicit filesystem host policy; the default filesystem supports macOS and Linux."
|
|
3452
|
+
);
|
|
3453
|
+
}
|
|
3454
|
+
return {
|
|
3455
|
+
runtimeLockParent: () => "/tmp",
|
|
3456
|
+
runtimeOwnerKey: () => `uid-${process.getuid()}`,
|
|
3457
|
+
enforcePrivateMode: true,
|
|
3458
|
+
isTransientOpenError: () => false,
|
|
3459
|
+
isReplacementConflict: () => false,
|
|
3460
|
+
isDirectoryContentionError: () => false
|
|
3461
|
+
};
|
|
3462
|
+
}
|
|
3463
|
+
function captureFilesystemHostPolicy(policy) {
|
|
3464
|
+
return snapshotHostPolicy(policy ?? defaultFilesystemHostPolicy());
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
// ../core/src/filesystem-lock.ts
|
|
3422
3468
|
import path from "node:path";
|
|
3423
3469
|
import { createHash, randomUUID } from "node:crypto";
|
|
3424
3470
|
var OWNER_FILE = "owner.json";
|
|
@@ -3438,16 +3484,6 @@ var FilesystemMutationLockError = class extends Error {
|
|
|
3438
3484
|
this.malformed = details.malformed;
|
|
3439
3485
|
}
|
|
3440
3486
|
};
|
|
3441
|
-
function runtimeOwnerKey() {
|
|
3442
|
-
const uid = process.getuid?.();
|
|
3443
|
-
if (uid !== void 0) return `uid-${uid}`;
|
|
3444
|
-
let username = "unknown";
|
|
3445
|
-
try {
|
|
3446
|
-
username = userInfo().username;
|
|
3447
|
-
} catch {
|
|
3448
|
-
}
|
|
3449
|
-
return `user-${createHash("sha256").update(username).digest("hex").slice(0, 16)}`;
|
|
3450
|
-
}
|
|
3451
3487
|
function canonicalExistingPath(value) {
|
|
3452
3488
|
try {
|
|
3453
3489
|
return realpathSync(value);
|
|
@@ -3459,11 +3495,12 @@ function pathContains(root, candidate) {
|
|
|
3459
3495
|
const rel = path.relative(root, candidate);
|
|
3460
3496
|
return rel === "" || !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
3461
3497
|
}
|
|
3462
|
-
function filesystemMutationLockRoot(portableRoot) {
|
|
3463
|
-
const
|
|
3498
|
+
function filesystemMutationLockRoot(portableRoot, hostPolicy) {
|
|
3499
|
+
const policy = captureFilesystemHostPolicy(hostPolicy);
|
|
3500
|
+
const runtimeParent = policy.runtimeLockParent();
|
|
3464
3501
|
const tempParent = canonicalExistingPath(runtimeParent);
|
|
3465
3502
|
const homeParent = canonicalExistingPath(homedir());
|
|
3466
|
-
const ownerKey = runtimeOwnerKey();
|
|
3503
|
+
const ownerKey = policy.runtimeOwnerKey();
|
|
3467
3504
|
const candidates = [
|
|
3468
3505
|
path.join(tempParent, `agentstate-lite-mutation-locks-${ownerKey}`),
|
|
3469
3506
|
path.join(homeParent, ".agentstate", `mutation-locks-${ownerKey}`)
|
|
@@ -3503,15 +3540,33 @@ function parseFilesystemMutationLockOwner(value) {
|
|
|
3503
3540
|
target: value.target
|
|
3504
3541
|
};
|
|
3505
3542
|
}
|
|
3506
|
-
async function
|
|
3543
|
+
async function readOwnerRecord(lockPath) {
|
|
3544
|
+
let raw;
|
|
3507
3545
|
try {
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3546
|
+
raw = await fs.readFile(path.join(lockPath, OWNER_FILE), "utf8");
|
|
3547
|
+
} catch (err) {
|
|
3548
|
+
const code = err.code;
|
|
3549
|
+
return code === "ENOENT" || code === "ENOTDIR" ? { state: "absent" } : { state: "unreadable", error: err };
|
|
3550
|
+
}
|
|
3551
|
+
let owner = null;
|
|
3552
|
+
try {
|
|
3553
|
+
owner = parseFilesystemMutationLockOwner(JSON.parse(raw));
|
|
3511
3554
|
} catch {
|
|
3512
|
-
|
|
3555
|
+
owner = null;
|
|
3556
|
+
}
|
|
3557
|
+
return owner === null ? { state: "absent" } : { state: "record", owner };
|
|
3558
|
+
}
|
|
3559
|
+
async function resolveOwnerRecord(lockPath, started, waitMs, pollMs) {
|
|
3560
|
+
while (true) {
|
|
3561
|
+
const record = await readOwnerRecord(lockPath);
|
|
3562
|
+
if (record.state !== "unreadable" || Date.now() - started >= waitMs) return record;
|
|
3563
|
+
await delay(pollMs);
|
|
3513
3564
|
}
|
|
3514
3565
|
}
|
|
3566
|
+
async function readOwner(lockPath) {
|
|
3567
|
+
const record = await readOwnerRecord(lockPath);
|
|
3568
|
+
return record.state === "record" ? record.owner : null;
|
|
3569
|
+
}
|
|
3515
3570
|
function processExists(pid) {
|
|
3516
3571
|
try {
|
|
3517
3572
|
process.kill(pid, 0);
|
|
@@ -3533,7 +3588,7 @@ async function pathExists(candidate) {
|
|
|
3533
3588
|
throw err;
|
|
3534
3589
|
}
|
|
3535
3590
|
}
|
|
3536
|
-
async function quarantineStaleLock(lockPath, owner) {
|
|
3591
|
+
async function quarantineStaleLock(lockPath, owner, policy) {
|
|
3537
3592
|
if (owner.hostname !== hostname() || processExists(owner.pid)) return false;
|
|
3538
3593
|
const quarantinePath = staleLockQuarantinePath(lockPath, owner);
|
|
3539
3594
|
try {
|
|
@@ -3543,7 +3598,7 @@ async function quarantineStaleLock(lockPath, owner) {
|
|
|
3543
3598
|
const code = err.code;
|
|
3544
3599
|
if (code === "ENOENT") return false;
|
|
3545
3600
|
if (await pathExists(quarantinePath)) return false;
|
|
3546
|
-
if (
|
|
3601
|
+
if (policy.isDirectoryContentionError(err)) return false;
|
|
3547
3602
|
throw err;
|
|
3548
3603
|
}
|
|
3549
3604
|
}
|
|
@@ -3562,7 +3617,7 @@ function isPrivateFilesystemMutationLockRoot(facts) {
|
|
|
3562
3617
|
const unsafeMode = facts.enforcePrivateMode && (facts.mode & 511) !== 448;
|
|
3563
3618
|
return facts.directory && !facts.symbolicLink && !wrongOwner && !unsafeMode;
|
|
3564
3619
|
}
|
|
3565
|
-
async function ensurePrivateLockRoot(root) {
|
|
3620
|
+
async function ensurePrivateLockRoot(root, policy) {
|
|
3566
3621
|
try {
|
|
3567
3622
|
await fs.mkdir(root, { recursive: true, mode: 448 });
|
|
3568
3623
|
} catch (err) {
|
|
@@ -3576,7 +3631,7 @@ async function ensurePrivateLockRoot(root) {
|
|
|
3576
3631
|
ownerUid: stat.uid,
|
|
3577
3632
|
expectedUid: uid,
|
|
3578
3633
|
mode: stat.mode,
|
|
3579
|
-
enforcePrivateMode:
|
|
3634
|
+
enforcePrivateMode: policy.enforcePrivateMode
|
|
3580
3635
|
})) {
|
|
3581
3636
|
throw new FilesystemMutationLockError(
|
|
3582
3637
|
`refusing unsafe filesystem mutation lock root '${root}'; it must be a private directory owned by this user`,
|
|
@@ -3603,44 +3658,43 @@ async function resolvedPortableRoot(portableRoot) {
|
|
|
3603
3658
|
if (portableRoot === void 0) return void 0;
|
|
3604
3659
|
return fs.realpath(portableRoot).catch(() => path.resolve(portableRoot));
|
|
3605
3660
|
}
|
|
3606
|
-
async function selectLockRoot(options) {
|
|
3661
|
+
async function selectLockRoot(options, policy) {
|
|
3607
3662
|
const portableRoot = await resolvedPortableRoot(options.portableRoot);
|
|
3608
|
-
const lockRoot = options.lockRoot !== void 0 ? explicitFilesystemMutationLockRoot(options.lockRoot, portableRoot) : filesystemMutationLockRoot(portableRoot);
|
|
3609
|
-
await ensurePrivateLockRoot(lockRoot);
|
|
3663
|
+
const lockRoot = options.lockRoot !== void 0 ? explicitFilesystemMutationLockRoot(options.lockRoot, portableRoot) : filesystemMutationLockRoot(portableRoot, policy);
|
|
3664
|
+
await ensurePrivateLockRoot(lockRoot, policy);
|
|
3610
3665
|
return lockRoot;
|
|
3611
3666
|
}
|
|
3612
|
-
|
|
3613
|
-
async function classifyLockClaimFailure(error, lockPath) {
|
|
3667
|
+
async function classifyLockClaimFailure(error, lockPath, policy) {
|
|
3614
3668
|
const code = error.code;
|
|
3615
3669
|
if (code === "EEXIST") return "contention";
|
|
3616
|
-
if (
|
|
3670
|
+
if (!policy.isDirectoryContentionError(error)) return "terminal";
|
|
3617
3671
|
try {
|
|
3618
3672
|
await fs.lstat(lockPath);
|
|
3619
3673
|
return "contention";
|
|
3620
3674
|
} catch (probeError) {
|
|
3621
3675
|
const probeCode = probeError.code;
|
|
3622
|
-
return probeCode === "ENOENT" || probeCode === "ENOTDIR" ? "unwitnessed-
|
|
3676
|
+
return probeCode === "ENOENT" || probeCode === "ENOTDIR" ? "unwitnessed-contention-error" : "terminal";
|
|
3623
3677
|
}
|
|
3624
3678
|
}
|
|
3625
|
-
async function claimLockPath(lockPath, owner, waitMs, pollMs) {
|
|
3679
|
+
async function claimLockPath(lockPath, owner, waitMs, pollMs, policy) {
|
|
3626
3680
|
const started = owner.created_at_ms;
|
|
3627
|
-
let
|
|
3681
|
+
let unwitnessedRetryUsed = false;
|
|
3628
3682
|
while (true) {
|
|
3629
3683
|
try {
|
|
3630
3684
|
await fs.mkdir(lockPath, { mode: 448 });
|
|
3631
3685
|
} catch (err) {
|
|
3632
|
-
const failure = await classifyLockClaimFailure(err, lockPath);
|
|
3686
|
+
const failure = await classifyLockClaimFailure(err, lockPath, policy);
|
|
3633
3687
|
if (failure === "terminal") throw err;
|
|
3634
|
-
if (failure === "unwitnessed-
|
|
3635
|
-
if (
|
|
3636
|
-
|
|
3688
|
+
if (failure === "unwitnessed-contention-error") {
|
|
3689
|
+
if (unwitnessedRetryUsed) throw err;
|
|
3690
|
+
unwitnessedRetryUsed = true;
|
|
3637
3691
|
continue;
|
|
3638
3692
|
} else {
|
|
3639
|
-
|
|
3693
|
+
unwitnessedRetryUsed = false;
|
|
3640
3694
|
}
|
|
3641
3695
|
let existingOwner = await readOwner(lockPath);
|
|
3642
3696
|
if (existingOwner !== null) {
|
|
3643
|
-
if (await quarantineStaleLock(lockPath, existingOwner)) continue;
|
|
3697
|
+
if (await quarantineStaleLock(lockPath, existingOwner, policy)) continue;
|
|
3644
3698
|
existingOwner = await readOwner(lockPath);
|
|
3645
3699
|
}
|
|
3646
3700
|
if (Date.now() - started >= waitMs) throw timeoutError(lockPath, existingOwner, owner.target);
|
|
@@ -3659,24 +3713,89 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs) {
|
|
|
3659
3713
|
});
|
|
3660
3714
|
throw err;
|
|
3661
3715
|
}
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
if (
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
);
|
|
3716
|
+
let inFlight;
|
|
3717
|
+
return () => {
|
|
3718
|
+
if (inFlight) return inFlight;
|
|
3719
|
+
inFlight = (async () => {
|
|
3720
|
+
const started2 = Date.now();
|
|
3721
|
+
const current = await resolveOwnerRecord(lockPath, started2, waitMs, pollMs);
|
|
3722
|
+
if (current.state === "unreadable") throw unreadableOwnerRefusal(lockPath, current.error);
|
|
3723
|
+
if (current.state !== "record" || current.owner.token !== owner.token) {
|
|
3724
|
+
throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
|
|
3725
|
+
}
|
|
3726
|
+
await removeReleasedLock(lockPath, owner, started2, waitMs, pollMs, policy);
|
|
3727
|
+
})();
|
|
3728
|
+
return inFlight.finally(() => {
|
|
3729
|
+
inFlight = void 0;
|
|
3730
|
+
});
|
|
3731
|
+
};
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
function changedOwnerRefusal(lockPath, current) {
|
|
3735
|
+
return new FilesystemMutationLockError(
|
|
3736
|
+
`refusing to release filesystem mutation lock '${lockPath}' because its owner token changed; the mutation may have completed, inspect the lock before retrying.`,
|
|
3737
|
+
{ lockPath, owner: current, stale: false, malformed: current === null }
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
function unreadableOwnerRefusal(lockPath, error) {
|
|
3741
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3742
|
+
return new FilesystemMutationLockError(
|
|
3743
|
+
`refusing to release filesystem mutation lock '${lockPath}' because its owner record could not be read within the wait budget (${message}); the mutation may have completed, inspect the lock before retrying.`,
|
|
3744
|
+
{ lockPath, owner: null, stale: false, malformed: true }
|
|
3745
|
+
);
|
|
3746
|
+
}
|
|
3747
|
+
function releasedLockRemnantPath(lockPath, owner) {
|
|
3748
|
+
const tokenHash = createHash("sha256").update(owner.token).digest("hex");
|
|
3749
|
+
return `${lockPath}.released-${tokenHash}`;
|
|
3750
|
+
}
|
|
3751
|
+
function removalFailure(inspectPath, owner, err, attempts, detail) {
|
|
3752
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3753
|
+
const suffix = attempts > 1 ? ` after ${attempts} bounded attempts` : "";
|
|
3754
|
+
return new FilesystemMutationLockError(
|
|
3755
|
+
`mutation completed but ${detail}${suffix} (${message}); inspect the lock before retrying.`,
|
|
3756
|
+
{ lockPath: inspectPath, owner, stale: false, malformed: false }
|
|
3757
|
+
);
|
|
3758
|
+
}
|
|
3759
|
+
async function removeReleasedLock(lockPath, owner, started, waitMs, pollMs, policy) {
|
|
3760
|
+
const remnant = releasedLockRemnantPath(lockPath, owner);
|
|
3761
|
+
let attempts = 0;
|
|
3762
|
+
while (true) {
|
|
3763
|
+
try {
|
|
3764
|
+
await fs.rename(lockPath, remnant);
|
|
3765
|
+
break;
|
|
3766
|
+
} catch (err) {
|
|
3767
|
+
attempts += 1;
|
|
3768
|
+
if (attempts > 1 && err.code === "ENOENT") return;
|
|
3769
|
+
if (!policy.isDirectoryContentionError(err) || Date.now() - started >= waitMs) {
|
|
3770
|
+
throw removalFailure(lockPath, owner, err, attempts, `filesystem lock '${lockPath}' could not be removed`);
|
|
3669
3771
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3772
|
+
}
|
|
3773
|
+
await delay(pollMs);
|
|
3774
|
+
const current = await resolveOwnerRecord(lockPath, started, waitMs, pollMs);
|
|
3775
|
+
if (current.state === "record" && current.owner.token === owner.token) continue;
|
|
3776
|
+
if (current.state === "unreadable") throw unreadableOwnerRefusal(lockPath, current.error);
|
|
3777
|
+
if (current.state === "absent" && !await pathExists(lockPath)) return;
|
|
3778
|
+
throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
|
|
3779
|
+
}
|
|
3780
|
+
attempts = 0;
|
|
3781
|
+
while (true) {
|
|
3782
|
+
try {
|
|
3783
|
+
await fs.rm(remnant, { recursive: true, force: false });
|
|
3784
|
+
return;
|
|
3785
|
+
} catch (err) {
|
|
3786
|
+
attempts += 1;
|
|
3787
|
+
if (err.code === "ENOENT") return;
|
|
3788
|
+
if (!policy.isDirectoryContentionError(err) || Date.now() - started >= waitMs) {
|
|
3789
|
+
throw removalFailure(
|
|
3790
|
+
remnant,
|
|
3791
|
+
owner,
|
|
3792
|
+
err,
|
|
3793
|
+
attempts,
|
|
3794
|
+
`filesystem lock '${lockPath}' was released yet its remnant '${remnant}' could not be removed`
|
|
3677
3795
|
);
|
|
3678
3796
|
}
|
|
3679
|
-
}
|
|
3797
|
+
}
|
|
3798
|
+
await delay(pollMs);
|
|
3680
3799
|
}
|
|
3681
3800
|
}
|
|
3682
3801
|
function newOwner(target) {
|
|
@@ -3694,21 +3813,20 @@ function assertIdentityKey(key) {
|
|
|
3694
3813
|
}
|
|
3695
3814
|
async function acquireFilesystemIdentityLock(key, identity, options = {}) {
|
|
3696
3815
|
assertIdentityKey(key);
|
|
3816
|
+
const policy = captureFilesystemHostPolicy(options.hostPolicy);
|
|
3697
3817
|
const waitMs = positiveOption(options.waitMs, DEFAULT_WAIT_MS, "waitMs");
|
|
3698
3818
|
const pollMs = positiveOption(options.pollMs, DEFAULT_POLL_MS, "pollMs");
|
|
3699
3819
|
const owner = newOwner(identity);
|
|
3700
|
-
const lockRoot = await selectLockRoot(options);
|
|
3701
|
-
return claimLockPath(path.join(lockRoot, `${key}.lock`), owner, waitMs, pollMs);
|
|
3820
|
+
const lockRoot = await selectLockRoot(options, policy);
|
|
3821
|
+
return claimLockPath(path.join(lockRoot, `${key}.lock`), owner, waitMs, pollMs, policy);
|
|
3702
3822
|
}
|
|
3703
3823
|
|
|
3704
3824
|
// ../core/src/filesystem-identity.ts
|
|
3705
|
-
var WINDOWS_REPLACE_CONFLICT_CODES = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
3706
3825
|
async function replaceExistingLeaf(port, tmp, target, rel) {
|
|
3707
3826
|
try {
|
|
3708
3827
|
await port.rename(tmp, target);
|
|
3709
3828
|
} catch (err) {
|
|
3710
|
-
|
|
3711
|
-
if ((port.platform ?? process.platform) === "win32" && WINDOWS_REPLACE_CONFLICT_CODES.has(code)) {
|
|
3829
|
+
if (port.isReplacementConflict?.(err)) {
|
|
3712
3830
|
throw new ConcurrentReplacementError(rel, 1, 10);
|
|
3713
3831
|
}
|
|
3714
3832
|
throw err;
|
|
@@ -3875,7 +3993,7 @@ async function observeExact(port, root, rel, read) {
|
|
|
3875
3993
|
opened = await port.open(target);
|
|
3876
3994
|
} catch (err) {
|
|
3877
3995
|
if (isAbsentPathError(err)) return { state: "absent" };
|
|
3878
|
-
if (
|
|
3996
|
+
if (port.isTransientOpenError?.(err)) {
|
|
3879
3997
|
restarts = countRestart(restarts, rel);
|
|
3880
3998
|
continue;
|
|
3881
3999
|
}
|
|
@@ -3909,7 +4027,15 @@ async function probeExact(port, root, rel) {
|
|
|
3909
4027
|
restarts = countRestart(restarts, rel);
|
|
3910
4028
|
}
|
|
3911
4029
|
}
|
|
3912
|
-
var linkUnsupportedRoots = /* @__PURE__ */ new
|
|
4030
|
+
var linkUnsupportedRoots = /* @__PURE__ */ new WeakMap();
|
|
4031
|
+
function unsupportedLinkRoots(port) {
|
|
4032
|
+
let roots = linkUnsupportedRoots.get(port);
|
|
4033
|
+
if (!roots) {
|
|
4034
|
+
roots = /* @__PURE__ */ new Set();
|
|
4035
|
+
linkUnsupportedRoots.set(port, roots);
|
|
4036
|
+
}
|
|
4037
|
+
return roots;
|
|
4038
|
+
}
|
|
3913
4039
|
var queues = /* @__PURE__ */ new Map();
|
|
3914
4040
|
function enqueue(key, fn) {
|
|
3915
4041
|
const tail = queues.get(key) ?? Promise.resolve();
|
|
@@ -3991,7 +4117,7 @@ async function createExactLeaf(port, tmp, target, rel, leaf, rootResolved) {
|
|
|
3991
4117
|
return;
|
|
3992
4118
|
}
|
|
3993
4119
|
if (outcome === "unsupported") {
|
|
3994
|
-
|
|
4120
|
+
unsupportedLinkRoots(port).add(rootResolved);
|
|
3995
4121
|
await port.rename(tmp, target);
|
|
3996
4122
|
return;
|
|
3997
4123
|
}
|
|
@@ -4027,7 +4153,7 @@ async function mutateExact(port, root, rel, body) {
|
|
|
4027
4153
|
await replaceExistingLeaf(port, tmp, target, rel);
|
|
4028
4154
|
return;
|
|
4029
4155
|
}
|
|
4030
|
-
if (
|
|
4156
|
+
if (unsupportedLinkRoots(port).has(rootResolved)) {
|
|
4031
4157
|
await port.rename(tmp, target);
|
|
4032
4158
|
return;
|
|
4033
4159
|
}
|
|
@@ -4054,80 +4180,84 @@ function kindOf(stats) {
|
|
|
4054
4180
|
if (stats.isFile()) return "file";
|
|
4055
4181
|
return "other";
|
|
4056
4182
|
}
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4183
|
+
function createNodeFilesystemIdentityPort(hostPolicy) {
|
|
4184
|
+
const policy = captureFilesystemHostPolicy(hostPolicy);
|
|
4185
|
+
return Object.freeze({
|
|
4186
|
+
isTransientOpenError: policy.isTransientOpenError,
|
|
4187
|
+
isReplacementConflict: policy.isReplacementConflict,
|
|
4188
|
+
async probe(target) {
|
|
4189
|
+
let lstats;
|
|
4190
|
+
try {
|
|
4191
|
+
lstats = await fs2.lstat(target);
|
|
4192
|
+
} catch (err) {
|
|
4193
|
+
if (isAbsentPathError(err)) return null;
|
|
4194
|
+
throw err;
|
|
4195
|
+
}
|
|
4196
|
+
return { kind: kindOf(lstats), dev: lstats.dev, ino: lstats.ino };
|
|
4197
|
+
},
|
|
4198
|
+
async entries(dir) {
|
|
4199
|
+
try {
|
|
4200
|
+
const dirents = await fs2.readdir(dir, { withFileTypes: true });
|
|
4201
|
+
return dirents.map((dirent) => ({ name: dirent.name, kind: kindOf(dirent) }));
|
|
4202
|
+
} catch (err) {
|
|
4203
|
+
if (isAbsentPathError(err)) return null;
|
|
4204
|
+
throw err;
|
|
4205
|
+
}
|
|
4206
|
+
},
|
|
4207
|
+
async open(target) {
|
|
4208
|
+
const handle = await fs2.open(target, "r");
|
|
4209
|
+
try {
|
|
4210
|
+
const stats = await handle.stat();
|
|
4211
|
+
return { handle, dev: stats.dev, ino: stats.ino };
|
|
4212
|
+
} catch (err) {
|
|
4213
|
+
await handle.close().catch(() => {
|
|
4214
|
+
});
|
|
4215
|
+
throw err;
|
|
4216
|
+
}
|
|
4217
|
+
},
|
|
4218
|
+
readAll(handle) {
|
|
4219
|
+
return handle.readFile();
|
|
4220
|
+
},
|
|
4221
|
+
close(handle) {
|
|
4222
|
+
return handle.close();
|
|
4223
|
+
},
|
|
4224
|
+
async stat(target) {
|
|
4225
|
+
return { mtime: (await fs2.stat(target)).mtime };
|
|
4226
|
+
},
|
|
4227
|
+
async mkdir(dir) {
|
|
4228
|
+
try {
|
|
4229
|
+
await fs2.mkdir(dir);
|
|
4230
|
+
return "created";
|
|
4231
|
+
} catch (err) {
|
|
4232
|
+
if (err.code === "EEXIST") return "exists";
|
|
4233
|
+
throw err;
|
|
4234
|
+
}
|
|
4235
|
+
},
|
|
4236
|
+
async writeTemp(dir, name, bytes) {
|
|
4237
|
+
await fs2.writeFile(path2.join(dir, name), bytes);
|
|
4238
|
+
},
|
|
4239
|
+
async link(from, to) {
|
|
4240
|
+
try {
|
|
4241
|
+
await fs2.link(from, to);
|
|
4242
|
+
return "linked";
|
|
4243
|
+
} catch (err) {
|
|
4244
|
+
const code = err.code;
|
|
4245
|
+
if (code === "EEXIST") return "exists";
|
|
4246
|
+
if (code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EXDEV") return "unsupported";
|
|
4247
|
+
throw err;
|
|
4248
|
+
}
|
|
4249
|
+
},
|
|
4250
|
+
async rename(from, to) {
|
|
4251
|
+
await fs2.rename(from, to);
|
|
4252
|
+
},
|
|
4253
|
+
async unlink(target) {
|
|
4254
|
+
await fs2.unlink(target);
|
|
4255
|
+
},
|
|
4256
|
+
claim(key, identity) {
|
|
4257
|
+
return acquireFilesystemIdentityLock(key, `${identity.root}:${identity.rel}`, { portableRoot: identity.root, hostPolicy: policy });
|
|
4119
4258
|
}
|
|
4120
|
-
}
|
|
4121
|
-
|
|
4122
|
-
await fs2.rename(from, to);
|
|
4123
|
-
},
|
|
4124
|
-
async unlink(target) {
|
|
4125
|
-
await fs2.unlink(target);
|
|
4126
|
-
},
|
|
4127
|
-
claim(key, identity) {
|
|
4128
|
-
return acquireFilesystemIdentityLock(key, `${identity.root}:${identity.rel}`, { portableRoot: identity.root });
|
|
4129
|
-
}
|
|
4130
|
-
});
|
|
4259
|
+
});
|
|
4260
|
+
}
|
|
4131
4261
|
|
|
4132
4262
|
// ../core/src/sha256.ts
|
|
4133
4263
|
var K = new Uint32Array([
|
|
@@ -4286,6 +4416,9 @@ function sha256HexOfUtf8(input) {
|
|
|
4286
4416
|
}
|
|
4287
4417
|
|
|
4288
4418
|
// ../core/src/version-transport.ts
|
|
4419
|
+
function isContentVersion(value) {
|
|
4420
|
+
return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
|
|
4421
|
+
}
|
|
4289
4422
|
var VersionConflict = class extends Error {
|
|
4290
4423
|
name = "VersionConflict";
|
|
4291
4424
|
id;
|
|
@@ -4317,7 +4450,7 @@ function defaultActor() {
|
|
|
4317
4450
|
}
|
|
4318
4451
|
|
|
4319
4452
|
// ../core/src/backend.ts
|
|
4320
|
-
var
|
|
4453
|
+
var publicationContexts = /* @__PURE__ */ new WeakMap();
|
|
4321
4454
|
function firstString(...vals) {
|
|
4322
4455
|
for (const v of vals) {
|
|
4323
4456
|
if (typeof v === "string" && v.trim() !== "") return v;
|
|
@@ -4336,22 +4469,22 @@ function notFound(root, rel) {
|
|
|
4336
4469
|
err.path = target;
|
|
4337
4470
|
return err;
|
|
4338
4471
|
}
|
|
4339
|
-
async function readBytes(handle) {
|
|
4472
|
+
async function readBytes(port, handle) {
|
|
4340
4473
|
try {
|
|
4341
|
-
return await
|
|
4474
|
+
return await port.readAll(handle);
|
|
4342
4475
|
} catch (err) {
|
|
4343
4476
|
if (isAbsentFileError(err)) return null;
|
|
4344
4477
|
throw err;
|
|
4345
4478
|
}
|
|
4346
4479
|
}
|
|
4347
|
-
async function walkFiles(root, keep, sub = "") {
|
|
4348
|
-
const entries = await
|
|
4480
|
+
async function walkFiles(port, root, keep, sub = "") {
|
|
4481
|
+
const entries = await port.entries(path3.join(root, sub)) ?? [];
|
|
4349
4482
|
const out = [];
|
|
4350
4483
|
for (const entry of entries) {
|
|
4351
4484
|
if (entry.name.startsWith(".")) continue;
|
|
4352
4485
|
const rel = sub === "" ? entry.name : `${sub}/${entry.name}`;
|
|
4353
4486
|
if (entry.kind === "directory") {
|
|
4354
|
-
out.push(...await walkFiles(root, keep, rel));
|
|
4487
|
+
out.push(...await walkFiles(port, root, keep, rel));
|
|
4355
4488
|
} else if (entry.kind === "file" && keep(entry.name)) {
|
|
4356
4489
|
out.push(rel);
|
|
4357
4490
|
}
|
|
@@ -4359,19 +4492,20 @@ async function walkFiles(root, keep, sub = "") {
|
|
|
4359
4492
|
return out;
|
|
4360
4493
|
}
|
|
4361
4494
|
function reservedPath(dir, name) {
|
|
4362
|
-
|
|
4363
|
-
return d === "" ? name : `${d}/${name}`;
|
|
4495
|
+
return dir === "" ? name : `${dir}/${name}`;
|
|
4364
4496
|
}
|
|
4365
4497
|
var FilesystemBackend = class _FilesystemBackend {
|
|
4366
4498
|
#root;
|
|
4499
|
+
#port;
|
|
4367
4500
|
/**
|
|
4368
4501
|
* The root is resolved once here: a relative root would otherwise re-resolve against the
|
|
4369
4502
|
* process's current directory on every operation, so a later `chdir` would silently move the
|
|
4370
4503
|
* bundle and derive a different identity key.
|
|
4371
4504
|
*/
|
|
4372
|
-
constructor(root) {
|
|
4505
|
+
constructor(root, options = {}) {
|
|
4506
|
+
this.#port = createNodeFilesystemIdentityPort(options.hostPolicy);
|
|
4373
4507
|
this.#root = path3.resolve(root);
|
|
4374
|
-
|
|
4508
|
+
publicationContexts.set(this, { root: this.#root, port: this.#port });
|
|
4375
4509
|
}
|
|
4376
4510
|
async read(id) {
|
|
4377
4511
|
return this.#readWithEdition(id, () => this.#readEdition());
|
|
@@ -4387,7 +4521,7 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4387
4521
|
async #readWithEdition(id, edition) {
|
|
4388
4522
|
assertSafeConceptId(id);
|
|
4389
4523
|
const rel = pathFromConceptId(id);
|
|
4390
|
-
const observed = await observeExact(
|
|
4524
|
+
const observed = await observeExact(this.#port, this.#root, rel, (handle) => readBytes(this.#port, handle));
|
|
4391
4525
|
if (observed.state === "absent") throw notFound(this.#root, rel);
|
|
4392
4526
|
const raw = observed.value.toString("utf8");
|
|
4393
4527
|
const { frontmatter: frontmatter2, body } = parseMarkdown(raw, rel, { okfVersion: await edition() });
|
|
@@ -4409,7 +4543,7 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4409
4543
|
assertSafeConceptId(id);
|
|
4410
4544
|
const raw = stringifyDoc(doc.frontmatter, doc.body ?? "");
|
|
4411
4545
|
const rel = pathFromConceptId(id);
|
|
4412
|
-
return mutateExact(
|
|
4546
|
+
return mutateExact(this.#port, this.#root, rel, async (target) => {
|
|
4413
4547
|
if (options.expectedVersion !== void 0) {
|
|
4414
4548
|
const bytes = await target.current();
|
|
4415
4549
|
const current = bytes === null ? null : versionOfBytes(bytes.toString("utf8"));
|
|
@@ -4423,7 +4557,7 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4423
4557
|
}
|
|
4424
4558
|
async delete(id, options = {}) {
|
|
4425
4559
|
assertSafeConceptId(id);
|
|
4426
|
-
return mutateExact(
|
|
4560
|
+
return mutateExact(this.#port, this.#root, pathFromConceptId(id), async (target) => {
|
|
4427
4561
|
const bytes = await target.current();
|
|
4428
4562
|
if (bytes === null) return false;
|
|
4429
4563
|
const current = versionOfBytes(bytes.toString("utf8"));
|
|
@@ -4437,10 +4571,10 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4437
4571
|
async versions(id) {
|
|
4438
4572
|
assertSafeConceptId(id);
|
|
4439
4573
|
const rel = pathFromConceptId(id);
|
|
4440
|
-
const observed = await observeExact(
|
|
4574
|
+
const observed = await observeExact(this.#port, this.#root, rel, async (handle, target) => {
|
|
4441
4575
|
try {
|
|
4442
|
-
const bytes = await
|
|
4443
|
-
const { mtime: mtime2 } = await
|
|
4576
|
+
const bytes = await this.#port.readAll(handle);
|
|
4577
|
+
const { mtime: mtime2 } = await this.#port.stat(target);
|
|
4444
4578
|
return { raw: bytes.toString("utf8"), mtime: mtime2 };
|
|
4445
4579
|
} catch {
|
|
4446
4580
|
return null;
|
|
@@ -4455,10 +4589,10 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4455
4589
|
}
|
|
4456
4590
|
async exists(id) {
|
|
4457
4591
|
assertSafeConceptId(id);
|
|
4458
|
-
return (await probeExact(
|
|
4592
|
+
return (await probeExact(this.#port, this.#root, pathFromConceptId(id))).state === "exact";
|
|
4459
4593
|
}
|
|
4460
4594
|
async list(prefix) {
|
|
4461
|
-
const files = await walkFiles(this.#root, (name) => name.endsWith(".md"));
|
|
4595
|
+
const files = await walkFiles(this.#port, this.#root, (name) => name.endsWith(".md"));
|
|
4462
4596
|
const ids = [];
|
|
4463
4597
|
for (const rel of files) {
|
|
4464
4598
|
if (isReservedFile(rel)) continue;
|
|
@@ -4472,20 +4606,22 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4472
4606
|
if (prefix && !id.startsWith(prefix)) continue;
|
|
4473
4607
|
ids.push(id);
|
|
4474
4608
|
}
|
|
4475
|
-
ids.sort(
|
|
4609
|
+
ids.sort(compareStorageKeys);
|
|
4476
4610
|
return ids;
|
|
4477
4611
|
}
|
|
4478
4612
|
async readReserved(dir, name) {
|
|
4479
4613
|
assertSafeReservedDir(dir);
|
|
4480
|
-
|
|
4614
|
+
assertSafeReservedFilename(name);
|
|
4615
|
+
const observed = await observeExact(this.#port, this.#root, reservedPath(dir, name), (handle) => readBytes(this.#port, handle));
|
|
4481
4616
|
if (observed.state === "absent") return null;
|
|
4482
4617
|
const content = observed.value.toString("utf8");
|
|
4483
4618
|
return { content, version: versionOfBytes(content) };
|
|
4484
4619
|
}
|
|
4485
4620
|
async writeReserved(dir, name, content, options = {}) {
|
|
4486
4621
|
assertSafeReservedDir(dir);
|
|
4622
|
+
assertSafeReservedFilename(name);
|
|
4487
4623
|
const rel = reservedPath(dir, name);
|
|
4488
|
-
return mutateExact(
|
|
4624
|
+
return mutateExact(this.#port, this.#root, rel, async (target) => {
|
|
4489
4625
|
if (options.expectedVersion !== void 0) {
|
|
4490
4626
|
const bytes = await target.current();
|
|
4491
4627
|
const current = bytes === null ? null : versionOfBytes(bytes.toString("utf8"));
|
|
@@ -4500,14 +4636,14 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4500
4636
|
// ── blobs: opaque bytes + a content-type ──────────────────────────────────
|
|
4501
4637
|
async readBlob(key) {
|
|
4502
4638
|
assertSafeBlobKey(key);
|
|
4503
|
-
const observed = await observeExact(
|
|
4639
|
+
const observed = await observeExact(this.#port, this.#root, key, (handle) => readBytes(this.#port, handle));
|
|
4504
4640
|
if (observed.state === "absent") return null;
|
|
4505
4641
|
const bytes = observed.value;
|
|
4506
4642
|
return { bytes, contentType: resolveContentType(key), version: blobVersion(bytes) };
|
|
4507
4643
|
}
|
|
4508
4644
|
async writeBlob(key, bytes, _contentType, options = {}) {
|
|
4509
4645
|
assertSafeBlobKey(key);
|
|
4510
|
-
return mutateExact(
|
|
4646
|
+
return mutateExact(this.#port, this.#root, key, async (target) => {
|
|
4511
4647
|
if (options.expectedVersion !== void 0) {
|
|
4512
4648
|
const existing = await target.current();
|
|
4513
4649
|
const current = existing === null ? null : blobVersion(existing);
|
|
@@ -4521,7 +4657,7 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4521
4657
|
}
|
|
4522
4658
|
async deleteBlob(key, options = {}) {
|
|
4523
4659
|
assertSafeBlobKey(key);
|
|
4524
|
-
return mutateExact(
|
|
4660
|
+
return mutateExact(this.#port, this.#root, key, async (target) => {
|
|
4525
4661
|
const existing = await target.current();
|
|
4526
4662
|
if (existing === null) return false;
|
|
4527
4663
|
const current = blobVersion(existing);
|
|
@@ -4534,14 +4670,14 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4534
4670
|
}
|
|
4535
4671
|
async existsBlob(key) {
|
|
4536
4672
|
assertSafeBlobKey(key);
|
|
4537
|
-
const observed = await probeExact(
|
|
4673
|
+
const observed = await probeExact(this.#port, this.#root, key);
|
|
4538
4674
|
return observed.state === "exact" && observed.value.kind === "file";
|
|
4539
4675
|
}
|
|
4540
4676
|
async listBlobs(prefix) {
|
|
4541
|
-
const files = await walkFiles(this.#root, (name) => !name.toLowerCase().endsWith(".md"));
|
|
4677
|
+
const files = await walkFiles(this.#port, this.#root, (name) => !name.toLowerCase().endsWith(".md"));
|
|
4542
4678
|
const keys = files.map(toPosix);
|
|
4543
4679
|
const filtered = prefix ? keys.filter((k) => k.startsWith(prefix)) : keys;
|
|
4544
|
-
filtered.sort(
|
|
4680
|
+
filtered.sort(compareStorageKeys);
|
|
4545
4681
|
return filtered;
|
|
4546
4682
|
}
|
|
4547
4683
|
capabilities() {
|
|
@@ -4592,7 +4728,7 @@ setDefaultBackendFactory((root) => new FilesystemBackend(root));
|
|
|
4592
4728
|
var CONVENTIONS_PREFIX = "conventions/";
|
|
4593
4729
|
var CONVENTION_TYPE = "Convention";
|
|
4594
4730
|
function buildKindRegistry(docs, initialWarnings = [], options = {}) {
|
|
4595
|
-
const
|
|
4731
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
4596
4732
|
const warnings = [...initialWarnings];
|
|
4597
4733
|
for (const doc of [...docs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
4598
4734
|
const parsed = parseConventionDoc(doc, options);
|
|
@@ -4615,10 +4751,10 @@ function buildKindRegistry(docs, initialWarnings = [], options = {}) {
|
|
|
4615
4751
|
severity: "warning"
|
|
4616
4752
|
});
|
|
4617
4753
|
}
|
|
4618
|
-
if (
|
|
4754
|
+
if (accepted.has(kind.governs)) {
|
|
4619
4755
|
warnings.push({
|
|
4620
4756
|
code: "KIND_DUPLICATE_GOVERNS",
|
|
4621
|
-
message: `duplicate kind convention for '${kind.governs}': '${doc.id}' ignored, keeping the first-declared '${
|
|
4757
|
+
message: `duplicate kind convention for '${kind.governs}': '${doc.id}' ignored, keeping the first-declared '${accepted.get(kind.governs).id}'.`,
|
|
4622
4758
|
field: kind.governs,
|
|
4623
4759
|
severity: "warning"
|
|
4624
4760
|
});
|
|
@@ -4632,10 +4768,18 @@ function buildKindRegistry(docs, initialWarnings = [], options = {}) {
|
|
|
4632
4768
|
severity: "warning"
|
|
4633
4769
|
});
|
|
4634
4770
|
}
|
|
4635
|
-
|
|
4771
|
+
accepted.set(kind.governs, kind);
|
|
4636
4772
|
}
|
|
4773
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
4774
|
+
for (const kind of [...accepted.values()].sort(compareReadingOrder)) kinds.set(kind.governs, kind);
|
|
4637
4775
|
return { kinds, warnings };
|
|
4638
4776
|
}
|
|
4777
|
+
function compareReadingOrder(a, b) {
|
|
4778
|
+
if (a.order !== void 0 && b.order !== void 0 && a.order !== b.order) return a.order - b.order;
|
|
4779
|
+
if (a.order !== void 0 && b.order === void 0) return -1;
|
|
4780
|
+
if (a.order === void 0 && b.order !== void 0) return 1;
|
|
4781
|
+
return a.id.localeCompare(b.id);
|
|
4782
|
+
}
|
|
4639
4783
|
var PROGRESS_STATUS_FIELD = "progress_status";
|
|
4640
4784
|
var SUPERBEE_PROGRESS_STATUS_FIELD = "superbee_progress_status";
|
|
4641
4785
|
function progressStatusStorageField(okfVersion) {
|
|
@@ -5104,6 +5248,19 @@ function parseConventionDoc(doc, options = {}) {
|
|
|
5104
5248
|
const path4 = typeof fm.path === "string" && fm.path.trim() !== "" ? fm.path.trim() : void 0;
|
|
5105
5249
|
const freshnessHorizon = typeof fm.freshness_horizon === "string" && fm.freshness_horizon.trim() !== "" ? fm.freshness_horizon.trim() : void 0;
|
|
5106
5250
|
const browseCollapsed = fm.browse_collapsed === true ? true : void 0;
|
|
5251
|
+
let order;
|
|
5252
|
+
if (fm.order !== void 0) {
|
|
5253
|
+
if (typeof fm.order === "number" && Number.isFinite(fm.order)) {
|
|
5254
|
+
order = fm.order;
|
|
5255
|
+
} else {
|
|
5256
|
+
warnings.push({
|
|
5257
|
+
code: "KIND_CONVENTION_BAD_SHAPE",
|
|
5258
|
+
message: `kind convention '${doc.id}' has an invalid 'order' (${describeShape(fm.order)}; expected a finite number such as 10); ignoring it.`,
|
|
5259
|
+
field: "order",
|
|
5260
|
+
severity: "warning"
|
|
5261
|
+
});
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5107
5264
|
const kind = {
|
|
5108
5265
|
id: doc.id,
|
|
5109
5266
|
title,
|
|
@@ -5118,6 +5275,7 @@ function parseConventionDoc(doc, options = {}) {
|
|
|
5118
5275
|
if (sections && sections.length > 0) kind.sections = sections;
|
|
5119
5276
|
if (freshnessHorizon !== void 0) kind.freshnessHorizon = freshnessHorizon;
|
|
5120
5277
|
if (browseCollapsed !== void 0) kind.browseCollapsed = browseCollapsed;
|
|
5278
|
+
if (order !== void 0) kind.order = order;
|
|
5121
5279
|
if (claim !== void 0) kind.claim = claim;
|
|
5122
5280
|
return {
|
|
5123
5281
|
ok: true,
|
|
@@ -5259,7 +5417,7 @@ function isAnyEntryKey(entry) {
|
|
|
5259
5417
|
return isPageEntryKey(entry) || isViewEntryKey(entry);
|
|
5260
5418
|
}
|
|
5261
5419
|
function isViewEntryVersion(value) {
|
|
5262
|
-
return
|
|
5420
|
+
return isContentVersion(value);
|
|
5263
5421
|
}
|
|
5264
5422
|
function parseRegistration(id, frontmatter2) {
|
|
5265
5423
|
if (!isAnyRegistryId(id) || !isPageTypeName(frontmatter2.type) || !isAnyEntryKey(frontmatter2.entry)) return null;
|
|
@@ -5286,9 +5444,45 @@ var MAX_QUERY_ROWS = 500;
|
|
|
5286
5444
|
var MAX_EDGE_ROWS = 1e3;
|
|
5287
5445
|
var MAX_DOCUMENT_BODY_BYTES = 1024 * 1024;
|
|
5288
5446
|
var MAX_REPLY_BYTES = 2 * 1024 * 1024;
|
|
5447
|
+
var GRAPH_MAX_DOCUMENTS = 1e3;
|
|
5448
|
+
var GRAPH_MAX_RELATIONSHIPS = 1e4;
|
|
5289
5449
|
var MAX_CHANGE_ROWS = 100;
|
|
5290
5450
|
var MAX_CHANGE_BYTES = 256 * 1024;
|
|
5291
5451
|
var MAX_SUBSCRIPTION_HEADS = 1e4;
|
|
5452
|
+
var MAX_HOST_CAPABILITY_BYTES = 128;
|
|
5453
|
+
var MAX_HOST_REQUEST_BYTES = 64 * 1024;
|
|
5454
|
+
var HOST_CAPABILITY_NAME = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
|
|
5455
|
+
var BRIDGE_SERVICE_LIMITS = Object.freeze({
|
|
5456
|
+
query: MAX_QUERY_ROWS,
|
|
5457
|
+
edges: MAX_EDGE_ROWS,
|
|
5458
|
+
graphDocuments: GRAPH_MAX_DOCUMENTS,
|
|
5459
|
+
graphRelationships: GRAPH_MAX_RELATIONSHIPS,
|
|
5460
|
+
replyBytes: MAX_REPLY_BYTES
|
|
5461
|
+
});
|
|
5462
|
+
var BRIDGE_HOST_CAPABILITIES = Object.freeze({
|
|
5463
|
+
queryKindProjection: "query.kind-projection",
|
|
5464
|
+
queryFieldOr: "query.field-or",
|
|
5465
|
+
queryOpen: "query.open",
|
|
5466
|
+
queryCount: "query.count",
|
|
5467
|
+
edges: "edges",
|
|
5468
|
+
renderDocument: "render-document",
|
|
5469
|
+
openPage: "open-page",
|
|
5470
|
+
subscribeDeltas: "subscribe-deltas",
|
|
5471
|
+
graph: "graph",
|
|
5472
|
+
graphModel: "graph.model",
|
|
5473
|
+
recordOpen: "record.open",
|
|
5474
|
+
frameResize: "frame.resize"
|
|
5475
|
+
});
|
|
5476
|
+
var BRIDGE_SERVICE_CAPABILITIES = Object.freeze([
|
|
5477
|
+
BRIDGE_HOST_CAPABILITIES.queryKindProjection,
|
|
5478
|
+
BRIDGE_HOST_CAPABILITIES.queryFieldOr,
|
|
5479
|
+
BRIDGE_HOST_CAPABILITIES.queryOpen,
|
|
5480
|
+
BRIDGE_HOST_CAPABILITIES.queryCount,
|
|
5481
|
+
BRIDGE_HOST_CAPABILITIES.edges,
|
|
5482
|
+
BRIDGE_HOST_CAPABILITIES.graph,
|
|
5483
|
+
BRIDGE_HOST_CAPABILITIES.renderDocument
|
|
5484
|
+
]);
|
|
5485
|
+
var V0_REQUEST_TYPES = /* @__PURE__ */ new Set(["hello", "query", "read", "render-document", "edges", "graph", "subscribe", "host", "open-page"]);
|
|
5292
5486
|
function isPlainRecord(value) {
|
|
5293
5487
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
5294
5488
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -5421,6 +5615,45 @@ function parseBridgeRequest(value) {
|
|
|
5421
5615
|
const params = normalizeEdgeParams(value.params);
|
|
5422
5616
|
return params ? { bridge: BRIDGE_PROTOCOL, type: "edges", id, params } : null;
|
|
5423
5617
|
}
|
|
5618
|
+
if (value.type === "graph") {
|
|
5619
|
+
const expected = value.includeBodies === void 0 ? ["bridge", "type", "id"] : ["bridge", "type", "id", "includeBodies"];
|
|
5620
|
+
if (!exactKeys(value, expected)) return null;
|
|
5621
|
+
if (value.includeBodies !== void 0 && typeof value.includeBodies !== "boolean") return null;
|
|
5622
|
+
return { bridge: BRIDGE_PROTOCOL, type: "graph", id, includeBodies: value.includeBodies === true };
|
|
5623
|
+
}
|
|
5624
|
+
if (value.type === "host") {
|
|
5625
|
+
const expected = value.input === void 0 ? ["bridge", "type", "id", "capability"] : ["bridge", "type", "id", "capability", "input"];
|
|
5626
|
+
if (!exactKeys(value, expected)) return null;
|
|
5627
|
+
const capability = boundedString(value.capability, MAX_HOST_CAPABILITY_BYTES);
|
|
5628
|
+
if (!capability || !HOST_CAPABILITY_NAME.test(capability)) return null;
|
|
5629
|
+
if (value.input !== void 0 && !isPlainRecord(value.input)) return null;
|
|
5630
|
+
let bytes;
|
|
5631
|
+
try {
|
|
5632
|
+
bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
5633
|
+
} catch {
|
|
5634
|
+
return null;
|
|
5635
|
+
}
|
|
5636
|
+
if (bytes > MAX_HOST_REQUEST_BYTES) return null;
|
|
5637
|
+
return {
|
|
5638
|
+
bridge: BRIDGE_PROTOCOL,
|
|
5639
|
+
type: "host",
|
|
5640
|
+
id,
|
|
5641
|
+
capability,
|
|
5642
|
+
...value.input === void 0 ? {} : { input: value.input }
|
|
5643
|
+
};
|
|
5644
|
+
}
|
|
5645
|
+
return null;
|
|
5646
|
+
}
|
|
5647
|
+
function unsupportedRequest(value) {
|
|
5648
|
+
if (!isPlainRecord(value) || typeof value.type !== "string") return null;
|
|
5649
|
+
if (value.bridge === BRIDGE_PROTOCOL) {
|
|
5650
|
+
return V0_REQUEST_TYPES.has(value.type) ? null : { bridge: BRIDGE_PROTOCOL, id: requestId(value.id) ?? void 0 };
|
|
5651
|
+
}
|
|
5652
|
+
if (value.bridge === ACTION_BRIDGE_PROTOCOL) {
|
|
5653
|
+
if (value.type === "read-versioned") return null;
|
|
5654
|
+
const id = value.type === "action.propose" ? value.requestId : value.id;
|
|
5655
|
+
return { bridge: ACTION_BRIDGE_PROTOCOL, id: requestId(id) ?? void 0 };
|
|
5656
|
+
}
|
|
5424
5657
|
return null;
|
|
5425
5658
|
}
|
|
5426
5659
|
function ok(id, bridge, type, result) {
|
|
@@ -5445,6 +5678,17 @@ var BridgeService = class {
|
|
|
5445
5678
|
async handle(launchId, rawRequest) {
|
|
5446
5679
|
const request = parseBridgeRequest(rawRequest);
|
|
5447
5680
|
if (!request) {
|
|
5681
|
+
const unsupported = unsupportedRequest(rawRequest);
|
|
5682
|
+
if (unsupported) {
|
|
5683
|
+
return {
|
|
5684
|
+
reply: fail(
|
|
5685
|
+
unsupported.id,
|
|
5686
|
+
unsupported.bridge,
|
|
5687
|
+
"FORBIDDEN",
|
|
5688
|
+
"this host does not offer the requested bridge operation"
|
|
5689
|
+
)
|
|
5690
|
+
};
|
|
5691
|
+
}
|
|
5448
5692
|
return {
|
|
5449
5693
|
reply: fail(
|
|
5450
5694
|
invalidV0RequestId(rawRequest),
|
|
@@ -5454,16 +5698,6 @@ var BridgeService = class {
|
|
|
5454
5698
|
)
|
|
5455
5699
|
};
|
|
5456
5700
|
}
|
|
5457
|
-
if (request.bridge === ACTION_BRIDGE_PROTOCOL && this.options.allowActionProtocol === false) {
|
|
5458
|
-
return {
|
|
5459
|
-
reply: fail(
|
|
5460
|
-
request.id,
|
|
5461
|
-
request.bridge,
|
|
5462
|
-
"FORBIDDEN",
|
|
5463
|
-
"this host admits only the read-only v0 View bridge"
|
|
5464
|
-
)
|
|
5465
|
-
};
|
|
5466
|
-
}
|
|
5467
5701
|
const dataBearing = request.type !== "open-page";
|
|
5468
5702
|
const before = await this.options.launches.resolve(launchId, dataBearing);
|
|
5469
5703
|
if (!before) {
|
|
@@ -5569,6 +5803,73 @@ var BridgeService = class {
|
|
|
5569
5803
|
[...rows].sort((a, b) => a.id.localeCompare(b.id)).map((row) => [row.id, row.version])
|
|
5570
5804
|
);
|
|
5571
5805
|
}
|
|
5806
|
+
/**
|
|
5807
|
+
* Whole-bundle projection for graph-shaped Views. The documents come from one head scan and
|
|
5808
|
+
* the relationships from `queryEdges`, which is a second full-bundle scan; the two scans are
|
|
5809
|
+
* accepted because the reply is bounded by the exported graph limits and the host never
|
|
5810
|
+
* caches bundle state on a View's behalf. No `model` or `definitions` are answered here: the
|
|
5811
|
+
* owner of that shape is undecided, and a host that has one declares the `graph.model`
|
|
5812
|
+
* capability before adding them.
|
|
5813
|
+
*/
|
|
5814
|
+
async graph(launch, request) {
|
|
5815
|
+
const heads = await queryHeads2(this.options.bundle, {});
|
|
5816
|
+
if (heads.length > GRAPH_MAX_DOCUMENTS) {
|
|
5817
|
+
return {
|
|
5818
|
+
reply: fail(request.id, request.bridge, "TOO_LARGE", `the graph exceeded ${GRAPH_MAX_DOCUMENTS} documents`)
|
|
5819
|
+
};
|
|
5820
|
+
}
|
|
5821
|
+
const [registry, declaredOkfVersion, edges] = await Promise.all([
|
|
5822
|
+
loadKinds(this.options.bundle),
|
|
5823
|
+
readBundleOkfVersion2(this.options.bundle),
|
|
5824
|
+
queryEdges2(this.options.bundle, {})
|
|
5825
|
+
]);
|
|
5826
|
+
if (edges.length > GRAPH_MAX_RELATIONSHIPS) {
|
|
5827
|
+
return {
|
|
5828
|
+
reply: fail(request.id, request.bridge, "TOO_LARGE", `the graph exceeded ${GRAPH_MAX_RELATIONSHIPS} relationships`)
|
|
5829
|
+
};
|
|
5830
|
+
}
|
|
5831
|
+
const okfVersion = declaredOkfVersion ?? "0.1";
|
|
5832
|
+
const includeBodies = request.includeBodies && (launch.capability === "bundle-read" || launch.capability === "bundle-propose");
|
|
5833
|
+
const documents = [];
|
|
5834
|
+
for (const head of heads) {
|
|
5835
|
+
const source = includeBodies ? await readDocVersioned2(this.options.bundle, head.id).then((result) => ({
|
|
5836
|
+
version: result.version,
|
|
5837
|
+
frontmatter: result.doc.frontmatter,
|
|
5838
|
+
body: result.doc.body
|
|
5839
|
+
})) : { version: head.version, frontmatter: head.frontmatter };
|
|
5840
|
+
if (source.body !== void 0 && Buffer.byteLength(source.body, "utf8") > MAX_DOCUMENT_BODY_BYTES) {
|
|
5841
|
+
return { reply: fail(request.id, request.bridge, "TOO_LARGE", "a document body exceeded the 1 MiB View limit") };
|
|
5842
|
+
}
|
|
5843
|
+
const kind = registry.kinds.get(String(source.frontmatter.type ?? ""));
|
|
5844
|
+
const frontmatter2 = kind ? projectLogicalKindFields(okfVersion, kind, source.frontmatter) : source.frontmatter;
|
|
5845
|
+
documents.push({
|
|
5846
|
+
id: head.id,
|
|
5847
|
+
version: source.version,
|
|
5848
|
+
frontmatter: frontmatter2,
|
|
5849
|
+
...source.body === void 0 ? {} : { body: source.body }
|
|
5850
|
+
});
|
|
5851
|
+
}
|
|
5852
|
+
const relationships = edges.map(({ from, to, text }) => ({ from, to, text }));
|
|
5853
|
+
return {
|
|
5854
|
+
reply: ok(request.id, request.bridge, request.type, {
|
|
5855
|
+
okfVersion,
|
|
5856
|
+
documents,
|
|
5857
|
+
relationships,
|
|
5858
|
+
counts: { documents: documents.length, relationships: relationships.length }
|
|
5859
|
+
})
|
|
5860
|
+
};
|
|
5861
|
+
}
|
|
5862
|
+
hostDescriptor() {
|
|
5863
|
+
const declared = new Set(this.options.host.capabilities);
|
|
5864
|
+
for (const name of Object.keys(this.options.hostHandlers ?? {})) declared.add(name);
|
|
5865
|
+
return {
|
|
5866
|
+
kind: this.options.host.kind,
|
|
5867
|
+
capabilities: [...declared].sort(),
|
|
5868
|
+
limits: { ...this.options.host.limits },
|
|
5869
|
+
...this.options.host.frame ? { frame: { ...this.options.host.frame } } : {},
|
|
5870
|
+
...this.options.host.theme ? { theme: { ...this.options.host.theme } } : {}
|
|
5871
|
+
};
|
|
5872
|
+
}
|
|
5572
5873
|
async execute(launch, request) {
|
|
5573
5874
|
if (request.type === "open-page") {
|
|
5574
5875
|
if (this.options.consumeOpenPage === true) {
|
|
@@ -5592,10 +5893,24 @@ var BridgeService = class {
|
|
|
5592
5893
|
bundle: { root: config.root, name: config.name },
|
|
5593
5894
|
mode: config.mode,
|
|
5594
5895
|
protocol: BRIDGE_PROTOCOL,
|
|
5595
|
-
grant: launch.capability === "bundle-propose" ? "propose" : "read"
|
|
5896
|
+
grant: launch.capability === "bundle-propose" ? "propose" : "read",
|
|
5897
|
+
host: this.hostDescriptor()
|
|
5596
5898
|
})
|
|
5597
5899
|
};
|
|
5598
5900
|
}
|
|
5901
|
+
if (request.type === "host") {
|
|
5902
|
+
const handlers = this.options.hostHandlers ?? {};
|
|
5903
|
+
if (!Object.hasOwn(handlers, request.capability)) {
|
|
5904
|
+
return { reply: fail(request.id, request.bridge, "FORBIDDEN", "this host does not offer the requested capability") };
|
|
5905
|
+
}
|
|
5906
|
+
const outcome = await handlers[request.capability]({
|
|
5907
|
+
capability: request.capability,
|
|
5908
|
+
input: request.input,
|
|
5909
|
+
launch
|
|
5910
|
+
});
|
|
5911
|
+
if (!outcome.ok) return { reply: fail(request.id, request.bridge, outcome.code, outcome.message) };
|
|
5912
|
+
return { reply: ok(request.id, request.bridge, request.type, { capability: request.capability, output: outcome.output }) };
|
|
5913
|
+
}
|
|
5599
5914
|
if (request.type === "query") {
|
|
5600
5915
|
const rows = await queryHeads2(this.options.bundle, {
|
|
5601
5916
|
...request.params.type ? { type: request.params.type } : {},
|
|
@@ -5673,6 +5988,9 @@ var BridgeService = class {
|
|
|
5673
5988
|
const projected = edges.map(({ from, to, text }) => ({ from, to, text }));
|
|
5674
5989
|
return { reply: ok(request.id, request.bridge, request.type, { edges: projected, count: projected.length }) };
|
|
5675
5990
|
}
|
|
5991
|
+
if (request.type === "graph") {
|
|
5992
|
+
return this.graph(launch, request);
|
|
5993
|
+
}
|
|
5676
5994
|
if (this.options.enablePolling) {
|
|
5677
5995
|
this.subscriptions.set(launch.launchId, {
|
|
5678
5996
|
baseline: await this.subscriptionSnapshot()
|
|
@@ -5821,6 +6139,10 @@ var STATIC_LAUNCH_ID = "publication-snapshot";
|
|
|
5821
6139
|
function digest(bytes) {
|
|
5822
6140
|
return `sha256:${createHash3("sha256").update(bytes).digest("hex")}`;
|
|
5823
6141
|
}
|
|
6142
|
+
var STATIC_BRIDGE_CAPABILITIES = Object.freeze([
|
|
6143
|
+
...BRIDGE_SERVICE_CAPABILITIES,
|
|
6144
|
+
BRIDGE_HOST_CAPABILITIES.openPage
|
|
6145
|
+
]);
|
|
5824
6146
|
function createPublicationBridge(options) {
|
|
5825
6147
|
if (options.protocol !== PUBLICATION_BRIDGE_V0) {
|
|
5826
6148
|
throw new PublicationError("CAPABILITY_UNAVAILABLE", "the requested publication bridge protocol is unsupported");
|
|
@@ -5868,7 +6190,11 @@ function createPublicationBridge(options) {
|
|
|
5868
6190
|
if (!rendered || rendered.id !== id) throw new Error(`document '${id}' is not available`);
|
|
5869
6191
|
return { html: rendered.html, bounded: rendered.bounded };
|
|
5870
6192
|
},
|
|
5871
|
-
|
|
6193
|
+
host: {
|
|
6194
|
+
kind: options.host?.kind ?? "oss",
|
|
6195
|
+
capabilities: options.host?.capabilities ?? STATIC_BRIDGE_CAPABILITIES,
|
|
6196
|
+
limits: BRIDGE_SERVICE_LIMITS
|
|
6197
|
+
},
|
|
5872
6198
|
enablePolling: false,
|
|
5873
6199
|
consumeOpenPage: false
|
|
5874
6200
|
});
|