@hraness/kb 0.17.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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { MetadataObject, MetadataValue } from "./graph.js";
|
|
2
|
+
|
|
3
|
+
export const MAX_PORTFOLIO_NAME_BYTES = 64;
|
|
4
|
+
export const MAX_DOCUMENT_ID_BYTES = 128;
|
|
5
|
+
|
|
6
|
+
const namePattern = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u;
|
|
7
|
+
const documentIdPattern = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/u;
|
|
8
|
+
const qualifiedUriPattern = /^kb:\/\/([a-z0-9][a-z0-9._-]{0,63})\/([a-z0-9][a-z0-9._-]{0,63})\/([a-z0-9][a-z0-9._-]{0,127})$/u;
|
|
9
|
+
|
|
10
|
+
export type VaultKey = `${string}/${string}`;
|
|
11
|
+
export type QualifiedDocumentUri = `kb://${string}/${string}/${string}`;
|
|
12
|
+
|
|
13
|
+
export type PortfolioVaultIdentity = {
|
|
14
|
+
readonly owner: string;
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly key: VaultKey;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type StablePortfolioDocumentIdentity = {
|
|
20
|
+
readonly kind: "stable";
|
|
21
|
+
readonly stable: true;
|
|
22
|
+
readonly vault: PortfolioVaultIdentity;
|
|
23
|
+
readonly documentId: string;
|
|
24
|
+
readonly uri: QualifiedDocumentUri;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type LegacyPathPortfolioDocumentIdentity = {
|
|
28
|
+
readonly kind: "legacy-path";
|
|
29
|
+
readonly stable: false;
|
|
30
|
+
readonly vault: PortfolioVaultIdentity;
|
|
31
|
+
readonly path: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type PortfolioDocumentIdentity =
|
|
35
|
+
| StablePortfolioDocumentIdentity
|
|
36
|
+
| LegacyPathPortfolioDocumentIdentity;
|
|
37
|
+
|
|
38
|
+
export type DocumentIdState =
|
|
39
|
+
| { readonly kind: "missing" }
|
|
40
|
+
| { readonly kind: "invalid"; readonly value: MetadataValue }
|
|
41
|
+
| { readonly kind: "valid"; readonly documentId: string };
|
|
42
|
+
|
|
43
|
+
function boundedAsciiName(value: unknown, label: string): string {
|
|
44
|
+
if (typeof value !== "string" || !namePattern.test(value)) {
|
|
45
|
+
throw new TypeError(
|
|
46
|
+
`${label} must be a canonical lowercase ASCII name using letters, digits, dots, underscores, or hyphens.`,
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (Buffer.byteLength(value, "utf8") > MAX_PORTFOLIO_NAME_BYTES) {
|
|
50
|
+
throw new RangeError(`${label} must be at most ${MAX_PORTFOLIO_NAME_BYTES} UTF-8 bytes.`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Parse an explicit logical vault identity. It is never derived from a path or Git remote. */
|
|
56
|
+
export function portfolioVaultIdentity(owner: unknown, id: unknown): PortfolioVaultIdentity {
|
|
57
|
+
const checkedOwner = boundedAsciiName(owner, "Portfolio vault owner");
|
|
58
|
+
const checkedId = boundedAsciiName(id, "Portfolio vault ID");
|
|
59
|
+
return Object.freeze({
|
|
60
|
+
owner: checkedOwner,
|
|
61
|
+
id: checkedId,
|
|
62
|
+
key: `${checkedOwner}/${checkedId}` as VaultKey,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parseVaultKey(value: unknown): PortfolioVaultIdentity {
|
|
67
|
+
if (typeof value !== "string") throw new TypeError("Portfolio vault key must be a string.");
|
|
68
|
+
const separator = value.indexOf("/");
|
|
69
|
+
if (separator < 1 || separator !== value.lastIndexOf("/")) {
|
|
70
|
+
throw new TypeError("Portfolio vault key must have the canonical owner/id form.");
|
|
71
|
+
}
|
|
72
|
+
const identity = portfolioVaultIdentity(value.slice(0, separator), value.slice(separator + 1));
|
|
73
|
+
if (identity.key !== value) {
|
|
74
|
+
throw new TypeError("Portfolio vault key must have the canonical owner/id form.");
|
|
75
|
+
}
|
|
76
|
+
return identity;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function parseDocumentId(value: unknown): string {
|
|
80
|
+
if (typeof value !== "string" || !documentIdPattern.test(value)) {
|
|
81
|
+
throw new TypeError(
|
|
82
|
+
"document_id must be a canonical lowercase ASCII ID using letters, digits, dots, underscores, or hyphens.",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (Buffer.byteLength(value, "utf8") > MAX_DOCUMENT_ID_BYTES) {
|
|
86
|
+
throw new RangeError(`document_id must be at most ${MAX_DOCUMENT_ID_BYTES} UTF-8 bytes.`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function metadataValues(
|
|
92
|
+
metadata: MetadataObject,
|
|
93
|
+
normalizedName: string,
|
|
94
|
+
): readonly MetadataValue[] {
|
|
95
|
+
return Object.entries(metadata)
|
|
96
|
+
.filter(([name]) => name.normalize("NFC").toLocaleLowerCase("en-US") === normalizedName)
|
|
97
|
+
.map(([, value]) => value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Inspect authored metadata without converting an invalid or absent ID into a stable identity. */
|
|
101
|
+
export function documentIdState(metadata: MetadataObject): DocumentIdState {
|
|
102
|
+
const values = metadataValues(metadata, "document_id");
|
|
103
|
+
if (values.length === 0) return Object.freeze({ kind: "missing" });
|
|
104
|
+
if (values.length !== 1) {
|
|
105
|
+
return Object.freeze({ kind: "invalid", value: Object.freeze([...values]) });
|
|
106
|
+
}
|
|
107
|
+
const value = values[0]!;
|
|
108
|
+
try {
|
|
109
|
+
return Object.freeze({ kind: "valid", documentId: parseDocumentId(value) });
|
|
110
|
+
} catch {
|
|
111
|
+
return Object.freeze({ kind: "invalid", value });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatQualifiedDocumentUri(
|
|
116
|
+
vault: Pick<PortfolioVaultIdentity, "owner" | "id">,
|
|
117
|
+
documentId: unknown,
|
|
118
|
+
): QualifiedDocumentUri {
|
|
119
|
+
const checkedVault = portfolioVaultIdentity(vault.owner, vault.id);
|
|
120
|
+
const checkedDocumentId = parseDocumentId(documentId);
|
|
121
|
+
return `kb://${checkedVault.owner}/${checkedVault.id}/${checkedDocumentId}` as QualifiedDocumentUri;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Parse only byte-canonical qualified identities; URL normalization and percent encoding are rejected. */
|
|
125
|
+
export function parseQualifiedDocumentUri(value: unknown): StablePortfolioDocumentIdentity {
|
|
126
|
+
if (typeof value !== "string") throw new TypeError("Qualified document URI must be a string.");
|
|
127
|
+
const match = qualifiedUriPattern.exec(value);
|
|
128
|
+
if (match === null) {
|
|
129
|
+
throw new TypeError("Qualified document URI must have canonical kb://owner/vault/document_id form.");
|
|
130
|
+
}
|
|
131
|
+
const vault = portfolioVaultIdentity(match[1], match[2]);
|
|
132
|
+
const documentId = parseDocumentId(match[3]);
|
|
133
|
+
const uri = formatQualifiedDocumentUri(vault, documentId);
|
|
134
|
+
if (uri !== value) {
|
|
135
|
+
throw new TypeError("Qualified document URI must be byte-canonical.");
|
|
136
|
+
}
|
|
137
|
+
return Object.freeze({ kind: "stable", stable: true, vault, documentId, uri });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function portfolioDocumentIdentity(
|
|
141
|
+
vault: Pick<PortfolioVaultIdentity, "owner" | "id">,
|
|
142
|
+
path: string,
|
|
143
|
+
metadata: MetadataObject,
|
|
144
|
+
): PortfolioDocumentIdentity {
|
|
145
|
+
const checkedVault = portfolioVaultIdentity(vault.owner, vault.id);
|
|
146
|
+
const state = documentIdState(metadata);
|
|
147
|
+
if (state.kind === "valid") {
|
|
148
|
+
return Object.freeze({
|
|
149
|
+
kind: "stable",
|
|
150
|
+
stable: true,
|
|
151
|
+
vault: checkedVault,
|
|
152
|
+
documentId: state.documentId,
|
|
153
|
+
uri: formatQualifiedDocumentUri(checkedVault, state.documentId),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return Object.freeze({
|
|
157
|
+
kind: "legacy-path",
|
|
158
|
+
stable: false,
|
|
159
|
+
vault: checkedVault,
|
|
160
|
+
path,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { lstat, open, realpath } from "node:fs/promises";
|
|
3
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
parseVaultKey,
|
|
7
|
+
portfolioVaultIdentity,
|
|
8
|
+
type PortfolioVaultIdentity,
|
|
9
|
+
type VaultKey,
|
|
10
|
+
} from "./portfolio-identity.js";
|
|
11
|
+
|
|
12
|
+
export const PORTFOLIO_REGISTRY_CONTRACT = "hraness.kb-portfolio/v1";
|
|
13
|
+
export const MAX_PORTFOLIO_REGISTRY_BYTES = 1_024 * 1_024;
|
|
14
|
+
export const MAX_PORTFOLIO_VAULTS = 128;
|
|
15
|
+
export const MAX_AUTHORIZED_VAULTS = 32;
|
|
16
|
+
export const MAX_AUTHORITY_GROUPS = 64;
|
|
17
|
+
const MAX_PATH_BYTES = 2_048;
|
|
18
|
+
const MAX_TEXT_BYTES = 1_024;
|
|
19
|
+
|
|
20
|
+
export type PortfolioVaultRole =
|
|
21
|
+
| "archive"
|
|
22
|
+
| "portfolio"
|
|
23
|
+
| "repository"
|
|
24
|
+
| "sample"
|
|
25
|
+
| "template";
|
|
26
|
+
|
|
27
|
+
export type PortfolioVaultVisibility =
|
|
28
|
+
| "organization"
|
|
29
|
+
| "personal"
|
|
30
|
+
| "private"
|
|
31
|
+
| "public";
|
|
32
|
+
|
|
33
|
+
export type PortfolioVaultEntry = PortfolioVaultIdentity & {
|
|
34
|
+
/** Logical repository identity; it need not equal the checkout directory. */
|
|
35
|
+
readonly repository: VaultKey;
|
|
36
|
+
/** Workspace-relative repository checkout. */
|
|
37
|
+
readonly checkout: string;
|
|
38
|
+
/** Repository-relative vault root. */
|
|
39
|
+
readonly root: string;
|
|
40
|
+
readonly role: PortfolioVaultRole;
|
|
41
|
+
readonly visibility: PortfolioVaultVisibility;
|
|
42
|
+
readonly defaultRef?: string;
|
|
43
|
+
readonly parserVersion: 1;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type PortfolioAuthorityGroup = {
|
|
47
|
+
readonly id: string;
|
|
48
|
+
readonly members: readonly VaultKey[];
|
|
49
|
+
readonly state: "resolved" | "unresolved";
|
|
50
|
+
readonly canonical?: VaultKey;
|
|
51
|
+
readonly protected?: boolean;
|
|
52
|
+
readonly reason?: string;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type PortfolioRegistryV1 = {
|
|
56
|
+
readonly contract: typeof PORTFOLIO_REGISTRY_CONTRACT;
|
|
57
|
+
readonly schemaVersion: 1;
|
|
58
|
+
readonly schema?: string;
|
|
59
|
+
readonly vaults: readonly PortfolioVaultEntry[];
|
|
60
|
+
readonly authorityGroups: readonly PortfolioAuthorityGroup[];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type ResolvedPortfolioVault = {
|
|
64
|
+
readonly entry: PortfolioVaultEntry;
|
|
65
|
+
readonly repositoryRoot: string;
|
|
66
|
+
readonly root: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type PathMetadata = {
|
|
70
|
+
readonly size: number | bigint;
|
|
71
|
+
readonly nlink: number | bigint;
|
|
72
|
+
isDirectory(): boolean;
|
|
73
|
+
isFile(): boolean;
|
|
74
|
+
isSymbolicLink(): boolean;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type PortfolioRegistryFileDependencies = {
|
|
78
|
+
readonly readRegistryFile?: (path: string, maximumBytes: number) => Promise<string>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type PortfolioPathDependencies = {
|
|
82
|
+
readonly lstat?: (path: string) => Promise<PathMetadata>;
|
|
83
|
+
readonly realpath?: (path: string) => Promise<string>;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
87
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertKeys(
|
|
91
|
+
value: Readonly<Record<string, unknown>>,
|
|
92
|
+
allowed: ReadonlySet<string>,
|
|
93
|
+
required: ReadonlySet<string>,
|
|
94
|
+
label: string,
|
|
95
|
+
): void {
|
|
96
|
+
for (const key of Object.keys(value)) {
|
|
97
|
+
if (!allowed.has(key)) throw new TypeError(`${label} has unknown property ${JSON.stringify(key)}.`);
|
|
98
|
+
}
|
|
99
|
+
for (const key of required) {
|
|
100
|
+
if (!Object.hasOwn(value, key)) throw new TypeError(`${label} is missing property ${JSON.stringify(key)}.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function boundedString(value: unknown, maximum: number, label: string): string {
|
|
105
|
+
if (typeof value !== "string") throw new TypeError(`${label} must be a string.`);
|
|
106
|
+
if (value.normalize("NFC") !== value) throw new TypeError(`${label} must use NFC-normalized text.`);
|
|
107
|
+
if ([...value].some((character) => {
|
|
108
|
+
const code = character.codePointAt(0) ?? 0;
|
|
109
|
+
return code <= 0x1f || code === 0x7f;
|
|
110
|
+
})) {
|
|
111
|
+
throw new TypeError(`${label} must not contain control characters.`);
|
|
112
|
+
}
|
|
113
|
+
if (Buffer.byteLength(value, "utf8") > maximum) {
|
|
114
|
+
throw new RangeError(`${label} must be at most ${maximum} UTF-8 bytes.`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function registryPath(value: unknown, label: string, allowDot: boolean): string {
|
|
120
|
+
const path = boundedString(value, MAX_PATH_BYTES, label);
|
|
121
|
+
if (path === "." && allowDot) return path;
|
|
122
|
+
if (
|
|
123
|
+
path === ""
|
|
124
|
+
|| path === "."
|
|
125
|
+
|| path.startsWith("/")
|
|
126
|
+
|| path.endsWith("/")
|
|
127
|
+
|| path.includes("\\")
|
|
128
|
+
) {
|
|
129
|
+
throw new TypeError(`${label} must be a canonical relative POSIX path.`);
|
|
130
|
+
}
|
|
131
|
+
const segments = path.split("/");
|
|
132
|
+
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
|
|
133
|
+
throw new TypeError(`${label} must not contain empty, dot, or parent segments.`);
|
|
134
|
+
}
|
|
135
|
+
return path;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function enumValue<T extends string>(
|
|
139
|
+
value: unknown,
|
|
140
|
+
values: readonly T[],
|
|
141
|
+
label: string,
|
|
142
|
+
): T {
|
|
143
|
+
if (typeof value !== "string" || !values.includes(value as T)) {
|
|
144
|
+
throw new TypeError(`${label} must be one of ${values.map((item) => JSON.stringify(item)).join(", ")}.`);
|
|
145
|
+
}
|
|
146
|
+
return value as T;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseVaultEntry(value: unknown, index: number): PortfolioVaultEntry {
|
|
150
|
+
const label = `Portfolio vault ${index + 1}`;
|
|
151
|
+
if (!isRecord(value)) throw new TypeError(`${label} must be an object.`);
|
|
152
|
+
assertKeys(
|
|
153
|
+
value,
|
|
154
|
+
new Set([
|
|
155
|
+
"owner", "id", "repository", "checkout", "root", "role", "visibility",
|
|
156
|
+
"defaultRef", "parserVersion",
|
|
157
|
+
]),
|
|
158
|
+
new Set([
|
|
159
|
+
"owner", "id", "repository", "checkout", "root", "role", "visibility",
|
|
160
|
+
"parserVersion",
|
|
161
|
+
]),
|
|
162
|
+
label,
|
|
163
|
+
);
|
|
164
|
+
const identity = portfolioVaultIdentity(value.owner, value.id);
|
|
165
|
+
const repository = parseVaultKey(value.repository).key;
|
|
166
|
+
if (value.parserVersion !== 1) throw new TypeError(`${label} parserVersion must be exactly 1.`);
|
|
167
|
+
const defaultRef = value.defaultRef === undefined
|
|
168
|
+
? undefined
|
|
169
|
+
: boundedString(value.defaultRef, MAX_TEXT_BYTES, `${label} defaultRef`);
|
|
170
|
+
if (defaultRef === "") throw new TypeError(`${label} defaultRef must not be empty.`);
|
|
171
|
+
return Object.freeze({
|
|
172
|
+
...identity,
|
|
173
|
+
repository,
|
|
174
|
+
checkout: registryPath(value.checkout, `${label} checkout`, false),
|
|
175
|
+
root: registryPath(value.root, `${label} root`, true),
|
|
176
|
+
role: enumValue(
|
|
177
|
+
value.role,
|
|
178
|
+
["archive", "portfolio", "repository", "sample", "template"],
|
|
179
|
+
`${label} role`,
|
|
180
|
+
),
|
|
181
|
+
visibility: enumValue(
|
|
182
|
+
value.visibility,
|
|
183
|
+
["organization", "personal", "private", "public"],
|
|
184
|
+
`${label} visibility`,
|
|
185
|
+
),
|
|
186
|
+
...(defaultRef === undefined ? {} : { defaultRef }),
|
|
187
|
+
parserVersion: 1,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function parseAuthorityGroup(
|
|
192
|
+
value: unknown,
|
|
193
|
+
index: number,
|
|
194
|
+
vaultKeys: ReadonlySet<VaultKey>,
|
|
195
|
+
): PortfolioAuthorityGroup {
|
|
196
|
+
const label = `Portfolio authority group ${index + 1}`;
|
|
197
|
+
if (!isRecord(value)) throw new TypeError(`${label} must be an object.`);
|
|
198
|
+
assertKeys(
|
|
199
|
+
value,
|
|
200
|
+
new Set(["id", "members", "state", "canonical", "protected", "reason"]),
|
|
201
|
+
new Set(["id", "members", "state"]),
|
|
202
|
+
label,
|
|
203
|
+
);
|
|
204
|
+
const id = portfolioVaultIdentity("authority", value.id).id;
|
|
205
|
+
if (!Array.isArray(value.members) || value.members.length < 2 || value.members.length > MAX_AUTHORIZED_VAULTS) {
|
|
206
|
+
throw new RangeError(`${label} members must contain 2 through ${MAX_AUTHORIZED_VAULTS} vault keys.`);
|
|
207
|
+
}
|
|
208
|
+
const members = value.members.map((member) => parseVaultKey(member).key);
|
|
209
|
+
if (new Set(members).size !== members.length) throw new TypeError(`${label} members must be unique.`);
|
|
210
|
+
for (const member of members) {
|
|
211
|
+
if (!vaultKeys.has(member)) throw new TypeError(`${label} references unknown vault ${JSON.stringify(member)}.`);
|
|
212
|
+
}
|
|
213
|
+
const state = enumValue(value.state, ["resolved", "unresolved"], `${label} state`);
|
|
214
|
+
const canonical = value.canonical === undefined ? undefined : parseVaultKey(value.canonical).key;
|
|
215
|
+
if (state === "resolved") {
|
|
216
|
+
if (canonical === undefined || !members.includes(canonical)) {
|
|
217
|
+
throw new TypeError(`${label} resolved state requires a canonical member.`);
|
|
218
|
+
}
|
|
219
|
+
} else if (canonical !== undefined) {
|
|
220
|
+
throw new TypeError(`${label} unresolved state must not declare a canonical member.`);
|
|
221
|
+
}
|
|
222
|
+
if (value.protected !== undefined && typeof value.protected !== "boolean") {
|
|
223
|
+
throw new TypeError(`${label} protected must be a boolean.`);
|
|
224
|
+
}
|
|
225
|
+
const reason = value.reason === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: boundedString(value.reason, MAX_TEXT_BYTES, `${label} reason`);
|
|
228
|
+
if (reason === "") throw new TypeError(`${label} reason must not be empty.`);
|
|
229
|
+
return Object.freeze({
|
|
230
|
+
id,
|
|
231
|
+
members: Object.freeze(members),
|
|
232
|
+
state,
|
|
233
|
+
...(canonical === undefined ? {} : { canonical }),
|
|
234
|
+
...(value.protected === undefined ? {} : { protected: value.protected }),
|
|
235
|
+
...(reason === undefined ? {} : { reason }),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Parse a foreign JSON value into the strict, bounded v1 portfolio registry contract. */
|
|
240
|
+
export function parsePortfolioRegistry(value: unknown): PortfolioRegistryV1 {
|
|
241
|
+
if (!isRecord(value)) throw new TypeError("Portfolio registry must be an object.");
|
|
242
|
+
assertKeys(
|
|
243
|
+
value,
|
|
244
|
+
new Set(["$schema", "contract", "schemaVersion", "vaults", "authorityGroups"]),
|
|
245
|
+
new Set(["contract", "schemaVersion", "vaults"]),
|
|
246
|
+
"Portfolio registry",
|
|
247
|
+
);
|
|
248
|
+
if (value.contract !== PORTFOLIO_REGISTRY_CONTRACT) {
|
|
249
|
+
throw new TypeError(`Portfolio registry contract must be ${JSON.stringify(PORTFOLIO_REGISTRY_CONTRACT)}.`);
|
|
250
|
+
}
|
|
251
|
+
if (value.schemaVersion !== 1) throw new TypeError("Portfolio registry schemaVersion must be exactly 1.");
|
|
252
|
+
if (!Array.isArray(value.vaults) || value.vaults.length > MAX_PORTFOLIO_VAULTS) {
|
|
253
|
+
throw new RangeError(`Portfolio registry vaults must contain at most ${MAX_PORTFOLIO_VAULTS} entries.`);
|
|
254
|
+
}
|
|
255
|
+
const vaults = value.vaults.map(parseVaultEntry);
|
|
256
|
+
const vaultKeys = new Set(vaults.map(({ key }) => key));
|
|
257
|
+
if (vaultKeys.size !== vaults.length) throw new TypeError("Portfolio registry vault keys must be unique.");
|
|
258
|
+
const rawGroups = value.authorityGroups ?? [];
|
|
259
|
+
if (!Array.isArray(rawGroups) || rawGroups.length > MAX_AUTHORITY_GROUPS) {
|
|
260
|
+
throw new RangeError(`Portfolio authorityGroups must contain at most ${MAX_AUTHORITY_GROUPS} entries.`);
|
|
261
|
+
}
|
|
262
|
+
const authorityGroups = rawGroups.map((group, index) =>
|
|
263
|
+
parseAuthorityGroup(group, index, vaultKeys));
|
|
264
|
+
if (new Set(authorityGroups.map(({ id }) => id)).size !== authorityGroups.length) {
|
|
265
|
+
throw new TypeError("Portfolio authority group IDs must be unique.");
|
|
266
|
+
}
|
|
267
|
+
const schema = value.$schema === undefined
|
|
268
|
+
? undefined
|
|
269
|
+
: boundedString(value.$schema, MAX_TEXT_BYTES, "Portfolio registry $schema");
|
|
270
|
+
return Object.freeze({
|
|
271
|
+
contract: PORTFOLIO_REGISTRY_CONTRACT,
|
|
272
|
+
schemaVersion: 1,
|
|
273
|
+
...(schema === undefined ? {} : { schema }),
|
|
274
|
+
vaults: Object.freeze(vaults),
|
|
275
|
+
authorityGroups: Object.freeze(authorityGroups),
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Revalidate and detach an already parsed registry before crossing an authorization boundary. */
|
|
280
|
+
export function snapshotPortfolioRegistry(registry: PortfolioRegistryV1): PortfolioRegistryV1 {
|
|
281
|
+
if (!isRecord(registry)) throw new TypeError("Portfolio registry snapshot must be an object.");
|
|
282
|
+
return parsePortfolioRegistry({
|
|
283
|
+
contract: registry.contract,
|
|
284
|
+
schemaVersion: registry.schemaVersion,
|
|
285
|
+
...(registry.schema === undefined ? {} : { $schema: registry.schema }),
|
|
286
|
+
vaults: Array.isArray(registry.vaults)
|
|
287
|
+
? registry.vaults.map((entry) => ({
|
|
288
|
+
owner: entry.owner,
|
|
289
|
+
id: entry.id,
|
|
290
|
+
repository: entry.repository,
|
|
291
|
+
checkout: entry.checkout,
|
|
292
|
+
root: entry.root,
|
|
293
|
+
role: entry.role,
|
|
294
|
+
visibility: entry.visibility,
|
|
295
|
+
...(entry.defaultRef === undefined ? {} : { defaultRef: entry.defaultRef }),
|
|
296
|
+
parserVersion: entry.parserVersion,
|
|
297
|
+
}))
|
|
298
|
+
: registry.vaults,
|
|
299
|
+
authorityGroups: Array.isArray(registry.authorityGroups)
|
|
300
|
+
? registry.authorityGroups.map((group) => ({
|
|
301
|
+
id: group.id,
|
|
302
|
+
members: group.members,
|
|
303
|
+
state: group.state,
|
|
304
|
+
...(group.canonical === undefined ? {} : { canonical: group.canonical }),
|
|
305
|
+
...(group.protected === undefined ? {} : { protected: group.protected }),
|
|
306
|
+
...(group.reason === undefined ? {} : { reason: group.reason }),
|
|
307
|
+
}))
|
|
308
|
+
: registry.authorityGroups,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function defaultReadRegistryFile(path: string, maximumBytes: number): Promise<string> {
|
|
313
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
314
|
+
try {
|
|
315
|
+
const before = await handle.stat({ bigint: true });
|
|
316
|
+
if (!before.isFile()) throw new Error("Portfolio registry must be a regular file.");
|
|
317
|
+
if (before.nlink !== 1n) throw new Error("Portfolio registry must not be hard-linked.");
|
|
318
|
+
if (before.size > BigInt(maximumBytes)) {
|
|
319
|
+
throw new RangeError(`Portfolio registry must be at most ${maximumBytes} UTF-8 bytes.`);
|
|
320
|
+
}
|
|
321
|
+
const chunks: Uint8Array[] = [];
|
|
322
|
+
let observedBytes = 0;
|
|
323
|
+
for (;;) {
|
|
324
|
+
const buffer = new Uint8Array(Math.min(64 * 1_024, maximumBytes - observedBytes + 1));
|
|
325
|
+
const read = await handle.read(buffer, 0, buffer.byteLength, null);
|
|
326
|
+
if (read.bytesRead === 0) break;
|
|
327
|
+
observedBytes += read.bytesRead;
|
|
328
|
+
if (observedBytes > maximumBytes) {
|
|
329
|
+
throw new RangeError(`Portfolio registry must be at most ${maximumBytes} UTF-8 bytes.`);
|
|
330
|
+
}
|
|
331
|
+
chunks.push(buffer.slice(0, read.bytesRead));
|
|
332
|
+
}
|
|
333
|
+
const after = await handle.stat({ bigint: true });
|
|
334
|
+
if (
|
|
335
|
+
after.dev !== before.dev
|
|
336
|
+
|| after.ino !== before.ino
|
|
337
|
+
|| after.size !== before.size
|
|
338
|
+
|| after.mtimeNs !== before.mtimeNs
|
|
339
|
+
|| after.size !== BigInt(observedBytes)
|
|
340
|
+
) {
|
|
341
|
+
throw new Error("Portfolio registry changed while it was being read; retry.");
|
|
342
|
+
}
|
|
343
|
+
const bytes = new Uint8Array(observedBytes);
|
|
344
|
+
let offset = 0;
|
|
345
|
+
for (const chunk of chunks) {
|
|
346
|
+
bytes.set(chunk, offset);
|
|
347
|
+
offset += chunk.byteLength;
|
|
348
|
+
}
|
|
349
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
350
|
+
} finally {
|
|
351
|
+
await handle.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export async function loadPortfolioRegistry(
|
|
356
|
+
path: string,
|
|
357
|
+
dependencies: PortfolioRegistryFileDependencies = {},
|
|
358
|
+
): Promise<PortfolioRegistryV1> {
|
|
359
|
+
const source = await (dependencies.readRegistryFile ?? defaultReadRegistryFile)(
|
|
360
|
+
resolve(path),
|
|
361
|
+
MAX_PORTFOLIO_REGISTRY_BYTES,
|
|
362
|
+
);
|
|
363
|
+
if (Buffer.byteLength(source, "utf8") > MAX_PORTFOLIO_REGISTRY_BYTES) {
|
|
364
|
+
throw new RangeError(`Portfolio registry must be at most ${MAX_PORTFOLIO_REGISTRY_BYTES} UTF-8 bytes.`);
|
|
365
|
+
}
|
|
366
|
+
let value: unknown;
|
|
367
|
+
try {
|
|
368
|
+
value = JSON.parse(source) as unknown;
|
|
369
|
+
} catch (error) {
|
|
370
|
+
throw new SyntaxError("Portfolio registry must be valid JSON.", { cause: error });
|
|
371
|
+
}
|
|
372
|
+
return parsePortfolioRegistry(value);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Select only explicitly authorized keys without listing or otherwise exposing available entries. */
|
|
376
|
+
export function selectAuthorizedVaults(
|
|
377
|
+
registry: PortfolioRegistryV1,
|
|
378
|
+
authorizedVaults: readonly VaultKey[],
|
|
379
|
+
): readonly PortfolioVaultEntry[] {
|
|
380
|
+
if (!Array.isArray(authorizedVaults) || authorizedVaults.length < 1 || authorizedVaults.length > MAX_AUTHORIZED_VAULTS) {
|
|
381
|
+
throw new RangeError(`authorizedVaults must contain 1 through ${MAX_AUTHORIZED_VAULTS} explicit vault keys.`);
|
|
382
|
+
}
|
|
383
|
+
const checked = authorizedVaults.map((key) => parseVaultKey(key).key);
|
|
384
|
+
if (new Set(checked).size !== checked.length) throw new TypeError("authorizedVaults must not contain duplicates.");
|
|
385
|
+
const byKey = new Map(registry.vaults.map((entry) => [entry.key, entry]));
|
|
386
|
+
for (const key of checked) {
|
|
387
|
+
if (!byKey.has(key)) {
|
|
388
|
+
throw new Error(`Authorized portfolio vault ${JSON.stringify(key)} is not available.`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const selected = new Set(checked);
|
|
392
|
+
return Object.freeze(registry.vaults.filter(({ key }) => selected.has(key)));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function insideOrSame(parent: string, child: string): boolean {
|
|
396
|
+
const fromParent = relative(parent, child);
|
|
397
|
+
return fromParent === "" || (fromParent !== ".." && !fromParent.startsWith(`..${sep}`));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function assertDirectoryChain(
|
|
401
|
+
base: string,
|
|
402
|
+
path: string,
|
|
403
|
+
metadata: (path: string) => Promise<PathMetadata>,
|
|
404
|
+
label: string,
|
|
405
|
+
): Promise<void> {
|
|
406
|
+
const segments = path === "." ? [] : path.split("/");
|
|
407
|
+
let current = base;
|
|
408
|
+
for (const segment of segments) {
|
|
409
|
+
current = join(current, segment);
|
|
410
|
+
const entry = await metadata(current);
|
|
411
|
+
if (entry.isSymbolicLink()) throw new Error(`${label} must not traverse a symbolic link.`);
|
|
412
|
+
if (!entry.isDirectory()) throw new Error(`${label} must contain directories only.`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Resolve one already-authorized registry entry through a caller-owned workspace. */
|
|
417
|
+
export async function resolvePortfolioVault(
|
|
418
|
+
entry: PortfolioVaultEntry,
|
|
419
|
+
workspaceRoot: string,
|
|
420
|
+
dependencies: PortfolioPathDependencies = {},
|
|
421
|
+
): Promise<ResolvedPortfolioVault> {
|
|
422
|
+
const metadata = dependencies.lstat ?? ((path: string) => lstat(path));
|
|
423
|
+
const canonical = dependencies.realpath ?? ((path: string) => realpath(path));
|
|
424
|
+
const requestedWorkspace = resolve(workspaceRoot);
|
|
425
|
+
const workspaceMetadata = await metadata(requestedWorkspace);
|
|
426
|
+
if (workspaceMetadata.isSymbolicLink()) throw new Error("Portfolio workspace root must not be a symbolic link.");
|
|
427
|
+
if (!workspaceMetadata.isDirectory()) throw new Error("Portfolio workspace root must be a directory.");
|
|
428
|
+
const workspace = await canonical(requestedWorkspace);
|
|
429
|
+
await assertDirectoryChain(workspace, entry.checkout, metadata, "Portfolio checkout");
|
|
430
|
+
const repositoryRoot = await canonical(join(workspace, ...entry.checkout.split("/")));
|
|
431
|
+
if (!insideOrSame(workspace, repositoryRoot)) throw new Error("Portfolio checkout resolves outside the workspace root.");
|
|
432
|
+
await assertDirectoryChain(repositoryRoot, entry.root, metadata, "Portfolio vault root");
|
|
433
|
+
const root = entry.root === "."
|
|
434
|
+
? repositoryRoot
|
|
435
|
+
: await canonical(join(repositoryRoot, ...entry.root.split("/")));
|
|
436
|
+
if (!insideOrSame(repositoryRoot, root)) throw new Error("Portfolio vault resolves outside its repository checkout.");
|
|
437
|
+
return Object.freeze({ entry, repositoryRoot, root });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function rootsOverlap(left: string, right: string): boolean {
|
|
441
|
+
return insideOrSame(left, right) || insideOrSame(right, left);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function validateResolvedPortfolioVaults(
|
|
445
|
+
vaults: readonly ResolvedPortfolioVault[],
|
|
446
|
+
): readonly ResolvedPortfolioVault[] {
|
|
447
|
+
for (let left = 0; left < vaults.length; left += 1) {
|
|
448
|
+
for (let right = left + 1; right < vaults.length; right += 1) {
|
|
449
|
+
const first = vaults[left];
|
|
450
|
+
const second = vaults[right];
|
|
451
|
+
if (first !== undefined && second !== undefined && rootsOverlap(first.root, second.root)) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`Authorized portfolio vault roots overlap: ${JSON.stringify(first.entry.key)} and ${JSON.stringify(second.entry.key)}.`,
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return Object.freeze([...vaults]);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export async function resolveAuthorizedVaults(
|
|
462
|
+
registry: PortfolioRegistryV1,
|
|
463
|
+
workspaceRoot: string,
|
|
464
|
+
authorizedVaults: readonly VaultKey[],
|
|
465
|
+
dependencies: PortfolioPathDependencies = {},
|
|
466
|
+
): Promise<readonly ResolvedPortfolioVault[]> {
|
|
467
|
+
const selected = selectAuthorizedVaults(registry, authorizedVaults);
|
|
468
|
+
const resolved: ResolvedPortfolioVault[] = [];
|
|
469
|
+
for (const entry of selected) {
|
|
470
|
+
resolved.push(await resolvePortfolioVault(entry, workspaceRoot, dependencies));
|
|
471
|
+
}
|
|
472
|
+
return validateResolvedPortfolioVaults(resolved);
|
|
473
|
+
}
|