@clossys/launcher 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1373 -60
- package/contracts/conversation-contract.md +2 -2
- package/contracts/product-ci-workflow.yml +74 -0
- package/contracts/repository-inventory.json +53 -0
- package/dist/admission-fixture.d.ts +168 -0
- package/dist/admission-fixture.d.ts.map +1 -0
- package/dist/admission-fixture.js +467 -0
- package/dist/admission-fixture.js.map +1 -0
- package/dist/admission.d.ts +124 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +804 -0
- package/dist/admission.js.map +1 -0
- package/dist/agents-guide.d.ts +9 -0
- package/dist/agents-guide.d.ts.map +1 -0
- package/dist/agents-guide.js +26 -0
- package/dist/agents-guide.js.map +1 -0
- package/dist/apply-command-options.check.d.ts +12 -0
- package/dist/apply-command-options.check.d.ts.map +1 -0
- package/dist/apply-command-options.check.js +20 -0
- package/dist/apply-command-options.check.js.map +1 -0
- package/dist/apply-plan-cli.d.ts +39 -1
- package/dist/apply-plan-cli.d.ts.map +1 -1
- package/dist/apply-plan-cli.js +432 -15
- package/dist/apply-plan-cli.js.map +1 -1
- package/dist/apply-plan.d.ts +46 -59
- package/dist/apply-plan.d.ts.map +1 -1
- package/dist/apply-plan.js +112 -97
- package/dist/apply-plan.js.map +1 -1
- package/dist/apply-step-fixture.d.ts +87 -0
- package/dist/apply-step-fixture.d.ts.map +1 -0
- package/dist/apply-step-fixture.js +199 -0
- package/dist/apply-step-fixture.js.map +1 -0
- package/dist/apply-store.d.ts +93 -0
- package/dist/apply-store.d.ts.map +1 -0
- package/dist/apply-store.js +625 -0
- package/dist/apply-store.js.map +1 -0
- package/dist/approval-sheet.d.ts +21 -0
- package/dist/approval-sheet.d.ts.map +1 -0
- package/dist/approval-sheet.js +163 -0
- package/dist/approval-sheet.js.map +1 -0
- package/dist/body-command.d.ts +42 -0
- package/dist/body-command.d.ts.map +1 -0
- package/dist/body-command.js +143 -0
- package/dist/body-command.js.map +1 -0
- package/dist/change-set-contract.d.ts +403 -0
- package/dist/change-set-contract.d.ts.map +1 -0
- package/dist/change-set-contract.js +781 -0
- package/dist/change-set-contract.js.map +1 -0
- package/dist/change-set-digest.d.ts +28 -0
- package/dist/change-set-digest.d.ts.map +1 -0
- package/dist/change-set-digest.js +65 -0
- package/dist/change-set-digest.js.map +1 -0
- package/dist/check-cli.d.ts.map +1 -1
- package/dist/check-cli.js +14 -3
- package/dist/check-cli.js.map +1 -1
- package/dist/cli.d.ts +17 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -23
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +79 -22
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +843 -268
- package/dist/core.js.map +1 -1
- package/dist/dry-materialize.d.ts +63 -0
- package/dist/dry-materialize.d.ts.map +1 -0
- package/dist/dry-materialize.js +330 -0
- package/dist/dry-materialize.js.map +1 -0
- package/dist/existing-declaration-adoption.check.d.ts +2 -0
- package/dist/existing-declaration-adoption.check.d.ts.map +1 -0
- package/dist/existing-declaration-adoption.check.js +10 -0
- package/dist/existing-declaration-adoption.check.js.map +1 -0
- package/dist/generated/contract-schema.generated.d.ts +97 -0
- package/dist/generated/contract-schema.generated.d.ts.map +1 -0
- package/dist/generated/contract-schema.generated.js +496 -0
- package/dist/generated/contract-schema.generated.js.map +1 -0
- package/dist/generated/package-scope.generated.d.ts +6 -0
- package/dist/generated/package-scope.generated.d.ts.map +1 -0
- package/dist/generated/package-scope.generated.js +10 -0
- package/dist/generated/package-scope.generated.js.map +1 -0
- package/dist/generated/plan-contracts.generated.d.ts +3 -0
- package/dist/generated/plan-contracts.generated.d.ts.map +1 -0
- package/dist/generated/plan-contracts.generated.js +3101 -0
- package/dist/generated/plan-contracts.generated.js.map +1 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +11 -0
- package/dist/host.js.map +1 -1
- package/dist/identity.d.ts +15 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +48 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +35 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory-adoption.d.ts +24 -5
- package/dist/inventory-adoption.d.ts.map +1 -1
- package/dist/inventory-adoption.js +70 -25
- package/dist/inventory-adoption.js.map +1 -1
- package/dist/inventory-choice.d.ts +40 -0
- package/dist/inventory-choice.d.ts.map +1 -0
- package/dist/inventory-choice.js +156 -0
- package/dist/inventory-choice.js.map +1 -0
- package/dist/inventory-contract.d.ts +89 -0
- package/dist/inventory-contract.d.ts.map +1 -0
- package/dist/inventory-contract.js +121 -0
- package/dist/inventory-contract.js.map +1 -0
- package/dist/key-editor.d.ts +30 -0
- package/dist/key-editor.d.ts.map +1 -0
- package/dist/key-editor.js +445 -0
- package/dist/key-editor.js.map +1 -0
- package/dist/ledger-contract.d.ts +190 -0
- package/dist/ledger-contract.d.ts.map +1 -0
- package/dist/ledger-contract.js +555 -0
- package/dist/ledger-contract.js.map +1 -0
- package/dist/ledger-trust.d.ts +90 -0
- package/dist/ledger-trust.d.ts.map +1 -0
- package/dist/ledger-trust.js +203 -0
- package/dist/ledger-trust.js.map +1 -0
- package/dist/lockfile-invariants.d.ts +48 -0
- package/dist/lockfile-invariants.d.ts.map +1 -0
- package/dist/lockfile-invariants.js +375 -0
- package/dist/lockfile-invariants.js.map +1 -0
- package/dist/lockfile-readers.d.ts +72 -0
- package/dist/lockfile-readers.d.ts.map +1 -0
- package/dist/lockfile-readers.js +713 -0
- package/dist/lockfile-readers.js.map +1 -0
- package/dist/lockfile-regen.d.ts +106 -0
- package/dist/lockfile-regen.d.ts.map +1 -0
- package/dist/lockfile-regen.js +760 -0
- package/dist/lockfile-regen.js.map +1 -0
- package/dist/lockfile-tool-env.d.ts +29 -0
- package/dist/lockfile-tool-env.d.ts.map +1 -0
- package/dist/lockfile-tool-env.js +111 -0
- package/dist/lockfile-tool-env.js.map +1 -0
- package/dist/materialize.d.ts +113 -0
- package/dist/materialize.d.ts.map +1 -0
- package/dist/materialize.js +881 -0
- package/dist/materialize.js.map +1 -0
- package/dist/observe-repository.d.ts +90 -0
- package/dist/observe-repository.d.ts.map +1 -0
- package/dist/observe-repository.js +1367 -0
- package/dist/observe-repository.js.map +1 -0
- package/dist/plan-bundle-setup-fixture.d.ts +68 -0
- package/dist/plan-bundle-setup-fixture.d.ts.map +1 -0
- package/dist/plan-bundle-setup-fixture.js +167 -0
- package/dist/plan-bundle-setup-fixture.js.map +1 -0
- package/dist/plan-bundle.d.ts +256 -0
- package/dist/plan-bundle.d.ts.map +1 -0
- package/dist/plan-bundle.js +882 -0
- package/dist/plan-bundle.js.map +1 -0
- package/dist/plan-command.d.ts +29 -0
- package/dist/plan-command.d.ts.map +1 -0
- package/dist/plan-command.js +523 -0
- package/dist/plan-command.js.map +1 -0
- package/dist/plan-contract.d.ts +153 -0
- package/dist/plan-contract.d.ts.map +1 -0
- package/dist/plan-contract.js +61 -0
- package/dist/plan-contract.js.map +1 -0
- package/dist/plan-digest.d.ts +25 -0
- package/dist/plan-digest.d.ts.map +1 -0
- package/dist/plan-digest.js +106 -0
- package/dist/plan-digest.js.map +1 -0
- package/dist/plan-rules.d.ts +23 -0
- package/dist/plan-rules.d.ts.map +1 -0
- package/dist/plan-rules.js +177 -0
- package/dist/plan-rules.js.map +1 -0
- package/dist/planned-bundle.d.ts +20 -0
- package/dist/planned-bundle.d.ts.map +1 -0
- package/dist/planned-bundle.js +191 -0
- package/dist/planned-bundle.js.map +1 -0
- package/dist/product-repository.d.ts +4 -0
- package/dist/product-repository.d.ts.map +1 -1
- package/dist/product-repository.js +9 -1
- package/dist/product-repository.js.map +1 -1
- package/dist/provenance-gate.d.ts +48 -0
- package/dist/provenance-gate.d.ts.map +1 -0
- package/dist/provenance-gate.js +324 -0
- package/dist/provenance-gate.js.map +1 -0
- package/dist/pull-request-body.d.ts +45 -0
- package/dist/pull-request-body.d.ts.map +1 -0
- package/dist/pull-request-body.js +232 -0
- package/dist/pull-request-body.js.map +1 -0
- package/dist/registry-snapshot.d.ts +141 -0
- package/dist/registry-snapshot.d.ts.map +1 -0
- package/dist/registry-snapshot.js +483 -0
- package/dist/registry-snapshot.js.map +1 -0
- package/dist/release-age-edit.d.ts +52 -0
- package/dist/release-age-edit.d.ts.map +1 -0
- package/dist/release-age-edit.js +413 -0
- package/dist/release-age-edit.js.map +1 -0
- package/dist/root-entries.d.ts +36 -0
- package/dist/root-entries.d.ts.map +1 -0
- package/dist/root-entries.js +80 -0
- package/dist/root-entries.js.map +1 -0
- package/dist/setup-template-scripts.d.ts +36 -0
- package/dist/setup-template-scripts.d.ts.map +1 -0
- package/dist/setup-template-scripts.js +568 -0
- package/dist/setup-template-scripts.js.map +1 -0
- package/dist/setup-templates.d.ts +55 -0
- package/dist/setup-templates.d.ts.map +1 -0
- package/dist/setup-templates.js +438 -0
- package/dist/setup-templates.js.map +1 -0
- package/dist/skills.d.ts +34 -1
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +129 -17
- package/dist/skills.js.map +1 -1
- package/dist/status.d.ts +63 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +539 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +151 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/skeleton/README.md +14 -9
- package/skeleton/package.json +2 -1
- package/skill/SKILL.md +23 -7
- package/skill-catalogue/advisor/SKILL.md +59 -6
- package/skill-catalogue/architect/SKILL.md +2 -2
- package/skill-catalogue/bouncer/SKILL.md +2 -2
- package/skill-catalogue/builder/SKILL.md +2 -2
- package/skill-catalogue/butler/SKILL.md +2 -2
- package/skill-catalogue/controller/SKILL.md +2 -2
- package/skill-catalogue/customer/SKILL.md +2 -2
- package/skill-catalogue/designer/SKILL.md +4 -2
- package/skill-catalogue/giver/SKILL.md +2 -2
- package/skill-catalogue/influencer/SKILL.md +2 -2
- package/skill-catalogue/inspector/SKILL.md +2 -2
- package/skill-catalogue/integrator/SKILL.md +2 -2
- package/skill-catalogue/keeper/SKILL.md +2 -2
- package/skill-catalogue/launcher/SKILL.md +23 -7
- package/skill-catalogue/locksmith/SKILL.md +2 -2
- package/skill-catalogue/messenger/SKILL.md +2 -2
- package/skill-catalogue/observer/SKILL.md +2 -2
- package/skill-catalogue/publisher/SKILL.md +2 -2
- package/skill-catalogue/starter/SKILL.md +3 -2
- package/skill-catalogue/strategist/SKILL.md +12 -4
- package/skill-catalogue/writer/SKILL.md +2 -2
- package/src/admission-fixture.ts +585 -0
- package/src/admission.ts +819 -0
- package/src/agents-guide.ts +29 -0
- package/src/apply-command-options.check.ts +27 -0
- package/src/apply-plan-cli.ts +454 -14
- package/src/apply-plan.ts +112 -124
- package/src/apply-step-fixture.ts +236 -0
- package/src/apply-store.ts +584 -0
- package/src/approval-sheet.ts +170 -0
- package/src/body-command.ts +162 -0
- package/src/change-set-contract.ts +987 -0
- package/src/change-set-digest.ts +70 -0
- package/src/check-cli.ts +14 -3
- package/src/cli.ts +90 -22
- package/src/core.ts +973 -275
- package/src/dry-materialize.ts +353 -0
- package/src/existing-declaration-adoption.check.ts +12 -0
- package/src/generated/contract-schema.generated.ts +520 -0
- package/src/generated/package-scope.generated.ts +10 -0
- package/src/generated/plan-contracts.generated.ts +3101 -0
- package/src/host.ts +10 -0
- package/src/identity.ts +51 -0
- package/src/index.ts +74 -3
- package/src/inventory-adoption.ts +107 -29
- package/src/inventory-choice.ts +172 -0
- package/src/inventory-contract.ts +166 -0
- package/src/key-editor.ts +446 -0
- package/src/ledger-contract.ts +660 -0
- package/src/ledger-trust.ts +272 -0
- package/src/lockfile-invariants.ts +421 -0
- package/src/lockfile-readers.ts +749 -0
- package/src/lockfile-regen.ts +851 -0
- package/src/lockfile-tool-env.ts +131 -0
- package/src/materialize.ts +915 -0
- package/src/observe-repository.ts +1365 -0
- package/src/plan-bundle-setup-fixture.ts +200 -0
- package/src/plan-bundle.ts +1014 -0
- package/src/plan-command.ts +532 -0
- package/src/plan-contract.ts +179 -0
- package/src/plan-digest.ts +102 -0
- package/src/plan-rules.ts +188 -0
- package/src/planned-bundle.ts +211 -0
- package/src/product-repository.ts +10 -1
- package/src/provenance-gate.ts +352 -0
- package/src/pull-request-body.ts +261 -0
- package/src/registry-snapshot.ts +534 -0
- package/src/release-age-edit.ts +430 -0
- package/src/root-entries.ts +81 -0
- package/src/setup-template-scripts.ts +580 -0
- package/src/setup-templates.ts +479 -0
- package/src/skills.ts +161 -18
- package/src/status.ts +557 -0
- package/src/types.ts +148 -13
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** Where the snapshot is written by default, relative to the hub root. */
|
|
2
|
+
export declare const REGISTRY_SNAPSHOT_REL = "clossys/.state/apply/registry-snapshot.json";
|
|
3
|
+
/** The most bytes read from any one registry response (10 MiB). */
|
|
4
|
+
export declare const MAX_RESPONSE_BYTES: number;
|
|
5
|
+
/** How long one request, its body included, may take. */
|
|
6
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
7
|
+
/**
|
|
8
|
+
* The port every registry read goes through: the shape of Node's `fetch`,
|
|
9
|
+
* as Integrator's transport has. The request's URL and init are built here,
|
|
10
|
+
* never by the transport's caller, so tests inspect exactly what would be sent.
|
|
11
|
+
*/
|
|
12
|
+
export type Transport = (input: URL, init: RequestInit) => Promise<Response>;
|
|
13
|
+
/** Node's own `fetch`. It reads no npm configuration and adds no registry credential (a proxy URL's own username and password, if any, go only to that proxy). */
|
|
14
|
+
export declare const nodeFetchTransport: Transport;
|
|
15
|
+
/** One version of a package, projected from the registry's document. */
|
|
16
|
+
export interface RegistrySnapshotVersion {
|
|
17
|
+
readonly version: string;
|
|
18
|
+
readonly integrity: string | null;
|
|
19
|
+
readonly tarball: string;
|
|
20
|
+
readonly deprecated: boolean;
|
|
21
|
+
readonly publishedAt: string | null;
|
|
22
|
+
readonly hasAttestations: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** One requested package. */
|
|
25
|
+
export interface RegistrySnapshotPackage {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly status: "found" | "not-found";
|
|
28
|
+
readonly latest: string | null;
|
|
29
|
+
readonly versions: readonly RegistrySnapshotVersion[];
|
|
30
|
+
readonly responseSha256: string;
|
|
31
|
+
}
|
|
32
|
+
/** clossys/.state/apply/registry-snapshot.json (docs/contracts/registry-snapshot.json, in the public repository, not shipped in this package). */
|
|
33
|
+
export interface RegistrySnapshot {
|
|
34
|
+
readonly schemaVersion: 1;
|
|
35
|
+
readonly kind: "clossys.registry-snapshot";
|
|
36
|
+
readonly registry: string;
|
|
37
|
+
readonly fetchedAt: string;
|
|
38
|
+
readonly fetchedBy: {
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly version: string;
|
|
41
|
+
};
|
|
42
|
+
readonly packages: readonly RegistrySnapshotPackage[];
|
|
43
|
+
}
|
|
44
|
+
/** One reason a snapshot is refused: `rule` is "schema" for the contract's keywords, else the code rule's id. */
|
|
45
|
+
export interface RegistrySnapshotViolation {
|
|
46
|
+
readonly rule: "schema" | "N1" | "N2" | "N3";
|
|
47
|
+
/** The field at fault, like `packages[1].versions[0].integrity`, or "" for the document itself. */
|
|
48
|
+
readonly path: string;
|
|
49
|
+
/** What is wrong, by position only, never quoting a value or an undeclared key. */
|
|
50
|
+
readonly message: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Why the snapshot step stopped. The message names a package only by its
|
|
54
|
+
* position in the request (`names[<n>]`), never by its name: a name is
|
|
55
|
+
* request text, and a request is a file anyone could have written. It
|
|
56
|
+
* never carries registry content either.
|
|
57
|
+
*/
|
|
58
|
+
export declare class RegistrySnapshotError extends Error {
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Every violation of the registry snapshot contract: its schema, then, only
|
|
62
|
+
* for a snapshot whose shape is known good, its code rules N1-N3 (no package
|
|
63
|
+
* named twice, no version recorded twice for one package, no `latest` or
|
|
64
|
+
* versions for a package that was not found). Implemented separately from
|
|
65
|
+
* @clossys/advisor's reader; both are tested against the one corpus,
|
|
66
|
+
* docs/contracts/registry-snapshot.fixture.json (in the public repository,
|
|
67
|
+
* not shipped in this package). Never throws.
|
|
68
|
+
*/
|
|
69
|
+
export declare function registrySnapshotViolations(value: unknown): RegistrySnapshotViolation[];
|
|
70
|
+
/** The snapshot in the contract's canonical order: packages sorted by name, each package's versions sorted by version. */
|
|
71
|
+
export declare function canonicalRegistrySnapshot(snapshot: RegistrySnapshot): RegistrySnapshot;
|
|
72
|
+
/**
|
|
73
|
+
* A package name's registry path segment. Copied, unchanged, from
|
|
74
|
+
* `registryEncodedName` in @clossys/integrator's provenance-check.ts, which
|
|
75
|
+
* this package must not depend on at runtime; a test compares the two
|
|
76
|
+
* function bodies so they cannot drift apart. The leading `@` stays literal
|
|
77
|
+
* and only the slash is percent-encoded (`@scope%2Fname`), the convention
|
|
78
|
+
* npm-compatible registries share; `encodeURIComponent` on the whole name
|
|
79
|
+
* would also encode the `@`, which none of them expect.
|
|
80
|
+
*/
|
|
81
|
+
export declare function registryEncodedName(name: string): string;
|
|
82
|
+
/** The full registry document's URL for `name`: `{registry}/{encoded name}`. */
|
|
83
|
+
export declare function packumentUrl(registry: string, name: string): URL;
|
|
84
|
+
/**
|
|
85
|
+
* The package names in a snapshot request: the report `advisor-package-request`
|
|
86
|
+
* prints, `{ state: "satisfied", names, findings: [] }`, or just `{ names }`.
|
|
87
|
+
* Every name must be a scoped package name in the packed publishing scope,
|
|
88
|
+
* named once. A refusal names positions only, never the request's text.
|
|
89
|
+
*/
|
|
90
|
+
export declare function requestedPackageNames(request: unknown, scope?: string): string[];
|
|
91
|
+
/**
|
|
92
|
+
* Projects a registry document into one snapshot entry's `latest` and
|
|
93
|
+
* `versions`, reading only what the contract records: the version the
|
|
94
|
+
* `latest` dist-tag names, and that one version's integrity, tarball,
|
|
95
|
+
* deprecation, publish time and attestation presence. Every other version,
|
|
96
|
+
* dist-tag and field is ignored. When `latest` names a version the document
|
|
97
|
+
* does not list, `versions` is empty, so a resolver refuses it by name.
|
|
98
|
+
*/
|
|
99
|
+
export declare function projectPackument(name: string, document: unknown): {
|
|
100
|
+
latest: string | null;
|
|
101
|
+
versions: RegistrySnapshotVersion[];
|
|
102
|
+
};
|
|
103
|
+
export interface FetchSnapshotOptions {
|
|
104
|
+
readonly transport?: Transport;
|
|
105
|
+
readonly timeoutMs?: number;
|
|
106
|
+
/** An ISO 8601 date-time, taken once every fetch has finished. */
|
|
107
|
+
readonly now?: () => string;
|
|
108
|
+
/** The package taking the snapshot; read from this package's own package.json unless given. */
|
|
109
|
+
readonly fetchedBy?: {
|
|
110
|
+
readonly name: string;
|
|
111
|
+
readonly version: string;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Fetches every named package's full registry document, one at a time, and
|
|
116
|
+
* returns the snapshot in canonical order, validated against the contract.
|
|
117
|
+
* Throws RegistrySnapshotError, having fetched no further, on the first
|
|
118
|
+
* package whose read fails: a transport error, a timeout, a redirect, an
|
|
119
|
+
* answer other than 200 or 404, a body over MAX_RESPONSE_BYTES, a 200 body
|
|
120
|
+
* that is not strict JSON or not a registry document for that package, or a
|
|
121
|
+
* projection the contract refuses. A 404 records `status: "not-found"`.
|
|
122
|
+
* `names` must already have passed `requestedPackageNames()`.
|
|
123
|
+
*/
|
|
124
|
+
export declare function takeRegistrySnapshot(names: readonly string[], options?: FetchSnapshotOptions): Promise<RegistrySnapshot>;
|
|
125
|
+
/** The snapshot's file text: two-space JSON with a final newline, in the order given. */
|
|
126
|
+
export declare function serializeRegistrySnapshot(snapshot: RegistrySnapshot): string;
|
|
127
|
+
/**
|
|
128
|
+
* Writes `text` to `path` so that a reader sees the old file or the whole
|
|
129
|
+
* new one, never part of it: the bytes go to a new temporary file in the same
|
|
130
|
+
* directory, are flushed to disk, and the temporary file is then renamed over
|
|
131
|
+
* `path`. On any failure the temporary file is removed and `path` is left as
|
|
132
|
+
* it was.
|
|
133
|
+
*/
|
|
134
|
+
export declare function writeFileAtomically(path: string, text: string): void;
|
|
135
|
+
/**
|
|
136
|
+
* Serializes a snapshot, re-reads that exact text strictly and validates it
|
|
137
|
+
* against the contract, and only then writes it atomically to `path`. The
|
|
138
|
+
* bytes on disk are therefore exactly the bytes that validated.
|
|
139
|
+
*/
|
|
140
|
+
export declare function writeRegistrySnapshot(path: string, snapshot: RegistrySnapshot): void;
|
|
141
|
+
//# sourceMappingURL=registry-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-snapshot.d.ts","sourceRoot":"","sources":["../src/registry-snapshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
// The registry snapshot step (issue #1178): the one step of applying a plan
|
|
2
|
+
// that reads the package registry. It fetches each requested package's registry
|
|
3
|
+
// document anonymously, projects it into the shared registry snapshot
|
|
4
|
+
// contract (docs/contracts/registry-snapshot.json, in the public repository,
|
|
5
|
+
// not shipped in this package; its content is packed into src/generated/ at
|
|
6
|
+
// build time), validates the whole snapshot against that contract, and only
|
|
7
|
+
// then writes it, atomically.
|
|
8
|
+
//
|
|
9
|
+
// Credentials: every read goes through an injected `Transport` whose default
|
|
10
|
+
// is Node's own `fetch`. Nothing here runs the npm CLI, reads an `.npmrc`,
|
|
11
|
+
// reads an environment variable, or sets an `Authorization` header; the only
|
|
12
|
+
// headers this step sets are `accept` and `accept-encoding: identity`, and
|
|
13
|
+
// Node's fetch adds its own default, non-credential headers. Identity asks
|
|
14
|
+
// for the body uncompressed, so when the server honours it the hash and the
|
|
15
|
+
// size cap apply to the bytes received. Redirects are refused (`redirect: "error"`, and a
|
|
16
|
+
// 3xx answer from any transport is refused too), each response is read as a
|
|
17
|
+
// stream and abandoned the moment it passes MAX_RESPONSE_BYTES (counted after
|
|
18
|
+
// any decoding, so a server that compresses anyway is still bounded), and each
|
|
19
|
+
// request, body included, is abandoned after a timeout.
|
|
20
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
21
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeSync } from "node:fs";
|
|
22
|
+
import { basename, dirname, join } from "node:path";
|
|
23
|
+
import { ContractDocumentError, readContractDocument, validateAgainstContract } from "./generated/contract-schema.generated.js";
|
|
24
|
+
import { PACKAGE_SCOPE } from "./generated/package-scope.generated.js";
|
|
25
|
+
import { PLAN_CONTRACTS } from "./generated/plan-contracts.generated.js";
|
|
26
|
+
/** Where the snapshot is written by default, relative to the hub root. */
|
|
27
|
+
export const REGISTRY_SNAPSHOT_REL = "clossys/.state/apply/registry-snapshot.json";
|
|
28
|
+
/** The most bytes read from any one registry response (10 MiB). */
|
|
29
|
+
export const MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
|
|
30
|
+
/** How long one request, its body included, may take. */
|
|
31
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
32
|
+
/** Node's own `fetch`. It reads no npm configuration and adds no registry credential (a proxy URL's own username and password, if any, go only to that proxy). */
|
|
33
|
+
export const nodeFetchTransport = (input, init) => fetch(input, init);
|
|
34
|
+
/**
|
|
35
|
+
* Why the snapshot step stopped. The message names a package only by its
|
|
36
|
+
* position in the request (`names[<n>]`), never by its name: a name is
|
|
37
|
+
* request text, and a request is a file anyone could have written. It
|
|
38
|
+
* never carries registry content either.
|
|
39
|
+
*/
|
|
40
|
+
export class RegistrySnapshotError extends Error {
|
|
41
|
+
}
|
|
42
|
+
function loadContract(name) {
|
|
43
|
+
const contract = Object.hasOwn(PLAN_CONTRACTS, name) ? PLAN_CONTRACTS[name] : undefined;
|
|
44
|
+
if (contract === undefined)
|
|
45
|
+
throw new Error(`no packed contract named ${JSON.stringify(name)}`);
|
|
46
|
+
return contract;
|
|
47
|
+
}
|
|
48
|
+
function eachRepeat(items, key, onRepeat) {
|
|
49
|
+
const first = new Map();
|
|
50
|
+
items.forEach((item, index) => {
|
|
51
|
+
const value = key(item);
|
|
52
|
+
const earlier = first.get(value);
|
|
53
|
+
if (earlier === undefined)
|
|
54
|
+
first.set(value, index);
|
|
55
|
+
else
|
|
56
|
+
onRepeat(index, earlier);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Every violation of the registry snapshot contract: its schema, then, only
|
|
61
|
+
* for a snapshot whose shape is known good, its code rules N1-N3 (no package
|
|
62
|
+
* named twice, no version recorded twice for one package, no `latest` or
|
|
63
|
+
* versions for a package that was not found). Implemented separately from
|
|
64
|
+
* @clossys/advisor's reader; both are tested against the one corpus,
|
|
65
|
+
* docs/contracts/registry-snapshot.fixture.json (in the public repository,
|
|
66
|
+
* not shipped in this package). Never throws.
|
|
67
|
+
*/
|
|
68
|
+
export function registrySnapshotViolations(value) {
|
|
69
|
+
const schema = validateAgainstContract(loadContract("registry-snapshot.json"), value, loadContract);
|
|
70
|
+
// The shared checker's path and message never carry document text: an undeclared field is placed at its object, by position.
|
|
71
|
+
if (schema.length > 0)
|
|
72
|
+
return schema.map((violation) => ({ rule: "schema", path: violation.path, message: violation.message }));
|
|
73
|
+
const snapshot = value;
|
|
74
|
+
const violations = [];
|
|
75
|
+
eachRepeat(snapshot.packages, (entry) => entry.name, (index, first) => violations.push({ rule: "N1", path: `packages[${index}].name`, message: `repeats packages[${first}].name` }));
|
|
76
|
+
snapshot.packages.forEach((entry, index) => {
|
|
77
|
+
eachRepeat(entry.versions, (version) => version.version, (position, first) => violations.push({ rule: "N2", path: `packages[${index}].versions[${position}].version`, message: `repeats packages[${index}].versions[${first}].version` }));
|
|
78
|
+
if (entry.status === "not-found") {
|
|
79
|
+
if (entry.latest !== null)
|
|
80
|
+
violations.push({ rule: "N3", path: `packages[${index}].latest`, message: "must be null when the package was not found" });
|
|
81
|
+
if (entry.versions.length > 0)
|
|
82
|
+
violations.push({ rule: "N3", path: `packages[${index}].versions`, message: "must be empty when the package was not found" });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
return violations;
|
|
86
|
+
}
|
|
87
|
+
/** Compares strings as sequences of UTF-16 code units, the order the snapshot contract sorts by. */
|
|
88
|
+
function byCodeUnits(left, right) {
|
|
89
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
90
|
+
}
|
|
91
|
+
/** The snapshot in the contract's canonical order: packages sorted by name, each package's versions sorted by version. */
|
|
92
|
+
export function canonicalRegistrySnapshot(snapshot) {
|
|
93
|
+
return {
|
|
94
|
+
...snapshot,
|
|
95
|
+
packages: [...snapshot.packages]
|
|
96
|
+
.sort((left, right) => byCodeUnits(left.name, right.name))
|
|
97
|
+
.map((entry) => ({ ...entry, versions: [...entry.versions].sort((left, right) => byCodeUnits(left.version, right.version)) })),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A package name's registry path segment. Copied, unchanged, from
|
|
102
|
+
* `registryEncodedName` in @clossys/integrator's provenance-check.ts, which
|
|
103
|
+
* this package must not depend on at runtime; a test compares the two
|
|
104
|
+
* function bodies so they cannot drift apart. The leading `@` stays literal
|
|
105
|
+
* and only the slash is percent-encoded (`@scope%2Fname`), the convention
|
|
106
|
+
* npm-compatible registries share; `encodeURIComponent` on the whole name
|
|
107
|
+
* would also encode the `@`, which none of them expect.
|
|
108
|
+
*/
|
|
109
|
+
export function registryEncodedName(name) {
|
|
110
|
+
const slash = name.indexOf("/");
|
|
111
|
+
if (slash === -1)
|
|
112
|
+
return encodeURIComponent(name);
|
|
113
|
+
return `@${encodeURIComponent(name.slice(1, slash))}%2F${encodeURIComponent(name.slice(slash + 1))}`;
|
|
114
|
+
}
|
|
115
|
+
/** The full registry document's URL for `name`: `{registry}/{encoded name}`. */
|
|
116
|
+
export function packumentUrl(registry, name) {
|
|
117
|
+
return new URL(registryEncodedName(name), registry.endsWith("/") ? registry : `${registry}/`);
|
|
118
|
+
}
|
|
119
|
+
const PACKAGE_NAME = new RegExp(loadContract("registry-snapshot.json").definitions.packageName.pattern);
|
|
120
|
+
/**
|
|
121
|
+
* The package names in a snapshot request: the report `advisor-package-request`
|
|
122
|
+
* prints, `{ state: "satisfied", names, findings: [] }`, or just `{ names }`.
|
|
123
|
+
* Every name must be a scoped package name in the packed publishing scope,
|
|
124
|
+
* named once. A refusal names positions only, never the request's text.
|
|
125
|
+
*/
|
|
126
|
+
export function requestedPackageNames(request, scope = PACKAGE_SCOPE.scope) {
|
|
127
|
+
if (typeof request !== "object" || request === null || Array.isArray(request))
|
|
128
|
+
throw new RegistrySnapshotError("the request must be a JSON object with a names array");
|
|
129
|
+
const record = request;
|
|
130
|
+
if (Object.keys(record).some((key) => key !== "state" && key !== "names" && key !== "findings")) {
|
|
131
|
+
throw new RegistrySnapshotError("the request has a field other than state, names and findings");
|
|
132
|
+
}
|
|
133
|
+
if (Object.hasOwn(record, "state") && record.state !== "satisfied") {
|
|
134
|
+
throw new RegistrySnapshotError("the request's state is not satisfied: fix the plan and run advisor-package-request again");
|
|
135
|
+
}
|
|
136
|
+
if (Object.hasOwn(record, "findings") && !(Array.isArray(record.findings) && record.findings.length === 0)) {
|
|
137
|
+
throw new RegistrySnapshotError("the request's findings must be an empty array");
|
|
138
|
+
}
|
|
139
|
+
const names = Object.hasOwn(record, "names") ? record.names : undefined;
|
|
140
|
+
if (!Array.isArray(names) || names.length === 0)
|
|
141
|
+
throw new RegistrySnapshotError("the request's names must be a non-empty array");
|
|
142
|
+
const seen = new Map();
|
|
143
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
144
|
+
if (!Object.hasOwn(names, index))
|
|
145
|
+
throw new RegistrySnapshotError(`names[${index}] is missing`);
|
|
146
|
+
const name = names[index];
|
|
147
|
+
if (typeof name !== "string" || !PACKAGE_NAME.test(name) || !name.startsWith(`${scope}/`)) {
|
|
148
|
+
throw new RegistrySnapshotError(`names[${index}] is not a package name in the ${scope} scope`);
|
|
149
|
+
}
|
|
150
|
+
const earlier = seen.get(name);
|
|
151
|
+
if (earlier !== undefined)
|
|
152
|
+
throw new RegistrySnapshotError(`names[${index}] repeats names[${earlier}]`);
|
|
153
|
+
seen.set(name, index);
|
|
154
|
+
}
|
|
155
|
+
return names;
|
|
156
|
+
}
|
|
157
|
+
/** A registry document that cannot be projected. `what` is fixed text, never the document's. */
|
|
158
|
+
class PackumentShapeError extends Error {
|
|
159
|
+
}
|
|
160
|
+
function isPlainObject(value) {
|
|
161
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
162
|
+
}
|
|
163
|
+
/** An own member only: a registry document's inherited members are never read. */
|
|
164
|
+
function own(record, key) {
|
|
165
|
+
return Object.hasOwn(record, key) ? record[key] : undefined;
|
|
166
|
+
}
|
|
167
|
+
/** An own member that must be a plain object when present; absent or null reads as undefined. */
|
|
168
|
+
function optionalObject(record, key, what) {
|
|
169
|
+
const value = own(record, key);
|
|
170
|
+
if (value === undefined || value === null)
|
|
171
|
+
return undefined;
|
|
172
|
+
if (!isPlainObject(value))
|
|
173
|
+
throw new PackumentShapeError(what);
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
function optionalString(record, key, what) {
|
|
177
|
+
const value = own(record, key);
|
|
178
|
+
if (value === undefined || value === null)
|
|
179
|
+
return null;
|
|
180
|
+
if (typeof value !== "string")
|
|
181
|
+
throw new PackumentShapeError(what);
|
|
182
|
+
return value;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Projects a registry document into one snapshot entry's `latest` and
|
|
186
|
+
* `versions`, reading only what the contract records: the version the
|
|
187
|
+
* `latest` dist-tag names, and that one version's integrity, tarball,
|
|
188
|
+
* deprecation, publish time and attestation presence. Every other version,
|
|
189
|
+
* dist-tag and field is ignored. When `latest` names a version the document
|
|
190
|
+
* does not list, `versions` is empty, so a resolver refuses it by name.
|
|
191
|
+
*/
|
|
192
|
+
export function projectPackument(name, document) {
|
|
193
|
+
if (!isPlainObject(document))
|
|
194
|
+
throw new PackumentShapeError("is not a JSON object");
|
|
195
|
+
if (own(document, "name") !== name)
|
|
196
|
+
throw new PackumentShapeError("does not name the package that was requested");
|
|
197
|
+
const distTags = optionalObject(document, "dist-tags", "has dist-tags that are not an object");
|
|
198
|
+
const latest = distTags === undefined ? null : optionalString(distTags, "latest", "has a latest dist-tag that is not a string");
|
|
199
|
+
if (latest === null)
|
|
200
|
+
return { latest: null, versions: [] };
|
|
201
|
+
const versions = optionalObject(document, "versions", "has versions that are not an object");
|
|
202
|
+
const entry = versions === undefined ? undefined : own(versions, latest);
|
|
203
|
+
if (entry === undefined)
|
|
204
|
+
return { latest, versions: [] };
|
|
205
|
+
if (!isPlainObject(entry))
|
|
206
|
+
throw new PackumentShapeError("lists the latest version as something that is not an object");
|
|
207
|
+
if (own(entry, "version") !== latest)
|
|
208
|
+
throw new PackumentShapeError("lists the latest version under a different version number");
|
|
209
|
+
const dist = optionalObject(entry, "dist", "has a latest version whose dist is not an object") ?? {};
|
|
210
|
+
const tarball = own(dist, "tarball");
|
|
211
|
+
if (typeof tarball !== "string")
|
|
212
|
+
throw new PackumentShapeError("has a latest version with no tarball URL");
|
|
213
|
+
const deprecation = own(entry, "deprecated");
|
|
214
|
+
let deprecated;
|
|
215
|
+
if (deprecation === undefined || deprecation === null)
|
|
216
|
+
deprecated = false;
|
|
217
|
+
else if (typeof deprecation === "boolean")
|
|
218
|
+
deprecated = deprecation;
|
|
219
|
+
// npm treats an empty deprecation message as no deprecation, and any other message as one.
|
|
220
|
+
else if (typeof deprecation === "string")
|
|
221
|
+
deprecated = deprecation !== "";
|
|
222
|
+
else
|
|
223
|
+
throw new PackumentShapeError("marks the latest version deprecated with something that is neither text nor a boolean");
|
|
224
|
+
const attestations = own(dist, "attestations");
|
|
225
|
+
if (attestations !== undefined && attestations !== null && !isPlainObject(attestations)) {
|
|
226
|
+
throw new PackumentShapeError("has latest-version attestations that are not an object");
|
|
227
|
+
}
|
|
228
|
+
const time = optionalObject(document, "time", "has a time field that is not an object");
|
|
229
|
+
return {
|
|
230
|
+
latest,
|
|
231
|
+
versions: [
|
|
232
|
+
{
|
|
233
|
+
version: latest,
|
|
234
|
+
integrity: optionalString(dist, "integrity", "has a latest-version integrity that is not a string"),
|
|
235
|
+
tarball,
|
|
236
|
+
deprecated,
|
|
237
|
+
publishedAt: time === undefined ? null : optionalString(time, latest, "has a latest-version publish time that is not a string"),
|
|
238
|
+
// Listed only when the registry names where the attestations are: an empty object lists none.
|
|
239
|
+
hasAttestations: isPlainObject(attestations) && typeof own(attestations, "url") === "string" && /\S/.test(own(attestations, "url")),
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const TIMED_OUT = Symbol("timed out");
|
|
245
|
+
/** Settles with `promise`, or rejects with TIMED_OUT as soon as `signal` aborts, whether or not the transport honours the signal. */
|
|
246
|
+
function untilAborted(promise, signal) {
|
|
247
|
+
if (signal.aborted)
|
|
248
|
+
return Promise.reject(TIMED_OUT);
|
|
249
|
+
return new Promise((resolve, reject) => {
|
|
250
|
+
const onAbort = () => reject(TIMED_OUT);
|
|
251
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
252
|
+
promise.then((value) => {
|
|
253
|
+
signal.removeEventListener("abort", onAbort);
|
|
254
|
+
resolve(value);
|
|
255
|
+
}, (error) => {
|
|
256
|
+
signal.removeEventListener("abort", onAbort);
|
|
257
|
+
reject(error);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* The body's bytes, read chunk by chunk. The read stops, and the stream is
|
|
263
|
+
* cancelled, the moment the running total passes `cap`, so an oversize body
|
|
264
|
+
* is never buffered past it.
|
|
265
|
+
*/
|
|
266
|
+
async function readCapped(body, cap, signal) {
|
|
267
|
+
if (body === null)
|
|
268
|
+
return new Uint8Array(0);
|
|
269
|
+
const reader = body.getReader();
|
|
270
|
+
const chunks = [];
|
|
271
|
+
let total = 0;
|
|
272
|
+
try {
|
|
273
|
+
for (;;) {
|
|
274
|
+
const { done, value } = await untilAborted(reader.read(), signal);
|
|
275
|
+
if (done)
|
|
276
|
+
break;
|
|
277
|
+
if (!(value instanceof Uint8Array))
|
|
278
|
+
throw new TypeError("a response chunk is not bytes");
|
|
279
|
+
total += value.byteLength;
|
|
280
|
+
if (total > cap)
|
|
281
|
+
return "too-large";
|
|
282
|
+
chunks.push(value);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
reader.cancel().catch(() => undefined);
|
|
287
|
+
}
|
|
288
|
+
const bytes = new Uint8Array(total);
|
|
289
|
+
let offset = 0;
|
|
290
|
+
for (const chunk of chunks) {
|
|
291
|
+
bytes.set(chunk, offset);
|
|
292
|
+
offset += chunk.byteLength;
|
|
293
|
+
}
|
|
294
|
+
return bytes;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The request options this step sets on every registry read: `accept`, and
|
|
298
|
+
* `accept-encoding: identity` so that a server that honours it sends the body
|
|
299
|
+
* uncompressed and `responseSha256` and the size cap apply to the exact bytes
|
|
300
|
+
* received; no credential, no redirect. Node's fetch adds its own default,
|
|
301
|
+
* non-credential headers.
|
|
302
|
+
*/
|
|
303
|
+
function requestInit(signal) {
|
|
304
|
+
return { method: "GET", headers: { accept: "application/json", "accept-encoding": "identity" }, redirect: "error", credentials: "omit", signal };
|
|
305
|
+
}
|
|
306
|
+
async function fetchOne(transport, url, where, timeoutMs) {
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
309
|
+
const seconds = `${timeoutMs / 1000} s`;
|
|
310
|
+
try {
|
|
311
|
+
let response;
|
|
312
|
+
try {
|
|
313
|
+
response = await untilAborted(transport(url, requestInit(controller.signal)), controller.signal);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
if (controller.signal.aborted)
|
|
317
|
+
throw new RegistrySnapshotError(`${where}: the registry did not answer within ${seconds}`);
|
|
318
|
+
throw new RegistrySnapshotError(`${where}: the registry could not be reached, or answered with a redirect, which is refused`);
|
|
319
|
+
}
|
|
320
|
+
const { status } = response;
|
|
321
|
+
const discard = () => {
|
|
322
|
+
response.body?.cancel().catch(() => undefined);
|
|
323
|
+
};
|
|
324
|
+
if (response.redirected || (status >= 300 && status < 400)) {
|
|
325
|
+
discard();
|
|
326
|
+
throw new RegistrySnapshotError(`${where}: the registry answered with a redirect${response.redirected ? "" : ` (HTTP ${status})`}, which is refused and never followed`);
|
|
327
|
+
}
|
|
328
|
+
if (status !== 200 && status !== 404) {
|
|
329
|
+
discard();
|
|
330
|
+
throw new RegistrySnapshotError(`${where}: the registry answered HTTP ${status}; only 200 and 404 are recorded`);
|
|
331
|
+
}
|
|
332
|
+
const tooLarge = `${where}: the registry's response is larger than ${MAX_RESPONSE_BYTES / (1024 * 1024)} MiB and was not read past that`;
|
|
333
|
+
const declared = response.headers.get("content-length");
|
|
334
|
+
if (declared !== null && /^\d+$/.test(declared.trim()) && Number(declared.trim()) > MAX_RESPONSE_BYTES) {
|
|
335
|
+
discard();
|
|
336
|
+
throw new RegistrySnapshotError(tooLarge);
|
|
337
|
+
}
|
|
338
|
+
let bytes;
|
|
339
|
+
try {
|
|
340
|
+
bytes = await readCapped(response.body, MAX_RESPONSE_BYTES, controller.signal);
|
|
341
|
+
}
|
|
342
|
+
catch {
|
|
343
|
+
if (controller.signal.aborted)
|
|
344
|
+
throw new RegistrySnapshotError(`${where}: the registry did not finish its response within ${seconds}`);
|
|
345
|
+
throw new RegistrySnapshotError(`${where}: the registry's response could not be read`);
|
|
346
|
+
}
|
|
347
|
+
if (bytes === "too-large")
|
|
348
|
+
throw new RegistrySnapshotError(tooLarge);
|
|
349
|
+
return { status, bytes };
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function strictJsonReason(cause) {
|
|
356
|
+
if (!(cause instanceof ContractDocumentError))
|
|
357
|
+
return "is not strict JSON";
|
|
358
|
+
const where = cause.position === undefined ? "" : ` at position ${cause.position}`;
|
|
359
|
+
const why = cause.reason === "encoding" ? "is not valid UTF-8" : cause.reason === "repeated-key" ? "repeats a key in one object" : "is not valid JSON";
|
|
360
|
+
return `${why}${where}`;
|
|
361
|
+
}
|
|
362
|
+
let ownIdentity;
|
|
363
|
+
/** This package's own name and version, from the package.json it ships with. */
|
|
364
|
+
function launcherIdentity() {
|
|
365
|
+
if (ownIdentity === undefined) {
|
|
366
|
+
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
367
|
+
if (typeof manifest.name !== "string" || typeof manifest.version !== "string")
|
|
368
|
+
throw new RegistrySnapshotError("this package's own package.json has no name and version");
|
|
369
|
+
ownIdentity = { name: manifest.name, version: manifest.version };
|
|
370
|
+
}
|
|
371
|
+
return ownIdentity;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Fetches every named package's full registry document, one at a time, and
|
|
375
|
+
* returns the snapshot in canonical order, validated against the contract.
|
|
376
|
+
* Throws RegistrySnapshotError, having fetched no further, on the first
|
|
377
|
+
* package whose read fails: a transport error, a timeout, a redirect, an
|
|
378
|
+
* answer other than 200 or 404, a body over MAX_RESPONSE_BYTES, a 200 body
|
|
379
|
+
* that is not strict JSON or not a registry document for that package, or a
|
|
380
|
+
* projection the contract refuses. A 404 records `status: "not-found"`.
|
|
381
|
+
* `names` must already have passed `requestedPackageNames()`.
|
|
382
|
+
*/
|
|
383
|
+
export async function takeRegistrySnapshot(names, options = {}) {
|
|
384
|
+
const transport = options.transport ?? nodeFetchTransport;
|
|
385
|
+
const { registry } = PACKAGE_SCOPE;
|
|
386
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
387
|
+
const fetchedBy = options.fetchedBy ?? launcherIdentity();
|
|
388
|
+
const ordered = names.map((name, index) => ({ name, index })).sort((left, right) => byCodeUnits(left.name, right.name));
|
|
389
|
+
const packages = [];
|
|
390
|
+
for (const { name, index } of ordered) {
|
|
391
|
+
// The position only: the name itself is request text.
|
|
392
|
+
const where = `names[${index}]`;
|
|
393
|
+
const { status, bytes } = await fetchOne(transport, packumentUrl(registry, name), where, timeoutMs);
|
|
394
|
+
const responseSha256 = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
395
|
+
if (status === 404) {
|
|
396
|
+
packages.push({ name, status: "not-found", latest: null, versions: [], responseSha256 });
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
let document;
|
|
400
|
+
try {
|
|
401
|
+
document = readContractDocument(bytes);
|
|
402
|
+
}
|
|
403
|
+
catch (cause) {
|
|
404
|
+
throw new RegistrySnapshotError(`${where}: the registry's response ${strictJsonReason(cause)}`);
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
packages.push({ name, status: "found", ...projectPackument(name, document), responseSha256 });
|
|
408
|
+
}
|
|
409
|
+
catch (cause) {
|
|
410
|
+
if (cause instanceof PackumentShapeError)
|
|
411
|
+
throw new RegistrySnapshotError(`${where}: the registry's document ${cause.message}`);
|
|
412
|
+
throw cause;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
const snapshot = canonicalRegistrySnapshot({
|
|
416
|
+
schemaVersion: 1,
|
|
417
|
+
kind: "clossys.registry-snapshot",
|
|
418
|
+
registry,
|
|
419
|
+
fetchedAt: (options.now ?? (() => new Date().toISOString()))(),
|
|
420
|
+
fetchedBy: { name: fetchedBy.name, version: fetchedBy.version },
|
|
421
|
+
packages,
|
|
422
|
+
});
|
|
423
|
+
assertValidSnapshot(snapshot);
|
|
424
|
+
return snapshot;
|
|
425
|
+
}
|
|
426
|
+
function assertValidSnapshot(value) {
|
|
427
|
+
const violations = registrySnapshotViolations(value);
|
|
428
|
+
if (violations.length > 0) {
|
|
429
|
+
const listed = violations.map((violation) => `snapshot${violation.path === "" ? "" : violation.path.startsWith("[") ? violation.path : `.${violation.path}`} ${violation.message} (rule ${violation.rule})`);
|
|
430
|
+
throw new RegistrySnapshotError(`the snapshot does not validate against the registry snapshot contract, so none is written: ${listed.join("; ")}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/** The snapshot's file text: two-space JSON with a final newline, in the order given. */
|
|
434
|
+
export function serializeRegistrySnapshot(snapshot) {
|
|
435
|
+
return `${JSON.stringify(snapshot, null, 2)}\n`;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Writes `text` to `path` so that a reader sees the old file or the whole
|
|
439
|
+
* new one, never part of it: the bytes go to a new temporary file in the same
|
|
440
|
+
* directory, are flushed to disk, and the temporary file is then renamed over
|
|
441
|
+
* `path`. On any failure the temporary file is removed and `path` is left as
|
|
442
|
+
* it was.
|
|
443
|
+
*/
|
|
444
|
+
export function writeFileAtomically(path, text) {
|
|
445
|
+
const directory = dirname(path);
|
|
446
|
+
mkdirSync(directory, { recursive: true });
|
|
447
|
+
const temporary = join(directory, `.${basename(path)}.${randomBytes(8).toString("hex")}.tmp`);
|
|
448
|
+
const bytes = Buffer.from(text, "utf8");
|
|
449
|
+
let descriptor;
|
|
450
|
+
try {
|
|
451
|
+
descriptor = openSync(temporary, "wx", 0o644);
|
|
452
|
+
let written = 0;
|
|
453
|
+
while (written < bytes.length)
|
|
454
|
+
written += writeSync(descriptor, bytes, written, bytes.length - written);
|
|
455
|
+
fsyncSync(descriptor);
|
|
456
|
+
closeSync(descriptor);
|
|
457
|
+
descriptor = undefined;
|
|
458
|
+
renameSync(temporary, path);
|
|
459
|
+
}
|
|
460
|
+
catch (cause) {
|
|
461
|
+
if (descriptor !== undefined) {
|
|
462
|
+
try {
|
|
463
|
+
closeSync(descriptor);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
// Already failing; the original error is the one reported.
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
rmSync(temporary, { force: true });
|
|
470
|
+
throw cause;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Serializes a snapshot, re-reads that exact text strictly and validates it
|
|
475
|
+
* against the contract, and only then writes it atomically to `path`. The
|
|
476
|
+
* bytes on disk are therefore exactly the bytes that validated.
|
|
477
|
+
*/
|
|
478
|
+
export function writeRegistrySnapshot(path, snapshot) {
|
|
479
|
+
const text = serializeRegistrySnapshot(snapshot);
|
|
480
|
+
assertValidSnapshot(readContractDocument(Buffer.from(text, "utf8")));
|
|
481
|
+
writeFileAtomically(path, text);
|
|
482
|
+
}
|
|
483
|
+
//# sourceMappingURL=registry-snapshot.js.map
|