@vincentt-xr/cli 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/args.js +1 -0
- package/dist/cli/index.js +177 -4
- package/dist/cli/usage-text.js +27 -0
- package/dist/cli/verb-surface.d.ts +3 -1
- package/dist/cli/verb-surface.js +12 -0
- package/dist/domain/api.d.ts +51 -0
- package/dist/domain/api.js +145 -0
- package/dist/domain/command.d.ts +75 -0
- package/dist/domain/command.js +467 -0
- package/dist/domain/copy.d.ts +334 -0
- package/dist/domain/copy.js +952 -0
- package/dist/domain/hostname.d.ts +57 -0
- package/dist/domain/hostname.js +116 -0
- package/dist/domain/names.d.ts +78 -0
- package/dist/domain/names.js +113 -0
- package/dist/domain/routedVerbs.d.ts +10 -0
- package/dist/domain/routedVerbs.js +30 -0
- package/dist/domain/wire.d.ts +140 -0
- package/dist/domain/wire.js +174 -0
- package/dist/dx/clusters.js +10 -1
- package/dist/dx/command.d.ts +13 -4
- package/dist/dx/command.js +26 -5
- package/dist/dx/report.d.ts +39 -7
- package/dist/dx/report.js +71 -20
- package/dist/dx/send.d.ts +22 -4
- package/dist/dx/send.js +29 -8
- package/dist/dx/skeleton.js +19 -0
- package/dist/dx/unbiddenVerbs.d.ts +25 -0
- package/dist/dx/unbiddenVerbs.js +39 -0
- package/dist/lib/exitCodes.d.ts +24 -0
- package/dist/lib/exitCodes.js +28 -0
- package/dist/lib/initCommand.d.ts +3 -1
- package/dist/lib/initCommand.js +10 -0
- package/dist/lib/initFrom.js +12 -2
- package/dist/lib/lifecycle.d.ts +5 -1
- package/dist/lib/projects.d.ts +8 -1
- package/dist/lib/projects.js +21 -4
- package/dist/platform/allowlist.d.ts +36 -0
- package/dist/platform/allowlist.js +34 -0
- package/dist/platform/behind.d.ts +41 -0
- package/dist/platform/behind.js +82 -0
- package/dist/platform/command.d.ts +16 -0
- package/dist/platform/command.js +90 -0
- package/dist/platform/footer.d.ts +60 -0
- package/dist/platform/footer.js +101 -0
- package/dist/platform/notice.d.ts +29 -0
- package/dist/platform/notice.js +66 -0
- package/dist/platform/offSwitch.d.ts +19 -0
- package/dist/platform/offSwitch.js +21 -0
- package/dist/platform/outdated.d.ts +25 -0
- package/dist/platform/outdated.js +246 -0
- package/dist/platform/releaseCache.d.ts +69 -0
- package/dist/platform/releaseCache.js +140 -0
- package/dist/platform/releaseFetch.d.ts +51 -0
- package/dist/platform/releaseFetch.js +150 -0
- package/dist/platform/resolve.d.ts +30 -0
- package/dist/platform/resolve.js +128 -0
- package/dist/platform/templateUpgrade.d.ts +56 -0
- package/dist/platform/templateUpgrade.js +476 -0
- package/dist/platform/upgradeReport.d.ts +132 -0
- package/dist/platform/upgradeReport.js +258 -0
- package/dist/platform/versionSummary.d.ts +82 -0
- package/dist/platform/versionSummary.js +181 -0
- package/dist/platform/zonePath.d.ts +154 -0
- package/dist/platform/zonePath.js +286 -0
- package/dist/platform/zones.d.ts +64 -0
- package/dist/platform/zones.js +79 -0
- package/dist/publish/api.d.ts +15 -4
- package/dist/publish/api.js +19 -5
- package/dist/publish/command.d.ts +10 -0
- package/dist/publish/command.js +7 -1
- package/dist/publish/copy.d.ts +14 -1
- package/dist/publish/copy.js +32 -3
- package/dist/scaffold/index.d.ts +50 -3
- package/dist/scaffold/index.js +61 -1
- package/dist/shared/config.d.ts +16 -0
- package/dist/shared/config.js +9 -1
- package/package.json +1 -1
- package/dist/dx/qa.f7.fixtures.d.ts +0 -73
- package/dist/dx/qa.f7.fixtures.js +0 -58
- package/dist/publish/qa.f4.fixtures.d.ts +0 -25
- package/dist/publish/qa.f4.fixtures.js +0 -140
- package/dist/relay/qa.f12.fixtures.d.ts +0 -154
- package/dist/relay/qa.f12.fixtures.js +0 -358
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The path floor: whether a path may be written into a creator's repository at
|
|
3
|
+
* all. Purpose-built, and deliberately NOT `publish/paths.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The publish validator was written to bound an S3 key for a dist upload, where
|
|
6
|
+
* the worst case is a weird object name. This bounds a DESTINATION FOR AN
|
|
7
|
+
* OVERWRITE IN SOMEONE ELSE'S REPOSITORY, where the worst case is code
|
|
8
|
+
* execution. Run against the zone corpus it accepts `.git/hooks/pre-commit`,
|
|
9
|
+
* `.git/config`, `.env`, `.npmrc`, `.gitignore`,
|
|
10
|
+
* `.github/workflows/release.yml`, `node_modules/.bin/esbuild`,
|
|
11
|
+
* `pnpm-lock.yaml` — and `src/Scene.tsx`, the one file the entire safety
|
|
12
|
+
* argument names. It rejects only `../` and `.`-segments and normalizes no
|
|
13
|
+
* unicode, so an RTL override, a Cyrillic homoglyph and a division slash all
|
|
14
|
+
* pass. Its `validatePathSet` also hard-requires `index.html`, so it FAILS the
|
|
15
|
+
* legitimate zone set — a reuse would have gone red on the happy path and the
|
|
16
|
+
* cheapest fix would have been to drop the control while the record still
|
|
17
|
+
* claimed it.
|
|
18
|
+
*
|
|
19
|
+
* THE MANIFEST IS NEVER CONSULTED HERE. It decides which of the two zones a
|
|
20
|
+
* PERMISSIBLE path belongs to; it is never the authority for whether a path is
|
|
21
|
+
* permissible. A boundary with no floor under it is not a boundary — it is a
|
|
22
|
+
* suggestion the manifest could withdraw. So this code is identical for every
|
|
23
|
+
* template version and runs BEFORE the manifest is read.
|
|
24
|
+
*/
|
|
25
|
+
/** The closed reason enum. Every refusal names the rule that fired. */
|
|
26
|
+
export type ZonePathRejection = "non_ascii" | "denied_by_name" | "malformed" | "dot_segment" | "node_modules" | "top_level" | "extension" | "too_deep" | "too_long" | "duplicate" | "case_duplicate" | "in_both_zones";
|
|
27
|
+
export interface ZonePathError {
|
|
28
|
+
rule: ZonePathRejection;
|
|
29
|
+
path: string;
|
|
30
|
+
}
|
|
31
|
+
export type ZonePathResult = {
|
|
32
|
+
ok: true;
|
|
33
|
+
path: string;
|
|
34
|
+
} | {
|
|
35
|
+
ok: false;
|
|
36
|
+
} & ZonePathError;
|
|
37
|
+
/**
|
|
38
|
+
* Rule 2 — denied by name, checked BEFORE any zone lookup.
|
|
39
|
+
*
|
|
40
|
+
* `src/Scene.tsx` is the creator's surface and the file the whole safety
|
|
41
|
+
* argument rests on. It is the ONLY entry here that needs rule 2 to survive:
|
|
42
|
+
* it passes every other rule (a `src/` path with an allowed extension), so
|
|
43
|
+
* without this list a manifest listing it under `platform` would be obeyed. The
|
|
44
|
+
* grounding contracts are the creator's own agent contract and are additionally
|
|
45
|
+
* refused by rules 6 and 7 — they are named here so rule 2 is the rule that
|
|
46
|
+
* fires for them, which is what the corpus asserts.
|
|
47
|
+
*
|
|
48
|
+
* A manifest listing any of these under `platform` is REFUSED rather than
|
|
49
|
+
* obeyed. That is why this cannot be expressed as "not in the creator list":
|
|
50
|
+
* the manifest is the thing being distrusted.
|
|
51
|
+
*
|
|
52
|
+
* ONLY `src/Scene.tsx` IS NAMED HERE, and the three grounding contracts are
|
|
53
|
+
* deliberately absent — not weakened, ALREADY REFUSED, twice, before this rule
|
|
54
|
+
* is consulted. Each is a single-segment root path, and rule 6 admits a
|
|
55
|
+
* single-segment path only if it is in ALLOWED_ROOT_FILES, which none of them is;
|
|
56
|
+
* rule 7's extension allowlist refuses `.md` again. Naming them would therefore
|
|
57
|
+
* add no refusal, while putting a vendor-specific agent filename into CLI source
|
|
58
|
+
* — which f12 tripwire (a) sweeps for absolutely, because the platform must never
|
|
59
|
+
* be able to tell which agent is driving it. A name assembled from fragments to
|
|
60
|
+
* satisfy both would be worse than either: the source would no longer contain a
|
|
61
|
+
* string the next reader can grep, so the code would lie about what it refuses.
|
|
62
|
+
*
|
|
63
|
+
* `src/Scene.tsx` genuinely needs this rule: it is a two-segment path under `src`,
|
|
64
|
+
* so rules 6 and 7 both ADMIT it, and only a deny-by-name stops a manifest that
|
|
65
|
+
* lists the creator's own file as a write target. It is also the one file the
|
|
66
|
+
* whole safety argument rests on.
|
|
67
|
+
*
|
|
68
|
+
* NOTHING HERE BRANCHES on which of these files exists, which is the property
|
|
69
|
+
* the tripwire actually protects.
|
|
70
|
+
*/
|
|
71
|
+
export declare const DENIED_BY_NAME: readonly string[];
|
|
72
|
+
/**
|
|
73
|
+
* Rule 2's predicate, CASE-INSENSITIVE, exported so the floor and the write site
|
|
74
|
+
* cannot drift.
|
|
75
|
+
*
|
|
76
|
+
* An exact match was a hole, and it was the worst available hole: `src/scene.tsx`
|
|
77
|
+
* passed every rule, and on a case-insensitive filesystem (macOS default,
|
|
78
|
+
* Windows) the write RESOLVED ONTO the creator's `src/Scene.tsx` — destroying the
|
|
79
|
+
* one file the entire safety argument rests on, with no backup, no pre-flight
|
|
80
|
+
* check and no revert, all ruled out deliberately. Worse, the report then named
|
|
81
|
+
* the destroyed file under LEFT ALONE while filing the destruction as a clean
|
|
82
|
+
* replacement, so the only compensating control pointed away from the damage.
|
|
83
|
+
*
|
|
84
|
+
* Case is the ASCII member of rule 1's family: the reason unicode is normalized
|
|
85
|
+
* is that a path can render as one file and BE another, defeating the human
|
|
86
|
+
* review of the manifest diff, which is the last control once the mechanical ones
|
|
87
|
+
* are gone. A case-only spelling does exactly that in plain ASCII.
|
|
88
|
+
*
|
|
89
|
+
* Case-insensitive is correct on a case-SENSITIVE filesystem too: there, a
|
|
90
|
+
* manifest naming a case-variant of the creator's surface creates a second file
|
|
91
|
+
* shadowing theirs, which is hostile for a different reason.
|
|
92
|
+
*/
|
|
93
|
+
export declare function isDeniedByName(path: string): boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Rule 6 — the top-level allowlist for a single-segment path.
|
|
96
|
+
*
|
|
97
|
+
* Refusal by name-absence rather than by a rule enumerating every hazard, so a
|
|
98
|
+
* file class nobody predicted (`pnpm-lock.yaml`, a `Makefile`, a `Dockerfile`)
|
|
99
|
+
* is refused without anyone having thought of it.
|
|
100
|
+
*
|
|
101
|
+
* `.eslintrc.json` is DELIBERATELY ABSENT even though the template ships it and
|
|
102
|
+
* the record's example manifest lists it under `platform`. Rule 4 refuses every
|
|
103
|
+
* dot-leading segment and runs first, so an entry here would be unreachable —
|
|
104
|
+
* and rule 4 is the arm that kills `.env`, `.npmrc`, `.gitignore` and
|
|
105
|
+
* `.github/…`. Admitting one dotfile means either narrowing rule 4 or carrying a
|
|
106
|
+
* per-name exception through the floor, and both weaken the control that exists
|
|
107
|
+
* because a dotfile overwrite is the credential-adjacent case. The consequence
|
|
108
|
+
* is that `.eslintrc.json` joins `scripts/*.mjs` in the reported-not-upgraded
|
|
109
|
+
* set. Raised as a fork against the record's §5 example rather than resolved by
|
|
110
|
+
* widening the floor.
|
|
111
|
+
*/
|
|
112
|
+
export declare const ALLOWED_ROOT_FILES: readonly string[];
|
|
113
|
+
/** Rule 6 — the only directories a multi-segment path may start in. */
|
|
114
|
+
export declare const ALLOWED_TOP_DIRS: readonly string[];
|
|
115
|
+
/** Rule 7 — the extension allowlist. Closes `.yaml`, `.yml`, `.sh`, extensionless. */
|
|
116
|
+
export declare const ALLOWED_EXTENSIONS: readonly string[];
|
|
117
|
+
/**
|
|
118
|
+
* Validate ONE path against rules 1-8. Rule 9 is set-level and lives in
|
|
119
|
+
* `validateZoneSet`.
|
|
120
|
+
*
|
|
121
|
+
* The order is load-bearing in one place and one place only: rule 2 runs before
|
|
122
|
+
* anything that could consult a zone. An innocuous-looking reorder that looks
|
|
123
|
+
* up the zone first and denies after is the regression this ordering exists to
|
|
124
|
+
* prevent, and it is invisible in a diff.
|
|
125
|
+
*/
|
|
126
|
+
export declare function validateZonePath(raw: string): ZonePathResult;
|
|
127
|
+
/**
|
|
128
|
+
* The same floor, told which zone the entry was listed in.
|
|
129
|
+
*
|
|
130
|
+
* Rule 2 applies to the OVERWRITE zone. `src/Scene.tsx` listed under `creator`
|
|
131
|
+
* is the correct and required content of a legitimate manifest — `creator` is
|
|
132
|
+
* the never-written list, so a denied name there is the manifest agreeing with
|
|
133
|
+
* the floor rather than attacking it. Applying rule 2 to both arrays refuses the
|
|
134
|
+
* legitimate zone set, which is the same shape of defect as the publish
|
|
135
|
+
* validator's `index.html` requirement: a control that fails the happy path gets
|
|
136
|
+
* dropped under build pressure while the record still claims it exists.
|
|
137
|
+
*
|
|
138
|
+
* Rules 1 and 3-8 apply to BOTH zones, because they bound what a manifest may
|
|
139
|
+
* NAME at all, and an unlisted-but-named path is still a path this code reads.
|
|
140
|
+
*/
|
|
141
|
+
export declare function validateZonePathIn(raw: string, zone: "platform" | "creator"): ZonePathResult;
|
|
142
|
+
export interface ZoneSet {
|
|
143
|
+
platform: string[];
|
|
144
|
+
creator: string[];
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Rule 9 — the set level. ONE failure refuses the WHOLE manifest.
|
|
148
|
+
*
|
|
149
|
+
* A per-entry skip looks like robustness and is the smuggling path: it is
|
|
150
|
+
* exactly how one hostile entry rides in behind twenty good ones, and the twenty
|
|
151
|
+
* good writes are what make the report look normal. There is no partial accept
|
|
152
|
+
* and no `continue` in this function.
|
|
153
|
+
*/
|
|
154
|
+
export declare function validateZoneSet(set: ZoneSet): ZonePathError | undefined;
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The path floor: whether a path may be written into a creator's repository at
|
|
3
|
+
* all. Purpose-built, and deliberately NOT `publish/paths.ts`.
|
|
4
|
+
*
|
|
5
|
+
* The publish validator was written to bound an S3 key for a dist upload, where
|
|
6
|
+
* the worst case is a weird object name. This bounds a DESTINATION FOR AN
|
|
7
|
+
* OVERWRITE IN SOMEONE ELSE'S REPOSITORY, where the worst case is code
|
|
8
|
+
* execution. Run against the zone corpus it accepts `.git/hooks/pre-commit`,
|
|
9
|
+
* `.git/config`, `.env`, `.npmrc`, `.gitignore`,
|
|
10
|
+
* `.github/workflows/release.yml`, `node_modules/.bin/esbuild`,
|
|
11
|
+
* `pnpm-lock.yaml` — and `src/Scene.tsx`, the one file the entire safety
|
|
12
|
+
* argument names. It rejects only `../` and `.`-segments and normalizes no
|
|
13
|
+
* unicode, so an RTL override, a Cyrillic homoglyph and a division slash all
|
|
14
|
+
* pass. Its `validatePathSet` also hard-requires `index.html`, so it FAILS the
|
|
15
|
+
* legitimate zone set — a reuse would have gone red on the happy path and the
|
|
16
|
+
* cheapest fix would have been to drop the control while the record still
|
|
17
|
+
* claimed it.
|
|
18
|
+
*
|
|
19
|
+
* THE MANIFEST IS NEVER CONSULTED HERE. It decides which of the two zones a
|
|
20
|
+
* PERMISSIBLE path belongs to; it is never the authority for whether a path is
|
|
21
|
+
* permissible. A boundary with no floor under it is not a boundary — it is a
|
|
22
|
+
* suggestion the manifest could withdraw. So this code is identical for every
|
|
23
|
+
* template version and runs BEFORE the manifest is read.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Rule 2 — denied by name, checked BEFORE any zone lookup.
|
|
27
|
+
*
|
|
28
|
+
* `src/Scene.tsx` is the creator's surface and the file the whole safety
|
|
29
|
+
* argument rests on. It is the ONLY entry here that needs rule 2 to survive:
|
|
30
|
+
* it passes every other rule (a `src/` path with an allowed extension), so
|
|
31
|
+
* without this list a manifest listing it under `platform` would be obeyed. The
|
|
32
|
+
* grounding contracts are the creator's own agent contract and are additionally
|
|
33
|
+
* refused by rules 6 and 7 — they are named here so rule 2 is the rule that
|
|
34
|
+
* fires for them, which is what the corpus asserts.
|
|
35
|
+
*
|
|
36
|
+
* A manifest listing any of these under `platform` is REFUSED rather than
|
|
37
|
+
* obeyed. That is why this cannot be expressed as "not in the creator list":
|
|
38
|
+
* the manifest is the thing being distrusted.
|
|
39
|
+
*
|
|
40
|
+
* ONLY `src/Scene.tsx` IS NAMED HERE, and the three grounding contracts are
|
|
41
|
+
* deliberately absent — not weakened, ALREADY REFUSED, twice, before this rule
|
|
42
|
+
* is consulted. Each is a single-segment root path, and rule 6 admits a
|
|
43
|
+
* single-segment path only if it is in ALLOWED_ROOT_FILES, which none of them is;
|
|
44
|
+
* rule 7's extension allowlist refuses `.md` again. Naming them would therefore
|
|
45
|
+
* add no refusal, while putting a vendor-specific agent filename into CLI source
|
|
46
|
+
* — which f12 tripwire (a) sweeps for absolutely, because the platform must never
|
|
47
|
+
* be able to tell which agent is driving it. A name assembled from fragments to
|
|
48
|
+
* satisfy both would be worse than either: the source would no longer contain a
|
|
49
|
+
* string the next reader can grep, so the code would lie about what it refuses.
|
|
50
|
+
*
|
|
51
|
+
* `src/Scene.tsx` genuinely needs this rule: it is a two-segment path under `src`,
|
|
52
|
+
* so rules 6 and 7 both ADMIT it, and only a deny-by-name stops a manifest that
|
|
53
|
+
* lists the creator's own file as a write target. It is also the one file the
|
|
54
|
+
* whole safety argument rests on.
|
|
55
|
+
*
|
|
56
|
+
* NOTHING HERE BRANCHES on which of these files exists, which is the property
|
|
57
|
+
* the tripwire actually protects.
|
|
58
|
+
*/
|
|
59
|
+
export const DENIED_BY_NAME = Object.freeze(["src/Scene.tsx"]);
|
|
60
|
+
/**
|
|
61
|
+
* Rule 2's predicate, CASE-INSENSITIVE, exported so the floor and the write site
|
|
62
|
+
* cannot drift.
|
|
63
|
+
*
|
|
64
|
+
* An exact match was a hole, and it was the worst available hole: `src/scene.tsx`
|
|
65
|
+
* passed every rule, and on a case-insensitive filesystem (macOS default,
|
|
66
|
+
* Windows) the write RESOLVED ONTO the creator's `src/Scene.tsx` — destroying the
|
|
67
|
+
* one file the entire safety argument rests on, with no backup, no pre-flight
|
|
68
|
+
* check and no revert, all ruled out deliberately. Worse, the report then named
|
|
69
|
+
* the destroyed file under LEFT ALONE while filing the destruction as a clean
|
|
70
|
+
* replacement, so the only compensating control pointed away from the damage.
|
|
71
|
+
*
|
|
72
|
+
* Case is the ASCII member of rule 1's family: the reason unicode is normalized
|
|
73
|
+
* is that a path can render as one file and BE another, defeating the human
|
|
74
|
+
* review of the manifest diff, which is the last control once the mechanical ones
|
|
75
|
+
* are gone. A case-only spelling does exactly that in plain ASCII.
|
|
76
|
+
*
|
|
77
|
+
* Case-insensitive is correct on a case-SENSITIVE filesystem too: there, a
|
|
78
|
+
* manifest naming a case-variant of the creator's surface creates a second file
|
|
79
|
+
* shadowing theirs, which is hostile for a different reason.
|
|
80
|
+
*/
|
|
81
|
+
export function isDeniedByName(path) {
|
|
82
|
+
const lowered = path.toLowerCase();
|
|
83
|
+
return DENIED_BY_NAME.some((denied) => denied.toLowerCase() === lowered);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Rule 6 — the top-level allowlist for a single-segment path.
|
|
87
|
+
*
|
|
88
|
+
* Refusal by name-absence rather than by a rule enumerating every hazard, so a
|
|
89
|
+
* file class nobody predicted (`pnpm-lock.yaml`, a `Makefile`, a `Dockerfile`)
|
|
90
|
+
* is refused without anyone having thought of it.
|
|
91
|
+
*
|
|
92
|
+
* `.eslintrc.json` is DELIBERATELY ABSENT even though the template ships it and
|
|
93
|
+
* the record's example manifest lists it under `platform`. Rule 4 refuses every
|
|
94
|
+
* dot-leading segment and runs first, so an entry here would be unreachable —
|
|
95
|
+
* and rule 4 is the arm that kills `.env`, `.npmrc`, `.gitignore` and
|
|
96
|
+
* `.github/…`. Admitting one dotfile means either narrowing rule 4 or carrying a
|
|
97
|
+
* per-name exception through the floor, and both weaken the control that exists
|
|
98
|
+
* because a dotfile overwrite is the credential-adjacent case. The consequence
|
|
99
|
+
* is that `.eslintrc.json` joins `scripts/*.mjs` in the reported-not-upgraded
|
|
100
|
+
* set. Raised as a fork against the record's §5 example rather than resolved by
|
|
101
|
+
* widening the floor.
|
|
102
|
+
*/
|
|
103
|
+
export const ALLOWED_ROOT_FILES = Object.freeze([
|
|
104
|
+
"index.html",
|
|
105
|
+
"package.json",
|
|
106
|
+
"tsconfig.json",
|
|
107
|
+
"tsconfig.node.json",
|
|
108
|
+
"vitest.config.ts",
|
|
109
|
+
"esbuild.config.mjs",
|
|
110
|
+
]);
|
|
111
|
+
/** Rule 6 — the only directories a multi-segment path may start in. */
|
|
112
|
+
export const ALLOWED_TOP_DIRS = Object.freeze(["src", "public"]);
|
|
113
|
+
/** Rule 7 — the extension allowlist. Closes `.yaml`, `.yml`, `.sh`, extensionless. */
|
|
114
|
+
export const ALLOWED_EXTENSIONS = Object.freeze([
|
|
115
|
+
".tsx",
|
|
116
|
+
".ts",
|
|
117
|
+
".css",
|
|
118
|
+
".mjs",
|
|
119
|
+
".json",
|
|
120
|
+
".html",
|
|
121
|
+
".d.ts",
|
|
122
|
+
]);
|
|
123
|
+
const MAX_SEGMENTS = 4;
|
|
124
|
+
const MAX_PATH_BYTES = 256;
|
|
125
|
+
const NON_ASCII = /[^\x20-\x7e]/;
|
|
126
|
+
const DRIVE_LETTER = /^[a-zA-Z]:/;
|
|
127
|
+
const CONTROL_BYTES = /[\x00-\x1f\x7f-\x9f]/;
|
|
128
|
+
const SURROGATE = /[\ud800-\udfff]/;
|
|
129
|
+
/**
|
|
130
|
+
* Validate ONE path against rules 1-8. Rule 9 is set-level and lives in
|
|
131
|
+
* `validateZoneSet`.
|
|
132
|
+
*
|
|
133
|
+
* The order is load-bearing in one place and one place only: rule 2 runs before
|
|
134
|
+
* anything that could consult a zone. An innocuous-looking reorder that looks
|
|
135
|
+
* up the zone first and denies after is the regression this ordering exists to
|
|
136
|
+
* prevent, and it is invisible in a diff.
|
|
137
|
+
*/
|
|
138
|
+
export function validateZonePath(raw) {
|
|
139
|
+
return validateZonePathIn(raw, "platform");
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The same floor, told which zone the entry was listed in.
|
|
143
|
+
*
|
|
144
|
+
* Rule 2 applies to the OVERWRITE zone. `src/Scene.tsx` listed under `creator`
|
|
145
|
+
* is the correct and required content of a legitimate manifest — `creator` is
|
|
146
|
+
* the never-written list, so a denied name there is the manifest agreeing with
|
|
147
|
+
* the floor rather than attacking it. Applying rule 2 to both arrays refuses the
|
|
148
|
+
* legitimate zone set, which is the same shape of defect as the publish
|
|
149
|
+
* validator's `index.html` requirement: a control that fails the happy path gets
|
|
150
|
+
* dropped under build pressure while the record still claims it exists.
|
|
151
|
+
*
|
|
152
|
+
* Rules 1 and 3-8 apply to BOTH zones, because they bound what a manifest may
|
|
153
|
+
* NAME at all, and an unlisted-but-named path is still a path this code reads.
|
|
154
|
+
*/
|
|
155
|
+
export function validateZonePathIn(raw, zone) {
|
|
156
|
+
// Rule 1 — NFC-normalize, then reject any non-ASCII byte.
|
|
157
|
+
//
|
|
158
|
+
// Normalizing FIRST matters: a decomposed sequence and its composed form are
|
|
159
|
+
// the same file, and comparing the un-normalized string against a deny list
|
|
160
|
+
// would miss one of the two spellings. Then ASCII-only, because every hazard
|
|
161
|
+
// in this class survives every other rule: a division slash is ONE segment so
|
|
162
|
+
// no segment rule sees it, and an RTL override, a Cyrillic homoglyph and a
|
|
163
|
+
// trailing zero-width space each render as an ordinary path in a report or a
|
|
164
|
+
// manifest diff while naming a different file. That defeats the human review
|
|
165
|
+
// of the manifest diff, which is the only control left once the mechanical
|
|
166
|
+
// ones are satisfied. The template has no non-ASCII filename and never will.
|
|
167
|
+
const path = raw.normalize("NFC");
|
|
168
|
+
if (NON_ASCII.test(path))
|
|
169
|
+
return reject("non_ascii", raw);
|
|
170
|
+
// Rule 2 — denied by name. BEFORE the zone lookup, and before the cheaper
|
|
171
|
+
// structural rules, so the refusal a reader cares about is the one reported.
|
|
172
|
+
if (zone === "platform" && isDeniedByName(path)) {
|
|
173
|
+
return reject("denied_by_name", raw);
|
|
174
|
+
}
|
|
175
|
+
// Rule 3 — the arms of the publish validator that were right, restated rather
|
|
176
|
+
// than imported. Restated because importing would couple this floor to a
|
|
177
|
+
// module whose threat model is a different one, and the next edit there would
|
|
178
|
+
// silently change what may be written into a repository.
|
|
179
|
+
if (path.includes("\\"))
|
|
180
|
+
return reject("malformed", raw);
|
|
181
|
+
if (path.startsWith("/"))
|
|
182
|
+
return reject("malformed", raw);
|
|
183
|
+
if (DRIVE_LETTER.test(path))
|
|
184
|
+
return reject("malformed", raw);
|
|
185
|
+
if (path.includes("%"))
|
|
186
|
+
return reject("malformed", raw);
|
|
187
|
+
// Control bytes and lone surrogates are already excluded by rule 1 (both are
|
|
188
|
+
// outside printable ASCII); the checks stay because rule 3 is asserted by
|
|
189
|
+
// name and a future widening of rule 1 must not silently drop them.
|
|
190
|
+
if (CONTROL_BYTES.test(path))
|
|
191
|
+
return reject("malformed", raw);
|
|
192
|
+
if (SURROGATE.test(path))
|
|
193
|
+
return reject("malformed", raw);
|
|
194
|
+
// Rule 8 — length, before the split, so a pathological string is bounded
|
|
195
|
+
// before any per-segment work runs over it.
|
|
196
|
+
if (Buffer.byteLength(path, "utf8") > MAX_PATH_BYTES)
|
|
197
|
+
return reject("too_long", raw);
|
|
198
|
+
const segments = path.split("/");
|
|
199
|
+
// Rule 8 — depth.
|
|
200
|
+
if (segments.length > MAX_SEGMENTS)
|
|
201
|
+
return reject("too_deep", raw);
|
|
202
|
+
// Rule 5 — `node_modules` as a first segment. BEFORE rule 4, so the rule
|
|
203
|
+
// REPORTED for `node_modules/.bin/esbuild` is the one that names why it is
|
|
204
|
+
// dangerous (a binary the creator's next build executes) rather than the
|
|
205
|
+
// incidental dot on its second segment. Both refuse it; the attribution is
|
|
206
|
+
// what a reader and the corpus depend on, and rule 5 exists precisely because
|
|
207
|
+
// it is not redundant with rule 4.
|
|
208
|
+
if (segments[0] === "node_modules")
|
|
209
|
+
return reject("node_modules", raw);
|
|
210
|
+
// Rule 4 — the dotfile rule the publish validator has none of. `.env`,
|
|
211
|
+
// `.npmrc`, `.gitignore`, `.git/hooks/pre-commit` and `.github/…` all die
|
|
212
|
+
// here. `..` and `.` are covered by the same clause rather than separately,
|
|
213
|
+
// because "begins with a dot" is the superset and stating it once is what
|
|
214
|
+
// makes the rule hard to narrow by accident.
|
|
215
|
+
for (const seg of segments) {
|
|
216
|
+
if (seg === "")
|
|
217
|
+
return reject("dot_segment", raw);
|
|
218
|
+
if (seg.startsWith("."))
|
|
219
|
+
return reject("dot_segment", raw);
|
|
220
|
+
}
|
|
221
|
+
// Rule 6 — the top-level allowlist.
|
|
222
|
+
if (segments.length === 1) {
|
|
223
|
+
if (!ALLOWED_ROOT_FILES.includes(segments[0]))
|
|
224
|
+
return reject("top_level", raw);
|
|
225
|
+
}
|
|
226
|
+
else if (!ALLOWED_TOP_DIRS.includes(segments[0])) {
|
|
227
|
+
// `scripts/dev.mjs` is refused HERE, and that is correct and deliberate:
|
|
228
|
+
// the template's build scripts execute on the creator's machine, which is
|
|
229
|
+
// the closest thing in the template to the `node_modules/.bin` case. They
|
|
230
|
+
// move to the report's not-permitted-to-be-upgraded set rather than the
|
|
231
|
+
// allowlist being widened to admit them.
|
|
232
|
+
return reject("top_level", raw);
|
|
233
|
+
}
|
|
234
|
+
// Rule 7 — extension. `.d.ts` is checked before `.ts` would match it, but
|
|
235
|
+
// both are allowed so the order is not load-bearing; the check is a suffix
|
|
236
|
+
// test rather than a parse so `vite-env.d.ts` needs no special case.
|
|
237
|
+
if (!ALLOWED_EXTENSIONS.some((ext) => path.endsWith(ext))) {
|
|
238
|
+
return reject("extension", raw);
|
|
239
|
+
}
|
|
240
|
+
return { ok: true, path };
|
|
241
|
+
}
|
|
242
|
+
function reject(rule, path) {
|
|
243
|
+
return { ok: false, rule, path };
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Rule 9 — the set level. ONE failure refuses the WHOLE manifest.
|
|
247
|
+
*
|
|
248
|
+
* A per-entry skip looks like robustness and is the smuggling path: it is
|
|
249
|
+
* exactly how one hostile entry rides in behind twenty good ones, and the twenty
|
|
250
|
+
* good writes are what make the report look normal. There is no partial accept
|
|
251
|
+
* and no `continue` in this function.
|
|
252
|
+
*/
|
|
253
|
+
export function validateZoneSet(set) {
|
|
254
|
+
const seen = new Set();
|
|
255
|
+
const seenLower = new Set();
|
|
256
|
+
const platform = new Set();
|
|
257
|
+
for (const zone of ["platform", "creator"]) {
|
|
258
|
+
for (const raw of set[zone]) {
|
|
259
|
+
const result = validateZonePathIn(raw, zone);
|
|
260
|
+
if (!result.ok)
|
|
261
|
+
return { rule: result.rule, path: result.path };
|
|
262
|
+
const p = result.path;
|
|
263
|
+
// Reported as in-both-zones rather than as a plain duplicate, because the
|
|
264
|
+
// two say different things to whoever fixes the manifest: a duplicate is a
|
|
265
|
+
// typo, and a path in both arrays is a file whose OWNER is contested —
|
|
266
|
+
// which either destroys creator work or leaves plumbing stale, depending
|
|
267
|
+
// on which list a reader assumes wins.
|
|
268
|
+
if (zone === "creator" && platform.has(p)) {
|
|
269
|
+
return { rule: "in_both_zones", path: p };
|
|
270
|
+
}
|
|
271
|
+
if (seen.has(p))
|
|
272
|
+
return { rule: "duplicate", path: p };
|
|
273
|
+
// A case-duplicate is two names for one file on macOS and Windows and two
|
|
274
|
+
// files on Linux, so the manifest means different things on different
|
|
275
|
+
// machines — and the overwrite set is the wrong place to discover that.
|
|
276
|
+
const lower = p.toLowerCase();
|
|
277
|
+
if (seenLower.has(lower))
|
|
278
|
+
return { rule: "case_duplicate", path: p };
|
|
279
|
+
seen.add(p);
|
|
280
|
+
seenLower.add(lower);
|
|
281
|
+
if (zone === "platform")
|
|
282
|
+
platform.add(p);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.vincentt-template.json` — the template's own per-version zone manifest.
|
|
3
|
+
*
|
|
4
|
+
* It is the authority for WHICH OF THE TWO ZONES a permissible path belongs to.
|
|
5
|
+
* It is never the authority for WHETHER a path is permissible: that is the
|
|
6
|
+
* floor, which is code, ships in the CLI, is identical for every template
|
|
7
|
+
* version, and runs BEFORE anything here consults a zone.
|
|
8
|
+
*
|
|
9
|
+
* Per-version because a project scaffolded from 1.2.0 needs 1.2.0's set, and
|
|
10
|
+
* only the 1.2.0 tag authoritatively knows it. Enumerated arrays with no globs,
|
|
11
|
+
* because the default must be LEAVE ALONE — an implicit rule claims every file
|
|
12
|
+
* the creator ever added, including one named `Scene2.tsx`, and the day a
|
|
13
|
+
* template file ships unclassified the default becomes destroy the creator's
|
|
14
|
+
* work.
|
|
15
|
+
*
|
|
16
|
+
* AN UNRECOGNIZED `schema` IS BLOCKED, never best-effort. A manifest this CLI
|
|
17
|
+
* does not understand is a manifest whose overwrite set it cannot bound.
|
|
18
|
+
*/
|
|
19
|
+
import { type ZonePathError, type ZoneSet } from "./zonePath.js";
|
|
20
|
+
/** The only schema this CLI understands. */
|
|
21
|
+
export declare const SUPPORTED_SCHEMA = 1;
|
|
22
|
+
export type ManifestRefusal = {
|
|
23
|
+
kind: "unparseable";
|
|
24
|
+
} | {
|
|
25
|
+
kind: "unsupported_schema";
|
|
26
|
+
schema: unknown;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "malformed";
|
|
29
|
+
} | {
|
|
30
|
+
kind: "floor";
|
|
31
|
+
error: ZonePathError;
|
|
32
|
+
};
|
|
33
|
+
export type ManifestResult = {
|
|
34
|
+
ok: true;
|
|
35
|
+
zones: ZoneSet;
|
|
36
|
+
} | {
|
|
37
|
+
ok: false;
|
|
38
|
+
refusal: ManifestRefusal;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Parse and floor-validate a manifest document.
|
|
42
|
+
*
|
|
43
|
+
* The floor runs over the WHOLE set before any caller may read a zone, and ONE
|
|
44
|
+
* failing entry refuses the WHOLE manifest. A per-entry skip looks like
|
|
45
|
+
* robustness and is the smuggling path — it is how one hostile entry rides in
|
|
46
|
+
* behind twenty good ones, with the twenty good writes making the report look
|
|
47
|
+
* normal.
|
|
48
|
+
*/
|
|
49
|
+
export declare function parseZoneManifest(raw: string): ManifestResult;
|
|
50
|
+
export type Zone = "platform" | "creator" | "unlisted";
|
|
51
|
+
/**
|
|
52
|
+
* Which zone a path is in.
|
|
53
|
+
*
|
|
54
|
+
* `unlisted` is a first-class answer and it means LEAVE ALONE — never read,
|
|
55
|
+
* never written. Every file the creator added is unlisted, which is the whole
|
|
56
|
+
* reason the lists are enumerated rather than inferred.
|
|
57
|
+
*
|
|
58
|
+
* This may only be called on a manifest the floor has already accepted, which is
|
|
59
|
+
* why it takes a validated `ZoneSet` rather than a raw document: the type is the
|
|
60
|
+
* reminder that permissibility was decided elsewhere and earlier.
|
|
61
|
+
*/
|
|
62
|
+
export declare function zoneOf(zones: ZoneSet, path: string): Zone;
|
|
63
|
+
/** The overwrite set: exactly the `platform` list, in manifest order. */
|
|
64
|
+
export declare function overwriteSet(zones: ZoneSet): string[];
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.vincentt-template.json` — the template's own per-version zone manifest.
|
|
3
|
+
*
|
|
4
|
+
* It is the authority for WHICH OF THE TWO ZONES a permissible path belongs to.
|
|
5
|
+
* It is never the authority for WHETHER a path is permissible: that is the
|
|
6
|
+
* floor, which is code, ships in the CLI, is identical for every template
|
|
7
|
+
* version, and runs BEFORE anything here consults a zone.
|
|
8
|
+
*
|
|
9
|
+
* Per-version because a project scaffolded from 1.2.0 needs 1.2.0's set, and
|
|
10
|
+
* only the 1.2.0 tag authoritatively knows it. Enumerated arrays with no globs,
|
|
11
|
+
* because the default must be LEAVE ALONE — an implicit rule claims every file
|
|
12
|
+
* the creator ever added, including one named `Scene2.tsx`, and the day a
|
|
13
|
+
* template file ships unclassified the default becomes destroy the creator's
|
|
14
|
+
* work.
|
|
15
|
+
*
|
|
16
|
+
* AN UNRECOGNIZED `schema` IS BLOCKED, never best-effort. A manifest this CLI
|
|
17
|
+
* does not understand is a manifest whose overwrite set it cannot bound.
|
|
18
|
+
*/
|
|
19
|
+
import { validateZoneSet } from "./zonePath.js";
|
|
20
|
+
/** The only schema this CLI understands. */
|
|
21
|
+
export const SUPPORTED_SCHEMA = 1;
|
|
22
|
+
/**
|
|
23
|
+
* Parse and floor-validate a manifest document.
|
|
24
|
+
*
|
|
25
|
+
* The floor runs over the WHOLE set before any caller may read a zone, and ONE
|
|
26
|
+
* failing entry refuses the WHOLE manifest. A per-entry skip looks like
|
|
27
|
+
* robustness and is the smuggling path — it is how one hostile entry rides in
|
|
28
|
+
* behind twenty good ones, with the twenty good writes making the report look
|
|
29
|
+
* normal.
|
|
30
|
+
*/
|
|
31
|
+
export function parseZoneManifest(raw) {
|
|
32
|
+
let doc;
|
|
33
|
+
try {
|
|
34
|
+
doc = JSON.parse(raw);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { ok: false, refusal: { kind: "unparseable" } };
|
|
38
|
+
}
|
|
39
|
+
if (typeof doc !== "object" || doc === null) {
|
|
40
|
+
return { ok: false, refusal: { kind: "malformed" } };
|
|
41
|
+
}
|
|
42
|
+
const { schema, platform, creator } = doc;
|
|
43
|
+
if (schema !== SUPPORTED_SCHEMA) {
|
|
44
|
+
return { ok: false, refusal: { kind: "unsupported_schema", schema } };
|
|
45
|
+
}
|
|
46
|
+
if (!isStringArray(platform) || !isStringArray(creator)) {
|
|
47
|
+
return { ok: false, refusal: { kind: "malformed" } };
|
|
48
|
+
}
|
|
49
|
+
const zones = { platform, creator };
|
|
50
|
+
const error = validateZoneSet(zones);
|
|
51
|
+
if (error !== undefined)
|
|
52
|
+
return { ok: false, refusal: { kind: "floor", error } };
|
|
53
|
+
return { ok: true, zones };
|
|
54
|
+
}
|
|
55
|
+
function isStringArray(v) {
|
|
56
|
+
return Array.isArray(v) && v.every((x) => typeof x === "string");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Which zone a path is in.
|
|
60
|
+
*
|
|
61
|
+
* `unlisted` is a first-class answer and it means LEAVE ALONE — never read,
|
|
62
|
+
* never written. Every file the creator added is unlisted, which is the whole
|
|
63
|
+
* reason the lists are enumerated rather than inferred.
|
|
64
|
+
*
|
|
65
|
+
* This may only be called on a manifest the floor has already accepted, which is
|
|
66
|
+
* why it takes a validated `ZoneSet` rather than a raw document: the type is the
|
|
67
|
+
* reminder that permissibility was decided elsewhere and earlier.
|
|
68
|
+
*/
|
|
69
|
+
export function zoneOf(zones, path) {
|
|
70
|
+
if (zones.platform.includes(path))
|
|
71
|
+
return "platform";
|
|
72
|
+
if (zones.creator.includes(path))
|
|
73
|
+
return "creator";
|
|
74
|
+
return "unlisted";
|
|
75
|
+
}
|
|
76
|
+
/** The overwrite set: exactly the `platform` list, in manifest order. */
|
|
77
|
+
export function overwriteSet(zones) {
|
|
78
|
+
return [...zones.platform];
|
|
79
|
+
}
|
package/dist/publish/api.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../shared/config.js";
|
|
2
2
|
import type { ManifestEntry } from "./manifest.js";
|
|
3
|
+
import type { BuiltWith } from "../platform/resolve.js";
|
|
3
4
|
export interface PublishProject {
|
|
4
5
|
id: string;
|
|
5
6
|
slug: string;
|
|
@@ -77,11 +78,21 @@ export declare class PublishApiError extends Error {
|
|
|
77
78
|
/** The ONE call before the checklist. liveVersion + livePublishedAt ride it, by design. */
|
|
78
79
|
export declare function readProject(cfg: ResolvedConfig, projectId: string, fetchImpl?: typeof fetch): Promise<PublishProject>;
|
|
79
80
|
/**
|
|
80
|
-
* Open an upload. The request body carries `files` (path/size/sha256)
|
|
81
|
-
* optional `note`, AND NOTHING ELSE — there is no
|
|
82
|
-
* field a parsed title, description or image URL
|
|
81
|
+
* Open an upload. The request body carries `files` (path/size/sha256), an
|
|
82
|
+
* optional `note`, and an optional `builtWith`, AND NOTHING ELSE — there is no
|
|
83
|
+
* field for a MIME type and no field a parsed title, description or image URL
|
|
84
|
+
* could land in.
|
|
85
|
+
*
|
|
86
|
+
* `builtWith` CANNOT FAIL A PUBLISH. It is evidence and never authorization: it
|
|
87
|
+
* is computed on a machine the platform does not control, so nothing may branch
|
|
88
|
+
* on it, and the API validates it in its own pass after the manifest, dropping
|
|
89
|
+
* over-bound entries rather than refusing. An instrument that can break the
|
|
90
|
+
* product's most important write path is worse than an instrument with a gap —
|
|
91
|
+
* which is why it is omitted entirely when nothing resolved rather than sent as
|
|
92
|
+
* an empty object. `{}` means "resolution ran and found nothing", a different
|
|
93
|
+
* and specific fact.
|
|
83
94
|
*/
|
|
84
|
-
export declare function openUpload(cfg: ResolvedConfig, projectId: string, files: ManifestEntry[], note: string | undefined, fetchImpl?: typeof fetch): Promise<OpenedUpload>;
|
|
95
|
+
export declare function openUpload(cfg: ResolvedConfig, projectId: string, files: ManifestEntry[], note: string | undefined, fetchImpl?: typeof fetch, builtWith?: BuiltWith): Promise<OpenedUpload>;
|
|
85
96
|
export declare function commitUpload(cfg: ResolvedConfig, projectId: string, uploadId: string, fetchImpl?: typeof fetch): Promise<CommitResult>;
|
|
86
97
|
/** Abandon an open upload. Idempotent server-side; a failure here must never mask the real error. */
|
|
87
98
|
export declare function abandonUpload(cfg: ResolvedConfig, projectId: string, uploadId: string, fetchImpl?: typeof fetch): Promise<void>;
|
package/dist/publish/api.js
CHANGED
|
@@ -58,15 +58,29 @@ export async function readProject(cfg, projectId, fetchImpl = fetch) {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* Open an upload. The request body carries `files` (path/size/sha256)
|
|
62
|
-
* optional `note`, AND NOTHING ELSE — there is no
|
|
63
|
-
* field a parsed title, description or image URL
|
|
61
|
+
* Open an upload. The request body carries `files` (path/size/sha256), an
|
|
62
|
+
* optional `note`, and an optional `builtWith`, AND NOTHING ELSE — there is no
|
|
63
|
+
* field for a MIME type and no field a parsed title, description or image URL
|
|
64
|
+
* could land in.
|
|
65
|
+
*
|
|
66
|
+
* `builtWith` CANNOT FAIL A PUBLISH. It is evidence and never authorization: it
|
|
67
|
+
* is computed on a machine the platform does not control, so nothing may branch
|
|
68
|
+
* on it, and the API validates it in its own pass after the manifest, dropping
|
|
69
|
+
* over-bound entries rather than refusing. An instrument that can break the
|
|
70
|
+
* product's most important write path is worse than an instrument with a gap —
|
|
71
|
+
* which is why it is omitted entirely when nothing resolved rather than sent as
|
|
72
|
+
* an empty object. `{}` means "resolution ran and found nothing", a different
|
|
73
|
+
* and specific fact.
|
|
64
74
|
*/
|
|
65
|
-
export async function openUpload(cfg, projectId, files, note, fetchImpl = fetch) {
|
|
75
|
+
export async function openUpload(cfg, projectId, files, note, fetchImpl = fetch, builtWith) {
|
|
66
76
|
const res = await fetchImpl(`${cfg.apiUrl}/projects/${encodeURIComponent(projectId)}/publishes`, {
|
|
67
77
|
method: "POST",
|
|
68
78
|
headers: { ...authHeaders(cfg), "content-type": "application/json" },
|
|
69
|
-
body: JSON.stringify({
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
files,
|
|
81
|
+
...(note ? { note } : {}),
|
|
82
|
+
...(builtWith ? { builtWith } : {}),
|
|
83
|
+
}),
|
|
70
84
|
});
|
|
71
85
|
if (!res.ok)
|
|
72
86
|
throw await readError(res);
|