superbee 0.2.1 → 0.3.0-pre.2
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 +25 -7
- package/SKILL.md +6 -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 +565 -217
- package/dist/publication.mjs +620 -233
- package/dist/superbee.mjs +114401 -101544
- package/package.json +11 -8
- package/references/hosted-checkout.md +130 -0
- 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,50 +3658,50 @@ 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);
|
|
3647
3701
|
await delay(pollMs);
|
|
3648
3702
|
continue;
|
|
3649
3703
|
}
|
|
3704
|
+
const claimed = await fs.lstat(lockPath, { bigint: true }).catch(() => null);
|
|
3650
3705
|
try {
|
|
3651
3706
|
await fs.writeFile(path.join(lockPath, OWNER_FILE), `${JSON.stringify(owner)}
|
|
3652
3707
|
`, {
|
|
@@ -3655,28 +3710,113 @@ async function claimLockPath(lockPath, owner, waitMs, pollMs) {
|
|
|
3655
3710
|
mode: 384
|
|
3656
3711
|
});
|
|
3657
3712
|
} catch (err) {
|
|
3658
|
-
|
|
3713
|
+
const code = err.code;
|
|
3714
|
+
if (code === "EEXIST") continue;
|
|
3715
|
+
await rollBackOwnClaim(lockPath, owner, claimed, waitMs, pollMs, policy).catch(() => {
|
|
3659
3716
|
});
|
|
3717
|
+
if (code === "ENOENT" || code === "ENOTDIR") continue;
|
|
3660
3718
|
throw err;
|
|
3661
3719
|
}
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
if (
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
);
|
|
3720
|
+
let inFlight;
|
|
3721
|
+
return () => {
|
|
3722
|
+
if (inFlight) return inFlight;
|
|
3723
|
+
inFlight = (async () => {
|
|
3724
|
+
const started2 = Date.now();
|
|
3725
|
+
const current = await resolveOwnerRecord(lockPath, started2, waitMs, pollMs);
|
|
3726
|
+
if (current.state === "unreadable") throw unreadableOwnerRefusal(lockPath, current.error);
|
|
3727
|
+
if (current.state !== "record" || current.owner.token !== owner.token) {
|
|
3728
|
+
throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
|
|
3729
|
+
}
|
|
3730
|
+
await removeReleasedLock(lockPath, owner, started2, waitMs, pollMs, policy);
|
|
3731
|
+
})();
|
|
3732
|
+
return inFlight.finally(() => {
|
|
3733
|
+
inFlight = void 0;
|
|
3734
|
+
});
|
|
3735
|
+
};
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
async function rollBackOwnClaim(lockPath, owner, claimed, waitMs, pollMs, policy) {
|
|
3739
|
+
const started = Date.now();
|
|
3740
|
+
const record = await resolveOwnerRecord(lockPath, started, waitMs, pollMs);
|
|
3741
|
+
if (record.state === "record") {
|
|
3742
|
+
if (record.owner.token === owner.token) await removeReleasedLock(lockPath, owner, started, waitMs, pollMs, policy);
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
if (record.state === "unreadable" || !await isClaimedDirectory(lockPath, claimed)) return;
|
|
3746
|
+
const remnant = releasedLockRemnantPath(lockPath, owner);
|
|
3747
|
+
await fs.rename(lockPath, remnant);
|
|
3748
|
+
await fs.rm(remnant, { recursive: true, force: true });
|
|
3749
|
+
}
|
|
3750
|
+
async function isClaimedDirectory(lockPath, claimed) {
|
|
3751
|
+
if (claimed === null || claimed.birthtimeNs <= 0n) return false;
|
|
3752
|
+
const current = await fs.lstat(lockPath, { bigint: true }).catch(() => null);
|
|
3753
|
+
return current !== null && current.isDirectory() && current.dev === claimed.dev && current.ino === claimed.ino && current.birthtimeNs === claimed.birthtimeNs;
|
|
3754
|
+
}
|
|
3755
|
+
function changedOwnerRefusal(lockPath, current) {
|
|
3756
|
+
return new FilesystemMutationLockError(
|
|
3757
|
+
`refusing to release filesystem mutation lock '${lockPath}' because its owner token changed; the mutation may have completed, inspect the lock before retrying.`,
|
|
3758
|
+
{ lockPath, owner: current, stale: false, malformed: current === null }
|
|
3759
|
+
);
|
|
3760
|
+
}
|
|
3761
|
+
function unreadableOwnerRefusal(lockPath, error) {
|
|
3762
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3763
|
+
return new FilesystemMutationLockError(
|
|
3764
|
+
`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.`,
|
|
3765
|
+
{ lockPath, owner: null, stale: false, malformed: true }
|
|
3766
|
+
);
|
|
3767
|
+
}
|
|
3768
|
+
function releasedLockRemnantPath(lockPath, owner) {
|
|
3769
|
+
const tokenHash = createHash("sha256").update(owner.token).digest("hex");
|
|
3770
|
+
return `${lockPath}.released-${tokenHash}`;
|
|
3771
|
+
}
|
|
3772
|
+
function removalFailure(inspectPath, owner, err, attempts, detail) {
|
|
3773
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3774
|
+
const suffix = attempts > 1 ? ` after ${attempts} bounded attempts` : "";
|
|
3775
|
+
return new FilesystemMutationLockError(
|
|
3776
|
+
`mutation completed but ${detail}${suffix} (${message}); inspect the lock before retrying.`,
|
|
3777
|
+
{ lockPath: inspectPath, owner, stale: false, malformed: false }
|
|
3778
|
+
);
|
|
3779
|
+
}
|
|
3780
|
+
async function removeReleasedLock(lockPath, owner, started, waitMs, pollMs, policy) {
|
|
3781
|
+
const remnant = releasedLockRemnantPath(lockPath, owner);
|
|
3782
|
+
let attempts = 0;
|
|
3783
|
+
while (true) {
|
|
3784
|
+
try {
|
|
3785
|
+
await fs.rename(lockPath, remnant);
|
|
3786
|
+
break;
|
|
3787
|
+
} catch (err) {
|
|
3788
|
+
attempts += 1;
|
|
3789
|
+
if (attempts > 1 && err.code === "ENOENT") return;
|
|
3790
|
+
if (!policy.isDirectoryContentionError(err) || Date.now() - started >= waitMs) {
|
|
3791
|
+
throw removalFailure(lockPath, owner, err, attempts, `filesystem lock '${lockPath}' could not be removed`);
|
|
3669
3792
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3793
|
+
}
|
|
3794
|
+
await delay(pollMs);
|
|
3795
|
+
const current = await resolveOwnerRecord(lockPath, started, waitMs, pollMs);
|
|
3796
|
+
if (current.state === "record" && current.owner.token === owner.token) continue;
|
|
3797
|
+
if (current.state === "unreadable") throw unreadableOwnerRefusal(lockPath, current.error);
|
|
3798
|
+
if (current.state === "absent" && !await pathExists(lockPath)) return;
|
|
3799
|
+
throw changedOwnerRefusal(lockPath, current.state === "record" ? current.owner : null);
|
|
3800
|
+
}
|
|
3801
|
+
attempts = 0;
|
|
3802
|
+
while (true) {
|
|
3803
|
+
try {
|
|
3804
|
+
await fs.rm(remnant, { recursive: true, force: false });
|
|
3805
|
+
return;
|
|
3806
|
+
} catch (err) {
|
|
3807
|
+
attempts += 1;
|
|
3808
|
+
if (err.code === "ENOENT") return;
|
|
3809
|
+
if (!policy.isDirectoryContentionError(err) || Date.now() - started >= waitMs) {
|
|
3810
|
+
throw removalFailure(
|
|
3811
|
+
remnant,
|
|
3812
|
+
owner,
|
|
3813
|
+
err,
|
|
3814
|
+
attempts,
|
|
3815
|
+
`filesystem lock '${lockPath}' was released yet its remnant '${remnant}' could not be removed`
|
|
3677
3816
|
);
|
|
3678
3817
|
}
|
|
3679
|
-
}
|
|
3818
|
+
}
|
|
3819
|
+
await delay(pollMs);
|
|
3680
3820
|
}
|
|
3681
3821
|
}
|
|
3682
3822
|
function newOwner(target) {
|
|
@@ -3694,21 +3834,20 @@ function assertIdentityKey(key) {
|
|
|
3694
3834
|
}
|
|
3695
3835
|
async function acquireFilesystemIdentityLock(key, identity, options = {}) {
|
|
3696
3836
|
assertIdentityKey(key);
|
|
3837
|
+
const policy = captureFilesystemHostPolicy(options.hostPolicy);
|
|
3697
3838
|
const waitMs = positiveOption(options.waitMs, DEFAULT_WAIT_MS, "waitMs");
|
|
3698
3839
|
const pollMs = positiveOption(options.pollMs, DEFAULT_POLL_MS, "pollMs");
|
|
3699
3840
|
const owner = newOwner(identity);
|
|
3700
|
-
const lockRoot = await selectLockRoot(options);
|
|
3701
|
-
return claimLockPath(path.join(lockRoot, `${key}.lock`), owner, waitMs, pollMs);
|
|
3841
|
+
const lockRoot = await selectLockRoot(options, policy);
|
|
3842
|
+
return claimLockPath(path.join(lockRoot, `${key}.lock`), owner, waitMs, pollMs, policy);
|
|
3702
3843
|
}
|
|
3703
3844
|
|
|
3704
3845
|
// ../core/src/filesystem-identity.ts
|
|
3705
|
-
var WINDOWS_REPLACE_CONFLICT_CODES = /* @__PURE__ */ new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
3706
3846
|
async function replaceExistingLeaf(port, tmp, target, rel) {
|
|
3707
3847
|
try {
|
|
3708
3848
|
await port.rename(tmp, target);
|
|
3709
3849
|
} catch (err) {
|
|
3710
|
-
|
|
3711
|
-
if ((port.platform ?? process.platform) === "win32" && WINDOWS_REPLACE_CONFLICT_CODES.has(code)) {
|
|
3850
|
+
if (port.isReplacementConflict?.(err)) {
|
|
3712
3851
|
throw new ConcurrentReplacementError(rel, 1, 10);
|
|
3713
3852
|
}
|
|
3714
3853
|
throw err;
|
|
@@ -3875,7 +4014,7 @@ async function observeExact(port, root, rel, read) {
|
|
|
3875
4014
|
opened = await port.open(target);
|
|
3876
4015
|
} catch (err) {
|
|
3877
4016
|
if (isAbsentPathError(err)) return { state: "absent" };
|
|
3878
|
-
if (
|
|
4017
|
+
if (port.isTransientOpenError?.(err)) {
|
|
3879
4018
|
restarts = countRestart(restarts, rel);
|
|
3880
4019
|
continue;
|
|
3881
4020
|
}
|
|
@@ -3909,7 +4048,15 @@ async function probeExact(port, root, rel) {
|
|
|
3909
4048
|
restarts = countRestart(restarts, rel);
|
|
3910
4049
|
}
|
|
3911
4050
|
}
|
|
3912
|
-
var linkUnsupportedRoots = /* @__PURE__ */ new
|
|
4051
|
+
var linkUnsupportedRoots = /* @__PURE__ */ new WeakMap();
|
|
4052
|
+
function unsupportedLinkRoots(port) {
|
|
4053
|
+
let roots = linkUnsupportedRoots.get(port);
|
|
4054
|
+
if (!roots) {
|
|
4055
|
+
roots = /* @__PURE__ */ new Set();
|
|
4056
|
+
linkUnsupportedRoots.set(port, roots);
|
|
4057
|
+
}
|
|
4058
|
+
return roots;
|
|
4059
|
+
}
|
|
3913
4060
|
var queues = /* @__PURE__ */ new Map();
|
|
3914
4061
|
function enqueue(key, fn) {
|
|
3915
4062
|
const tail = queues.get(key) ?? Promise.resolve();
|
|
@@ -3991,7 +4138,7 @@ async function createExactLeaf(port, tmp, target, rel, leaf, rootResolved) {
|
|
|
3991
4138
|
return;
|
|
3992
4139
|
}
|
|
3993
4140
|
if (outcome === "unsupported") {
|
|
3994
|
-
|
|
4141
|
+
unsupportedLinkRoots(port).add(rootResolved);
|
|
3995
4142
|
await port.rename(tmp, target);
|
|
3996
4143
|
return;
|
|
3997
4144
|
}
|
|
@@ -4027,7 +4174,7 @@ async function mutateExact(port, root, rel, body) {
|
|
|
4027
4174
|
await replaceExistingLeaf(port, tmp, target, rel);
|
|
4028
4175
|
return;
|
|
4029
4176
|
}
|
|
4030
|
-
if (
|
|
4177
|
+
if (unsupportedLinkRoots(port).has(rootResolved)) {
|
|
4031
4178
|
await port.rename(tmp, target);
|
|
4032
4179
|
return;
|
|
4033
4180
|
}
|
|
@@ -4054,80 +4201,84 @@ function kindOf(stats) {
|
|
|
4054
4201
|
if (stats.isFile()) return "file";
|
|
4055
4202
|
return "other";
|
|
4056
4203
|
}
|
|
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
|
-
|
|
4204
|
+
function createNodeFilesystemIdentityPort(hostPolicy) {
|
|
4205
|
+
const policy = captureFilesystemHostPolicy(hostPolicy);
|
|
4206
|
+
return Object.freeze({
|
|
4207
|
+
isTransientOpenError: policy.isTransientOpenError,
|
|
4208
|
+
isReplacementConflict: policy.isReplacementConflict,
|
|
4209
|
+
async probe(target) {
|
|
4210
|
+
let lstats;
|
|
4211
|
+
try {
|
|
4212
|
+
lstats = await fs2.lstat(target);
|
|
4213
|
+
} catch (err) {
|
|
4214
|
+
if (isAbsentPathError(err)) return null;
|
|
4215
|
+
throw err;
|
|
4216
|
+
}
|
|
4217
|
+
return { kind: kindOf(lstats), dev: lstats.dev, ino: lstats.ino };
|
|
4218
|
+
},
|
|
4219
|
+
async entries(dir) {
|
|
4220
|
+
try {
|
|
4221
|
+
const dirents = await fs2.readdir(dir, { withFileTypes: true });
|
|
4222
|
+
return dirents.map((dirent) => ({ name: dirent.name, kind: kindOf(dirent) }));
|
|
4223
|
+
} catch (err) {
|
|
4224
|
+
if (isAbsentPathError(err)) return null;
|
|
4225
|
+
throw err;
|
|
4226
|
+
}
|
|
4227
|
+
},
|
|
4228
|
+
async open(target) {
|
|
4229
|
+
const handle = await fs2.open(target, "r");
|
|
4230
|
+
try {
|
|
4231
|
+
const stats = await handle.stat();
|
|
4232
|
+
return { handle, dev: stats.dev, ino: stats.ino };
|
|
4233
|
+
} catch (err) {
|
|
4234
|
+
await handle.close().catch(() => {
|
|
4235
|
+
});
|
|
4236
|
+
throw err;
|
|
4237
|
+
}
|
|
4238
|
+
},
|
|
4239
|
+
readAll(handle) {
|
|
4240
|
+
return handle.readFile();
|
|
4241
|
+
},
|
|
4242
|
+
close(handle) {
|
|
4243
|
+
return handle.close();
|
|
4244
|
+
},
|
|
4245
|
+
async stat(target) {
|
|
4246
|
+
return { mtime: (await fs2.stat(target)).mtime };
|
|
4247
|
+
},
|
|
4248
|
+
async mkdir(dir) {
|
|
4249
|
+
try {
|
|
4250
|
+
await fs2.mkdir(dir);
|
|
4251
|
+
return "created";
|
|
4252
|
+
} catch (err) {
|
|
4253
|
+
if (err.code === "EEXIST") return "exists";
|
|
4254
|
+
throw err;
|
|
4255
|
+
}
|
|
4256
|
+
},
|
|
4257
|
+
async writeTemp(dir, name, bytes) {
|
|
4258
|
+
await fs2.writeFile(path2.join(dir, name), bytes);
|
|
4259
|
+
},
|
|
4260
|
+
async link(from, to) {
|
|
4261
|
+
try {
|
|
4262
|
+
await fs2.link(from, to);
|
|
4263
|
+
return "linked";
|
|
4264
|
+
} catch (err) {
|
|
4265
|
+
const code = err.code;
|
|
4266
|
+
if (code === "EEXIST") return "exists";
|
|
4267
|
+
if (code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EXDEV") return "unsupported";
|
|
4268
|
+
throw err;
|
|
4269
|
+
}
|
|
4270
|
+
},
|
|
4271
|
+
async rename(from, to) {
|
|
4272
|
+
await fs2.rename(from, to);
|
|
4273
|
+
},
|
|
4274
|
+
async unlink(target) {
|
|
4275
|
+
await fs2.unlink(target);
|
|
4276
|
+
},
|
|
4277
|
+
claim(key, identity) {
|
|
4278
|
+
return acquireFilesystemIdentityLock(key, `${identity.root}:${identity.rel}`, { portableRoot: identity.root, hostPolicy: policy });
|
|
4119
4279
|
}
|
|
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
|
-
});
|
|
4280
|
+
});
|
|
4281
|
+
}
|
|
4131
4282
|
|
|
4132
4283
|
// ../core/src/sha256.ts
|
|
4133
4284
|
var K = new Uint32Array([
|
|
@@ -4286,6 +4437,9 @@ function sha256HexOfUtf8(input) {
|
|
|
4286
4437
|
}
|
|
4287
4438
|
|
|
4288
4439
|
// ../core/src/version-transport.ts
|
|
4440
|
+
function isContentVersion(value) {
|
|
4441
|
+
return typeof value === "string" && /^sha256:[0-9a-f]{64}$/.test(value);
|
|
4442
|
+
}
|
|
4289
4443
|
var VersionConflict = class extends Error {
|
|
4290
4444
|
name = "VersionConflict";
|
|
4291
4445
|
id;
|
|
@@ -4317,7 +4471,7 @@ function defaultActor() {
|
|
|
4317
4471
|
}
|
|
4318
4472
|
|
|
4319
4473
|
// ../core/src/backend.ts
|
|
4320
|
-
var
|
|
4474
|
+
var publicationContexts = /* @__PURE__ */ new WeakMap();
|
|
4321
4475
|
function firstString(...vals) {
|
|
4322
4476
|
for (const v of vals) {
|
|
4323
4477
|
if (typeof v === "string" && v.trim() !== "") return v;
|
|
@@ -4336,22 +4490,22 @@ function notFound(root, rel) {
|
|
|
4336
4490
|
err.path = target;
|
|
4337
4491
|
return err;
|
|
4338
4492
|
}
|
|
4339
|
-
async function readBytes(handle) {
|
|
4493
|
+
async function readBytes(port, handle) {
|
|
4340
4494
|
try {
|
|
4341
|
-
return await
|
|
4495
|
+
return await port.readAll(handle);
|
|
4342
4496
|
} catch (err) {
|
|
4343
4497
|
if (isAbsentFileError(err)) return null;
|
|
4344
4498
|
throw err;
|
|
4345
4499
|
}
|
|
4346
4500
|
}
|
|
4347
|
-
async function walkFiles(root, keep, sub = "") {
|
|
4348
|
-
const entries = await
|
|
4501
|
+
async function walkFiles(port, root, keep, sub = "") {
|
|
4502
|
+
const entries = await port.entries(path3.join(root, sub)) ?? [];
|
|
4349
4503
|
const out = [];
|
|
4350
4504
|
for (const entry of entries) {
|
|
4351
4505
|
if (entry.name.startsWith(".")) continue;
|
|
4352
4506
|
const rel = sub === "" ? entry.name : `${sub}/${entry.name}`;
|
|
4353
4507
|
if (entry.kind === "directory") {
|
|
4354
|
-
out.push(...await walkFiles(root, keep, rel));
|
|
4508
|
+
out.push(...await walkFiles(port, root, keep, rel));
|
|
4355
4509
|
} else if (entry.kind === "file" && keep(entry.name)) {
|
|
4356
4510
|
out.push(rel);
|
|
4357
4511
|
}
|
|
@@ -4359,19 +4513,20 @@ async function walkFiles(root, keep, sub = "") {
|
|
|
4359
4513
|
return out;
|
|
4360
4514
|
}
|
|
4361
4515
|
function reservedPath(dir, name) {
|
|
4362
|
-
|
|
4363
|
-
return d === "" ? name : `${d}/${name}`;
|
|
4516
|
+
return dir === "" ? name : `${dir}/${name}`;
|
|
4364
4517
|
}
|
|
4365
4518
|
var FilesystemBackend = class _FilesystemBackend {
|
|
4366
4519
|
#root;
|
|
4520
|
+
#port;
|
|
4367
4521
|
/**
|
|
4368
4522
|
* The root is resolved once here: a relative root would otherwise re-resolve against the
|
|
4369
4523
|
* process's current directory on every operation, so a later `chdir` would silently move the
|
|
4370
4524
|
* bundle and derive a different identity key.
|
|
4371
4525
|
*/
|
|
4372
|
-
constructor(root) {
|
|
4526
|
+
constructor(root, options = {}) {
|
|
4527
|
+
this.#port = createNodeFilesystemIdentityPort(options.hostPolicy);
|
|
4373
4528
|
this.#root = path3.resolve(root);
|
|
4374
|
-
|
|
4529
|
+
publicationContexts.set(this, { root: this.#root, port: this.#port });
|
|
4375
4530
|
}
|
|
4376
4531
|
async read(id) {
|
|
4377
4532
|
return this.#readWithEdition(id, () => this.#readEdition());
|
|
@@ -4387,7 +4542,7 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4387
4542
|
async #readWithEdition(id, edition) {
|
|
4388
4543
|
assertSafeConceptId(id);
|
|
4389
4544
|
const rel = pathFromConceptId(id);
|
|
4390
|
-
const observed = await observeExact(
|
|
4545
|
+
const observed = await observeExact(this.#port, this.#root, rel, (handle) => readBytes(this.#port, handle));
|
|
4391
4546
|
if (observed.state === "absent") throw notFound(this.#root, rel);
|
|
4392
4547
|
const raw = observed.value.toString("utf8");
|
|
4393
4548
|
const { frontmatter: frontmatter2, body } = parseMarkdown(raw, rel, { okfVersion: await edition() });
|
|
@@ -4409,7 +4564,7 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4409
4564
|
assertSafeConceptId(id);
|
|
4410
4565
|
const raw = stringifyDoc(doc.frontmatter, doc.body ?? "");
|
|
4411
4566
|
const rel = pathFromConceptId(id);
|
|
4412
|
-
return mutateExact(
|
|
4567
|
+
return mutateExact(this.#port, this.#root, rel, async (target) => {
|
|
4413
4568
|
if (options.expectedVersion !== void 0) {
|
|
4414
4569
|
const bytes = await target.current();
|
|
4415
4570
|
const current = bytes === null ? null : versionOfBytes(bytes.toString("utf8"));
|
|
@@ -4423,7 +4578,7 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4423
4578
|
}
|
|
4424
4579
|
async delete(id, options = {}) {
|
|
4425
4580
|
assertSafeConceptId(id);
|
|
4426
|
-
return mutateExact(
|
|
4581
|
+
return mutateExact(this.#port, this.#root, pathFromConceptId(id), async (target) => {
|
|
4427
4582
|
const bytes = await target.current();
|
|
4428
4583
|
if (bytes === null) return false;
|
|
4429
4584
|
const current = versionOfBytes(bytes.toString("utf8"));
|
|
@@ -4437,10 +4592,10 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4437
4592
|
async versions(id) {
|
|
4438
4593
|
assertSafeConceptId(id);
|
|
4439
4594
|
const rel = pathFromConceptId(id);
|
|
4440
|
-
const observed = await observeExact(
|
|
4595
|
+
const observed = await observeExact(this.#port, this.#root, rel, async (handle, target) => {
|
|
4441
4596
|
try {
|
|
4442
|
-
const bytes = await
|
|
4443
|
-
const { mtime: mtime2 } = await
|
|
4597
|
+
const bytes = await this.#port.readAll(handle);
|
|
4598
|
+
const { mtime: mtime2 } = await this.#port.stat(target);
|
|
4444
4599
|
return { raw: bytes.toString("utf8"), mtime: mtime2 };
|
|
4445
4600
|
} catch {
|
|
4446
4601
|
return null;
|
|
@@ -4455,10 +4610,10 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4455
4610
|
}
|
|
4456
4611
|
async exists(id) {
|
|
4457
4612
|
assertSafeConceptId(id);
|
|
4458
|
-
return (await probeExact(
|
|
4613
|
+
return (await probeExact(this.#port, this.#root, pathFromConceptId(id))).state === "exact";
|
|
4459
4614
|
}
|
|
4460
4615
|
async list(prefix) {
|
|
4461
|
-
const files = await walkFiles(this.#root, (name) => name.endsWith(".md"));
|
|
4616
|
+
const files = await walkFiles(this.#port, this.#root, (name) => name.endsWith(".md"));
|
|
4462
4617
|
const ids = [];
|
|
4463
4618
|
for (const rel of files) {
|
|
4464
4619
|
if (isReservedFile(rel)) continue;
|
|
@@ -4472,20 +4627,22 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4472
4627
|
if (prefix && !id.startsWith(prefix)) continue;
|
|
4473
4628
|
ids.push(id);
|
|
4474
4629
|
}
|
|
4475
|
-
ids.sort(
|
|
4630
|
+
ids.sort(compareStorageKeys);
|
|
4476
4631
|
return ids;
|
|
4477
4632
|
}
|
|
4478
4633
|
async readReserved(dir, name) {
|
|
4479
4634
|
assertSafeReservedDir(dir);
|
|
4480
|
-
|
|
4635
|
+
assertSafeReservedFilename(name);
|
|
4636
|
+
const observed = await observeExact(this.#port, this.#root, reservedPath(dir, name), (handle) => readBytes(this.#port, handle));
|
|
4481
4637
|
if (observed.state === "absent") return null;
|
|
4482
4638
|
const content = observed.value.toString("utf8");
|
|
4483
4639
|
return { content, version: versionOfBytes(content) };
|
|
4484
4640
|
}
|
|
4485
4641
|
async writeReserved(dir, name, content, options = {}) {
|
|
4486
4642
|
assertSafeReservedDir(dir);
|
|
4643
|
+
assertSafeReservedFilename(name);
|
|
4487
4644
|
const rel = reservedPath(dir, name);
|
|
4488
|
-
return mutateExact(
|
|
4645
|
+
return mutateExact(this.#port, this.#root, rel, async (target) => {
|
|
4489
4646
|
if (options.expectedVersion !== void 0) {
|
|
4490
4647
|
const bytes = await target.current();
|
|
4491
4648
|
const current = bytes === null ? null : versionOfBytes(bytes.toString("utf8"));
|
|
@@ -4500,14 +4657,14 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4500
4657
|
// ── blobs: opaque bytes + a content-type ──────────────────────────────────
|
|
4501
4658
|
async readBlob(key) {
|
|
4502
4659
|
assertSafeBlobKey(key);
|
|
4503
|
-
const observed = await observeExact(
|
|
4660
|
+
const observed = await observeExact(this.#port, this.#root, key, (handle) => readBytes(this.#port, handle));
|
|
4504
4661
|
if (observed.state === "absent") return null;
|
|
4505
4662
|
const bytes = observed.value;
|
|
4506
4663
|
return { bytes, contentType: resolveContentType(key), version: blobVersion(bytes) };
|
|
4507
4664
|
}
|
|
4508
4665
|
async writeBlob(key, bytes, _contentType, options = {}) {
|
|
4509
4666
|
assertSafeBlobKey(key);
|
|
4510
|
-
return mutateExact(
|
|
4667
|
+
return mutateExact(this.#port, this.#root, key, async (target) => {
|
|
4511
4668
|
if (options.expectedVersion !== void 0) {
|
|
4512
4669
|
const existing = await target.current();
|
|
4513
4670
|
const current = existing === null ? null : blobVersion(existing);
|
|
@@ -4521,7 +4678,7 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4521
4678
|
}
|
|
4522
4679
|
async deleteBlob(key, options = {}) {
|
|
4523
4680
|
assertSafeBlobKey(key);
|
|
4524
|
-
return mutateExact(
|
|
4681
|
+
return mutateExact(this.#port, this.#root, key, async (target) => {
|
|
4525
4682
|
const existing = await target.current();
|
|
4526
4683
|
if (existing === null) return false;
|
|
4527
4684
|
const current = blobVersion(existing);
|
|
@@ -4534,14 +4691,14 @@ var FilesystemBackend = class _FilesystemBackend {
|
|
|
4534
4691
|
}
|
|
4535
4692
|
async existsBlob(key) {
|
|
4536
4693
|
assertSafeBlobKey(key);
|
|
4537
|
-
const observed = await probeExact(
|
|
4694
|
+
const observed = await probeExact(this.#port, this.#root, key);
|
|
4538
4695
|
return observed.state === "exact" && observed.value.kind === "file";
|
|
4539
4696
|
}
|
|
4540
4697
|
async listBlobs(prefix) {
|
|
4541
|
-
const files = await walkFiles(this.#root, (name) => !name.toLowerCase().endsWith(".md"));
|
|
4698
|
+
const files = await walkFiles(this.#port, this.#root, (name) => !name.toLowerCase().endsWith(".md"));
|
|
4542
4699
|
const keys = files.map(toPosix);
|
|
4543
4700
|
const filtered = prefix ? keys.filter((k) => k.startsWith(prefix)) : keys;
|
|
4544
|
-
filtered.sort(
|
|
4701
|
+
filtered.sort(compareStorageKeys);
|
|
4545
4702
|
return filtered;
|
|
4546
4703
|
}
|
|
4547
4704
|
capabilities() {
|
|
@@ -4592,7 +4749,7 @@ setDefaultBackendFactory((root) => new FilesystemBackend(root));
|
|
|
4592
4749
|
var CONVENTIONS_PREFIX = "conventions/";
|
|
4593
4750
|
var CONVENTION_TYPE = "Convention";
|
|
4594
4751
|
function buildKindRegistry(docs, initialWarnings = [], options = {}) {
|
|
4595
|
-
const
|
|
4752
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
4596
4753
|
const warnings = [...initialWarnings];
|
|
4597
4754
|
for (const doc of [...docs].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
4598
4755
|
const parsed = parseConventionDoc(doc, options);
|
|
@@ -4615,10 +4772,10 @@ function buildKindRegistry(docs, initialWarnings = [], options = {}) {
|
|
|
4615
4772
|
severity: "warning"
|
|
4616
4773
|
});
|
|
4617
4774
|
}
|
|
4618
|
-
if (
|
|
4775
|
+
if (accepted.has(kind.governs)) {
|
|
4619
4776
|
warnings.push({
|
|
4620
4777
|
code: "KIND_DUPLICATE_GOVERNS",
|
|
4621
|
-
message: `duplicate kind convention for '${kind.governs}': '${doc.id}' ignored, keeping the first-declared '${
|
|
4778
|
+
message: `duplicate kind convention for '${kind.governs}': '${doc.id}' ignored, keeping the first-declared '${accepted.get(kind.governs).id}'.`,
|
|
4622
4779
|
field: kind.governs,
|
|
4623
4780
|
severity: "warning"
|
|
4624
4781
|
});
|
|
@@ -4632,10 +4789,18 @@ function buildKindRegistry(docs, initialWarnings = [], options = {}) {
|
|
|
4632
4789
|
severity: "warning"
|
|
4633
4790
|
});
|
|
4634
4791
|
}
|
|
4635
|
-
|
|
4792
|
+
accepted.set(kind.governs, kind);
|
|
4636
4793
|
}
|
|
4794
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
4795
|
+
for (const kind of [...accepted.values()].sort(compareReadingOrder)) kinds.set(kind.governs, kind);
|
|
4637
4796
|
return { kinds, warnings };
|
|
4638
4797
|
}
|
|
4798
|
+
function compareReadingOrder(a, b) {
|
|
4799
|
+
if (a.order !== void 0 && b.order !== void 0 && a.order !== b.order) return a.order - b.order;
|
|
4800
|
+
if (a.order !== void 0 && b.order === void 0) return -1;
|
|
4801
|
+
if (a.order === void 0 && b.order !== void 0) return 1;
|
|
4802
|
+
return a.id.localeCompare(b.id);
|
|
4803
|
+
}
|
|
4639
4804
|
var PROGRESS_STATUS_FIELD = "progress_status";
|
|
4640
4805
|
var SUPERBEE_PROGRESS_STATUS_FIELD = "superbee_progress_status";
|
|
4641
4806
|
function progressStatusStorageField(okfVersion) {
|
|
@@ -5104,6 +5269,19 @@ function parseConventionDoc(doc, options = {}) {
|
|
|
5104
5269
|
const path4 = typeof fm.path === "string" && fm.path.trim() !== "" ? fm.path.trim() : void 0;
|
|
5105
5270
|
const freshnessHorizon = typeof fm.freshness_horizon === "string" && fm.freshness_horizon.trim() !== "" ? fm.freshness_horizon.trim() : void 0;
|
|
5106
5271
|
const browseCollapsed = fm.browse_collapsed === true ? true : void 0;
|
|
5272
|
+
let order;
|
|
5273
|
+
if (fm.order !== void 0) {
|
|
5274
|
+
if (typeof fm.order === "number" && Number.isFinite(fm.order)) {
|
|
5275
|
+
order = fm.order;
|
|
5276
|
+
} else {
|
|
5277
|
+
warnings.push({
|
|
5278
|
+
code: "KIND_CONVENTION_BAD_SHAPE",
|
|
5279
|
+
message: `kind convention '${doc.id}' has an invalid 'order' (${describeShape(fm.order)}; expected a finite number such as 10); ignoring it.`,
|
|
5280
|
+
field: "order",
|
|
5281
|
+
severity: "warning"
|
|
5282
|
+
});
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5107
5285
|
const kind = {
|
|
5108
5286
|
id: doc.id,
|
|
5109
5287
|
title,
|
|
@@ -5118,6 +5296,7 @@ function parseConventionDoc(doc, options = {}) {
|
|
|
5118
5296
|
if (sections && sections.length > 0) kind.sections = sections;
|
|
5119
5297
|
if (freshnessHorizon !== void 0) kind.freshnessHorizon = freshnessHorizon;
|
|
5120
5298
|
if (browseCollapsed !== void 0) kind.browseCollapsed = browseCollapsed;
|
|
5299
|
+
if (order !== void 0) kind.order = order;
|
|
5121
5300
|
if (claim !== void 0) kind.claim = claim;
|
|
5122
5301
|
return {
|
|
5123
5302
|
ok: true,
|
|
@@ -5259,7 +5438,7 @@ function isAnyEntryKey(entry) {
|
|
|
5259
5438
|
return isPageEntryKey(entry) || isViewEntryKey(entry);
|
|
5260
5439
|
}
|
|
5261
5440
|
function isViewEntryVersion(value) {
|
|
5262
|
-
return
|
|
5441
|
+
return isContentVersion(value);
|
|
5263
5442
|
}
|
|
5264
5443
|
function parseRegistration(id, frontmatter2) {
|
|
5265
5444
|
if (!isAnyRegistryId(id) || !isPageTypeName(frontmatter2.type) || !isAnyEntryKey(frontmatter2.entry)) return null;
|
|
@@ -5286,9 +5465,45 @@ var MAX_QUERY_ROWS = 500;
|
|
|
5286
5465
|
var MAX_EDGE_ROWS = 1e3;
|
|
5287
5466
|
var MAX_DOCUMENT_BODY_BYTES = 1024 * 1024;
|
|
5288
5467
|
var MAX_REPLY_BYTES = 2 * 1024 * 1024;
|
|
5468
|
+
var GRAPH_MAX_DOCUMENTS = 1e3;
|
|
5469
|
+
var GRAPH_MAX_RELATIONSHIPS = 1e4;
|
|
5289
5470
|
var MAX_CHANGE_ROWS = 100;
|
|
5290
5471
|
var MAX_CHANGE_BYTES = 256 * 1024;
|
|
5291
5472
|
var MAX_SUBSCRIPTION_HEADS = 1e4;
|
|
5473
|
+
var MAX_HOST_CAPABILITY_BYTES = 128;
|
|
5474
|
+
var MAX_HOST_REQUEST_BYTES = 64 * 1024;
|
|
5475
|
+
var HOST_CAPABILITY_NAME = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
|
|
5476
|
+
var BRIDGE_SERVICE_LIMITS = Object.freeze({
|
|
5477
|
+
query: MAX_QUERY_ROWS,
|
|
5478
|
+
edges: MAX_EDGE_ROWS,
|
|
5479
|
+
graphDocuments: GRAPH_MAX_DOCUMENTS,
|
|
5480
|
+
graphRelationships: GRAPH_MAX_RELATIONSHIPS,
|
|
5481
|
+
replyBytes: MAX_REPLY_BYTES
|
|
5482
|
+
});
|
|
5483
|
+
var BRIDGE_HOST_CAPABILITIES = Object.freeze({
|
|
5484
|
+
queryKindProjection: "query.kind-projection",
|
|
5485
|
+
queryFieldOr: "query.field-or",
|
|
5486
|
+
queryOpen: "query.open",
|
|
5487
|
+
queryCount: "query.count",
|
|
5488
|
+
edges: "edges",
|
|
5489
|
+
renderDocument: "render-document",
|
|
5490
|
+
openPage: "open-page",
|
|
5491
|
+
subscribeDeltas: "subscribe-deltas",
|
|
5492
|
+
graph: "graph",
|
|
5493
|
+
graphModel: "graph.model",
|
|
5494
|
+
recordOpen: "record.open",
|
|
5495
|
+
frameResize: "frame.resize"
|
|
5496
|
+
});
|
|
5497
|
+
var BRIDGE_SERVICE_CAPABILITIES = Object.freeze([
|
|
5498
|
+
BRIDGE_HOST_CAPABILITIES.queryKindProjection,
|
|
5499
|
+
BRIDGE_HOST_CAPABILITIES.queryFieldOr,
|
|
5500
|
+
BRIDGE_HOST_CAPABILITIES.queryOpen,
|
|
5501
|
+
BRIDGE_HOST_CAPABILITIES.queryCount,
|
|
5502
|
+
BRIDGE_HOST_CAPABILITIES.edges,
|
|
5503
|
+
BRIDGE_HOST_CAPABILITIES.graph,
|
|
5504
|
+
BRIDGE_HOST_CAPABILITIES.renderDocument
|
|
5505
|
+
]);
|
|
5506
|
+
var V0_REQUEST_TYPES = /* @__PURE__ */ new Set(["hello", "query", "read", "render-document", "edges", "graph", "subscribe", "host", "open-page"]);
|
|
5292
5507
|
function isPlainRecord(value) {
|
|
5293
5508
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
5294
5509
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -5421,6 +5636,45 @@ function parseBridgeRequest(value) {
|
|
|
5421
5636
|
const params = normalizeEdgeParams(value.params);
|
|
5422
5637
|
return params ? { bridge: BRIDGE_PROTOCOL, type: "edges", id, params } : null;
|
|
5423
5638
|
}
|
|
5639
|
+
if (value.type === "graph") {
|
|
5640
|
+
const expected = value.includeBodies === void 0 ? ["bridge", "type", "id"] : ["bridge", "type", "id", "includeBodies"];
|
|
5641
|
+
if (!exactKeys(value, expected)) return null;
|
|
5642
|
+
if (value.includeBodies !== void 0 && typeof value.includeBodies !== "boolean") return null;
|
|
5643
|
+
return { bridge: BRIDGE_PROTOCOL, type: "graph", id, includeBodies: value.includeBodies === true };
|
|
5644
|
+
}
|
|
5645
|
+
if (value.type === "host") {
|
|
5646
|
+
const expected = value.input === void 0 ? ["bridge", "type", "id", "capability"] : ["bridge", "type", "id", "capability", "input"];
|
|
5647
|
+
if (!exactKeys(value, expected)) return null;
|
|
5648
|
+
const capability = boundedString(value.capability, MAX_HOST_CAPABILITY_BYTES);
|
|
5649
|
+
if (!capability || !HOST_CAPABILITY_NAME.test(capability)) return null;
|
|
5650
|
+
if (value.input !== void 0 && !isPlainRecord(value.input)) return null;
|
|
5651
|
+
let bytes;
|
|
5652
|
+
try {
|
|
5653
|
+
bytes = Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
5654
|
+
} catch {
|
|
5655
|
+
return null;
|
|
5656
|
+
}
|
|
5657
|
+
if (bytes > MAX_HOST_REQUEST_BYTES) return null;
|
|
5658
|
+
return {
|
|
5659
|
+
bridge: BRIDGE_PROTOCOL,
|
|
5660
|
+
type: "host",
|
|
5661
|
+
id,
|
|
5662
|
+
capability,
|
|
5663
|
+
...value.input === void 0 ? {} : { input: value.input }
|
|
5664
|
+
};
|
|
5665
|
+
}
|
|
5666
|
+
return null;
|
|
5667
|
+
}
|
|
5668
|
+
function unsupportedRequest(value) {
|
|
5669
|
+
if (!isPlainRecord(value) || typeof value.type !== "string") return null;
|
|
5670
|
+
if (value.bridge === BRIDGE_PROTOCOL) {
|
|
5671
|
+
return V0_REQUEST_TYPES.has(value.type) ? null : { bridge: BRIDGE_PROTOCOL, id: requestId(value.id) ?? void 0 };
|
|
5672
|
+
}
|
|
5673
|
+
if (value.bridge === ACTION_BRIDGE_PROTOCOL) {
|
|
5674
|
+
if (value.type === "read-versioned") return null;
|
|
5675
|
+
const id = value.type === "action.propose" ? value.requestId : value.id;
|
|
5676
|
+
return { bridge: ACTION_BRIDGE_PROTOCOL, id: requestId(id) ?? void 0 };
|
|
5677
|
+
}
|
|
5424
5678
|
return null;
|
|
5425
5679
|
}
|
|
5426
5680
|
function ok(id, bridge, type, result) {
|
|
@@ -5445,6 +5699,17 @@ var BridgeService = class {
|
|
|
5445
5699
|
async handle(launchId, rawRequest) {
|
|
5446
5700
|
const request = parseBridgeRequest(rawRequest);
|
|
5447
5701
|
if (!request) {
|
|
5702
|
+
const unsupported = unsupportedRequest(rawRequest);
|
|
5703
|
+
if (unsupported) {
|
|
5704
|
+
return {
|
|
5705
|
+
reply: fail(
|
|
5706
|
+
unsupported.id,
|
|
5707
|
+
unsupported.bridge,
|
|
5708
|
+
"FORBIDDEN",
|
|
5709
|
+
"this host does not offer the requested bridge operation"
|
|
5710
|
+
)
|
|
5711
|
+
};
|
|
5712
|
+
}
|
|
5448
5713
|
return {
|
|
5449
5714
|
reply: fail(
|
|
5450
5715
|
invalidV0RequestId(rawRequest),
|
|
@@ -5454,16 +5719,6 @@ var BridgeService = class {
|
|
|
5454
5719
|
)
|
|
5455
5720
|
};
|
|
5456
5721
|
}
|
|
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
5722
|
const dataBearing = request.type !== "open-page";
|
|
5468
5723
|
const before = await this.options.launches.resolve(launchId, dataBearing);
|
|
5469
5724
|
if (!before) {
|
|
@@ -5569,6 +5824,73 @@ var BridgeService = class {
|
|
|
5569
5824
|
[...rows].sort((a, b) => a.id.localeCompare(b.id)).map((row) => [row.id, row.version])
|
|
5570
5825
|
);
|
|
5571
5826
|
}
|
|
5827
|
+
/**
|
|
5828
|
+
* Whole-bundle projection for graph-shaped Views. The documents come from one head scan and
|
|
5829
|
+
* the relationships from `queryEdges`, which is a second full-bundle scan; the two scans are
|
|
5830
|
+
* accepted because the reply is bounded by the exported graph limits and the host never
|
|
5831
|
+
* caches bundle state on a View's behalf. No `model` or `definitions` are answered here: the
|
|
5832
|
+
* owner of that shape is undecided, and a host that has one declares the `graph.model`
|
|
5833
|
+
* capability before adding them.
|
|
5834
|
+
*/
|
|
5835
|
+
async graph(launch, request) {
|
|
5836
|
+
const heads = await queryHeads2(this.options.bundle, {});
|
|
5837
|
+
if (heads.length > GRAPH_MAX_DOCUMENTS) {
|
|
5838
|
+
return {
|
|
5839
|
+
reply: fail(request.id, request.bridge, "TOO_LARGE", `the graph exceeded ${GRAPH_MAX_DOCUMENTS} documents`)
|
|
5840
|
+
};
|
|
5841
|
+
}
|
|
5842
|
+
const [registry, declaredOkfVersion, edges] = await Promise.all([
|
|
5843
|
+
loadKinds(this.options.bundle),
|
|
5844
|
+
readBundleOkfVersion2(this.options.bundle),
|
|
5845
|
+
queryEdges2(this.options.bundle, {})
|
|
5846
|
+
]);
|
|
5847
|
+
if (edges.length > GRAPH_MAX_RELATIONSHIPS) {
|
|
5848
|
+
return {
|
|
5849
|
+
reply: fail(request.id, request.bridge, "TOO_LARGE", `the graph exceeded ${GRAPH_MAX_RELATIONSHIPS} relationships`)
|
|
5850
|
+
};
|
|
5851
|
+
}
|
|
5852
|
+
const okfVersion = declaredOkfVersion ?? "0.1";
|
|
5853
|
+
const includeBodies = request.includeBodies && (launch.capability === "bundle-read" || launch.capability === "bundle-propose");
|
|
5854
|
+
const documents = [];
|
|
5855
|
+
for (const head of heads) {
|
|
5856
|
+
const source = includeBodies ? await readDocVersioned2(this.options.bundle, head.id).then((result) => ({
|
|
5857
|
+
version: result.version,
|
|
5858
|
+
frontmatter: result.doc.frontmatter,
|
|
5859
|
+
body: result.doc.body
|
|
5860
|
+
})) : { version: head.version, frontmatter: head.frontmatter };
|
|
5861
|
+
if (source.body !== void 0 && Buffer.byteLength(source.body, "utf8") > MAX_DOCUMENT_BODY_BYTES) {
|
|
5862
|
+
return { reply: fail(request.id, request.bridge, "TOO_LARGE", "a document body exceeded the 1 MiB View limit") };
|
|
5863
|
+
}
|
|
5864
|
+
const kind = registry.kinds.get(String(source.frontmatter.type ?? ""));
|
|
5865
|
+
const frontmatter2 = kind ? projectLogicalKindFields(okfVersion, kind, source.frontmatter) : source.frontmatter;
|
|
5866
|
+
documents.push({
|
|
5867
|
+
id: head.id,
|
|
5868
|
+
version: source.version,
|
|
5869
|
+
frontmatter: frontmatter2,
|
|
5870
|
+
...source.body === void 0 ? {} : { body: source.body }
|
|
5871
|
+
});
|
|
5872
|
+
}
|
|
5873
|
+
const relationships = edges.map(({ from, to, text }) => ({ from, to, text }));
|
|
5874
|
+
return {
|
|
5875
|
+
reply: ok(request.id, request.bridge, request.type, {
|
|
5876
|
+
okfVersion,
|
|
5877
|
+
documents,
|
|
5878
|
+
relationships,
|
|
5879
|
+
counts: { documents: documents.length, relationships: relationships.length }
|
|
5880
|
+
})
|
|
5881
|
+
};
|
|
5882
|
+
}
|
|
5883
|
+
hostDescriptor() {
|
|
5884
|
+
const declared = new Set(this.options.host.capabilities);
|
|
5885
|
+
for (const name of Object.keys(this.options.hostHandlers ?? {})) declared.add(name);
|
|
5886
|
+
return {
|
|
5887
|
+
kind: this.options.host.kind,
|
|
5888
|
+
capabilities: [...declared].sort(),
|
|
5889
|
+
limits: { ...this.options.host.limits },
|
|
5890
|
+
...this.options.host.frame ? { frame: { ...this.options.host.frame } } : {},
|
|
5891
|
+
...this.options.host.theme ? { theme: { ...this.options.host.theme } } : {}
|
|
5892
|
+
};
|
|
5893
|
+
}
|
|
5572
5894
|
async execute(launch, request) {
|
|
5573
5895
|
if (request.type === "open-page") {
|
|
5574
5896
|
if (this.options.consumeOpenPage === true) {
|
|
@@ -5592,10 +5914,25 @@ var BridgeService = class {
|
|
|
5592
5914
|
bundle: { root: config.root, name: config.name },
|
|
5593
5915
|
mode: config.mode,
|
|
5594
5916
|
protocol: BRIDGE_PROTOCOL,
|
|
5595
|
-
grant: launch.capability === "bundle-propose" ? "propose" : "read"
|
|
5917
|
+
grant: launch.capability === "bundle-propose" ? "propose" : "read",
|
|
5918
|
+
...config.mode === "dir" && this.options.host.kind === "oss" && launch.capability === "bundle-propose" ? { actionProtocol: "v1", actions: ["document.set-field", "document.set-body", "document.update"] } : {},
|
|
5919
|
+
host: this.hostDescriptor()
|
|
5596
5920
|
})
|
|
5597
5921
|
};
|
|
5598
5922
|
}
|
|
5923
|
+
if (request.type === "host") {
|
|
5924
|
+
const handlers = this.options.hostHandlers ?? {};
|
|
5925
|
+
if (!Object.hasOwn(handlers, request.capability)) {
|
|
5926
|
+
return { reply: fail(request.id, request.bridge, "FORBIDDEN", "this host does not offer the requested capability") };
|
|
5927
|
+
}
|
|
5928
|
+
const outcome = await handlers[request.capability]({
|
|
5929
|
+
capability: request.capability,
|
|
5930
|
+
input: request.input,
|
|
5931
|
+
launch
|
|
5932
|
+
});
|
|
5933
|
+
if (!outcome.ok) return { reply: fail(request.id, request.bridge, outcome.code, outcome.message) };
|
|
5934
|
+
return { reply: ok(request.id, request.bridge, request.type, { capability: request.capability, output: outcome.output }) };
|
|
5935
|
+
}
|
|
5599
5936
|
if (request.type === "query") {
|
|
5600
5937
|
const rows = await queryHeads2(this.options.bundle, {
|
|
5601
5938
|
...request.params.type ? { type: request.params.type } : {},
|
|
@@ -5673,6 +6010,9 @@ var BridgeService = class {
|
|
|
5673
6010
|
const projected = edges.map(({ from, to, text }) => ({ from, to, text }));
|
|
5674
6011
|
return { reply: ok(request.id, request.bridge, request.type, { edges: projected, count: projected.length }) };
|
|
5675
6012
|
}
|
|
6013
|
+
if (request.type === "graph") {
|
|
6014
|
+
return this.graph(launch, request);
|
|
6015
|
+
}
|
|
5676
6016
|
if (this.options.enablePolling) {
|
|
5677
6017
|
this.subscriptions.set(launch.launchId, {
|
|
5678
6018
|
baseline: await this.subscriptionSnapshot()
|
|
@@ -5821,6 +6161,10 @@ var STATIC_LAUNCH_ID = "publication-snapshot";
|
|
|
5821
6161
|
function digest(bytes) {
|
|
5822
6162
|
return `sha256:${createHash3("sha256").update(bytes).digest("hex")}`;
|
|
5823
6163
|
}
|
|
6164
|
+
var STATIC_BRIDGE_CAPABILITIES = Object.freeze([
|
|
6165
|
+
...BRIDGE_SERVICE_CAPABILITIES,
|
|
6166
|
+
BRIDGE_HOST_CAPABILITIES.openPage
|
|
6167
|
+
]);
|
|
5824
6168
|
function createPublicationBridge(options) {
|
|
5825
6169
|
if (options.protocol !== PUBLICATION_BRIDGE_V0) {
|
|
5826
6170
|
throw new PublicationError("CAPABILITY_UNAVAILABLE", "the requested publication bridge protocol is unsupported");
|
|
@@ -5868,7 +6212,11 @@ function createPublicationBridge(options) {
|
|
|
5868
6212
|
if (!rendered || rendered.id !== id) throw new Error(`document '${id}' is not available`);
|
|
5869
6213
|
return { html: rendered.html, bounded: rendered.bounded };
|
|
5870
6214
|
},
|
|
5871
|
-
|
|
6215
|
+
host: {
|
|
6216
|
+
kind: options.host?.kind ?? "oss",
|
|
6217
|
+
capabilities: options.host?.capabilities ?? STATIC_BRIDGE_CAPABILITIES,
|
|
6218
|
+
limits: BRIDGE_SERVICE_LIMITS
|
|
6219
|
+
},
|
|
5872
6220
|
enablePolling: false,
|
|
5873
6221
|
consumeOpenPage: false
|
|
5874
6222
|
});
|