@cimplify/cli 0.15.2 → 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/LICENSE +21 -0
- package/README.md +2 -2
- package/dist/add-SXC6RUPQ.mjs +140 -0
- package/dist/{add-KRIIKAS4.mjs → add-XQVQLD3F.mjs} +4 -4
- package/dist/ai-NQH5L4I6.mjs +17 -0
- package/dist/{assets-CWB72PZG.mjs → assets-FZGVOQXH.mjs} +12 -7
- package/dist/{auth-step-up-Z6KOTTV7.mjs → auth-step-up-67LCPDNL.mjs} +5 -5
- package/dist/build-J4ZUIOPX.mjs +625 -0
- package/dist/{cancel-ZHVVILOU.mjs → cancel-VGJHZJ4J.mjs} +4 -4
- package/dist/capture-RAJ2B3BH.mjs +69 -0
- package/dist/chunk-25ZPFHLF.mjs +51 -0
- package/dist/chunk-47ZXSY47.mjs +52 -0
- package/dist/{chunk-NK6VC264.mjs → chunk-54J3XR4T.mjs} +2 -2
- package/dist/chunk-5HP2INMY.mjs +10631 -0
- package/dist/chunk-7JR6JCFF.mjs +32 -0
- package/dist/chunk-7PQL7E7K.mjs +68 -0
- package/dist/chunk-AKACUJ3W.mjs +81 -0
- package/dist/chunk-BYNE553A.mjs +2945 -0
- package/dist/{chunk-NYXA6X56.mjs → chunk-CLQYPTJ2.mjs} +31 -11
- package/dist/chunk-CNZAOVSK.mjs +104 -0
- package/dist/chunk-CPBESPWK.mjs +5576 -0
- package/dist/{chunk-GRR5U2M5.mjs → chunk-DYWUPHV3.mjs} +11 -7
- package/dist/chunk-EAV2P4N3.mjs +1941 -0
- package/dist/{chunk-KN2YSKXZ.mjs → chunk-EDZSOL5O.mjs} +14 -6
- package/dist/chunk-EGQIPYUN.mjs +696 -0
- package/dist/{chunk-DMLQBAGC.mjs → chunk-EOO4UOQD.mjs} +1 -1
- package/dist/chunk-EYEMPIMP.mjs +74 -0
- package/dist/chunk-G2ZEFSQJ.mjs +46 -0
- package/dist/{chunk-TH4XENKH.mjs → chunk-I4HNHPEM.mjs} +1 -1
- package/dist/chunk-JEVSFNPT.mjs +363 -0
- package/dist/chunk-JKECUAAV.mjs +2169 -0
- package/dist/chunk-KMHORI4Y.mjs +638 -0
- package/dist/chunk-KNPYBZ6B.mjs +502 -0
- package/dist/{chunk-3QUK5BPY.mjs → chunk-KOD4GBQ5.mjs} +8 -1
- package/dist/chunk-KQNZKV6E.mjs +535 -0
- package/dist/{chunk-MXYUAJEW.mjs → chunk-LLRUOIMU.mjs} +5 -2
- package/dist/{chunk-F2UI225N.mjs → chunk-LNMITJ4Z.mjs} +6 -6
- package/dist/chunk-MEM3MDV4.mjs +6463 -0
- package/dist/chunk-NY4YT7O6.mjs +84 -0
- package/dist/{chunk-A6WHBSXZ.mjs → chunk-Q6CF4C3E.mjs} +15 -13
- package/dist/chunk-RP66A52P.mjs +86 -0
- package/dist/{chunk-I2UP7YTG.mjs → chunk-SAXGWS2K.mjs} +1 -1
- package/dist/chunk-SCKQ4XSD.mjs +41 -0
- package/dist/chunk-SF44L2OD.mjs +1152 -0
- package/dist/chunk-SGT5IJVW.mjs +28 -0
- package/dist/{chunk-WEAB6HHQ.mjs → chunk-SS4JTMIV.mjs} +8 -8
- package/dist/chunk-T4MW3PD4.mjs +1456 -0
- package/dist/chunk-THATLMEX.mjs +1227 -0
- package/dist/{chunk-MLE4MVYC.mjs → chunk-TT4YI33G.mjs} +2 -2
- package/dist/chunk-TYV2SVFN.mjs +101 -0
- package/dist/chunk-VHK7WBO3.mjs +189 -0
- package/dist/chunk-VSA44UKR.mjs +391 -0
- package/dist/chunk-XL54SQJ3.mjs +42 -0
- package/dist/{chunk-5KYR6WJB.mjs → chunk-XQAHREKO.mjs} +17 -1
- package/dist/chunk-ZUCDXRFI.mjs +2011 -0
- package/dist/create-4S2EAUR3.mjs +10 -0
- package/dist/credits-NJOAOPPW.mjs +33 -0
- package/dist/{deploy-JE6TUYCL.mjs → deploy-26Y4UGZZ.mjs} +14 -14
- package/dist/dev-MOYURRWQ.mjs +401 -0
- package/dist/dev-WL43Q7VD.mjs +8 -0
- package/dist/dispatcher.mjs +131 -30
- package/dist/{doctor-HFB4IBYK.mjs → doctor-E6ETD2GJ.mjs} +12 -19
- package/dist/{domains-BZKGIDF4.mjs → domains-LOTQMVOR.mjs} +6 -6
- package/dist/engine-runtime-fetch-CIE7S7PB.mjs +10 -0
- package/dist/{env-FZNXKLGL.mjs → env-3PDFC5XF.mjs} +7 -7
- package/dist/env-MOTYOPM5.mjs +19 -0
- package/dist/explain-K5QEVWCD.mjs +251 -0
- package/dist/{inspect-X3M7WSTL.mjs → inspect-Q3E2DHYY.mjs} +3 -3
- package/dist/inspect-SJ4WBAXO.mjs +86 -0
- package/dist/introspect-7F34YN5V.mjs +9 -0
- package/dist/kernel-DEGWMHFL.mjs +662 -0
- package/dist/kernel-plugin.js +6862 -0
- package/dist/keys-W5SN3DPK.mjs +71 -0
- package/dist/lines-XGJYTQ7P.mjs +56 -0
- package/dist/{link-4NM2QMI5.mjs → link-NKUAVNM2.mjs} +6 -5
- package/dist/{lint-DDZ7HUQN.mjs → lint-G4YPQSSO.mjs} +2 -2
- package/dist/{list-2MFOQKKQ.mjs → list-TDR7K2JB.mjs} +3 -3
- package/dist/lock-EJPW2LM3.mjs +57 -0
- package/dist/{login-RLMDEWAL.mjs → login-AYLU7JU7.mjs} +10 -8
- package/dist/{logout-Z57X6HPE.mjs → logout-3M6VMHLH.mjs} +2 -2
- package/dist/{logs-36BLYJHJ.mjs → logs-4EL5SOXN.mjs} +5 -5
- package/dist/logs-4QQGG2XY.mjs +44 -0
- package/dist/mcp-4R2R6E5M.mjs +1077 -0
- package/dist/{mock-EVER34IX.mjs → mock-AK24GSP2.mjs} +2 -2
- package/dist/outdated-JEKCVLTG.mjs +58 -0
- package/dist/port-SBZ7X3EH.mjs +2408 -0
- package/dist/{projects-4XRNHJAH.mjs → projects-7JKTC5WJ.mjs} +9 -7
- package/dist/publish-QQCX47HK.mjs +20 -0
- package/dist/remix-NLLYD3BI.mjs +125 -0
- package/dist/repo-Z4G4EW3N.mjs +9 -0
- package/dist/report-M7F4ZYP7.mjs +2 -0
- package/dist/rollback-2WQ2CW7T.mjs +43 -0
- package/dist/{rollback-WCRWXSE3.mjs → rollback-ZGU32HGT.mjs} +6 -6
- package/dist/scene-HXARHUM7.mjs +324 -0
- package/dist/{status-VA326DWD.mjs → status-C3ZSLZ7Q.mjs} +5 -5
- package/dist/status-WEQOC25K.mjs +18 -0
- package/dist/test-ZPBWV4VX.mjs +233 -0
- package/dist/testers-A3LMXOJJ.mjs +49 -0
- package/dist/{unlink-H3BO3DOQ.mjs → unlink-4G4FE4RJ.mjs} +2 -2
- package/dist/{update-YYOTN7MG.mjs → update-GHKKM5FS.mjs} +4 -4
- package/dist/{whoami-CQPPARRS.mjs → whoami-3QQN6G2A.mjs} +3 -3
- package/dist/workshop-CD74D2LK.mjs +1703 -0
- package/package.json +11 -7
- package/templates/storefront-auto/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-auto/package.json +1 -1
- package/templates/storefront-bakery/package.json +1 -1
- package/templates/storefront-fashion/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-fashion/package.json +1 -1
- package/templates/storefront-grocery/package.json +1 -1
- package/templates/storefront-pharmacy/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-pharmacy/package.json +1 -1
- package/templates/storefront-restaurant/package.json +1 -1
- package/templates/storefront-retail/app/products/[slug]/page.tsx +1 -1
- package/templates/storefront-retail/package.json +1 -1
- package/templates/storefront-services/package.json +1 -1
- package/dist/chunk-ARIDYKLO.mjs +0 -6463
- package/dist/dev-V7JSUVOI.mjs +0 -8
- package/dist/explain-O5AC73EE.mjs +0 -251
- package/dist/introspect-2NGESQZE.mjs +0 -8
- package/dist/repo-X3KZ5R64.mjs +0 -9
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { RemoteError } from './chunk-SCKQ4XSD.mjs';
|
|
3
|
+
import { VERIFIED_SOURCE_STORAGE_QUOTA_BYTES, SOURCE_STORAGE_QUOTA_BYTES, UPLOAD_PART_BYTES, VERIFIED_UPLOAD_BYTES_PER_HOUR, UPLOAD_BYTES_PER_HOUR, MAX_CAPTURE_FILE_BYTES, MAX_SOURCE_FILE_BYTES, VERIFIED_MAX_SOURCE_SET_BYTES, MAX_SOURCE_SET_BYTES, sourceSetFromFiles, buildInteractive } from './chunk-CPBESPWK.mjs';
|
|
4
|
+
import { digestHex, sha256 } from './chunk-25ZPFHLF.mjs';
|
|
5
|
+
import { formatDiagnostic } from './chunk-SGT5IJVW.mjs';
|
|
6
|
+
import { ApiClient } from './chunk-CLQYPTJ2.mjs';
|
|
7
|
+
import { authConfigDir, readAuthOrNull } from './chunk-TT4YI33G.mjs';
|
|
8
|
+
import { CliError, CLI_ERROR_CODE } from './chunk-KOD4GBQ5.mjs';
|
|
9
|
+
import { generateKeyPairSync, createPrivateKey, sign, randomBytes, createPublicKey, verify } from 'crypto';
|
|
10
|
+
import { promises } from 'fs';
|
|
11
|
+
import path2 from 'path';
|
|
12
|
+
|
|
13
|
+
var SIGNING_KEY_FILE = "interactive-signing-key.json";
|
|
14
|
+
var SOURCE_SIGNATURE_CONTEXT = "cimplify.interactives.source.v1\n";
|
|
15
|
+
var KEY_FILE_MODE = 384;
|
|
16
|
+
var SIGNATURE_ALGORITHM = "ed25519";
|
|
17
|
+
var RAW_PUBLIC_KEY_BYTES = 32;
|
|
18
|
+
var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
19
|
+
function keyPath(dir) {
|
|
20
|
+
return path2.join(dir, SIGNING_KEY_FILE);
|
|
21
|
+
}
|
|
22
|
+
function rawPublicKeyOf(spkiBase64) {
|
|
23
|
+
const der = Buffer.from(spkiBase64, "base64");
|
|
24
|
+
if (der.length !== ED25519_SPKI_PREFIX.length + RAW_PUBLIC_KEY_BYTES || !der.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) {
|
|
25
|
+
throw new Error("the stored signing key is not an Ed25519 public key");
|
|
26
|
+
}
|
|
27
|
+
return der.subarray(ED25519_SPKI_PREFIX.length).toString("base64");
|
|
28
|
+
}
|
|
29
|
+
function withKeys(publicKey, privateKey, registrations) {
|
|
30
|
+
return { publicKey, privateKey, rawPublicKey: rawPublicKeyOf(publicKey), registrations };
|
|
31
|
+
}
|
|
32
|
+
function generateSigningKey() {
|
|
33
|
+
const pair = generateKeyPairSync(SIGNATURE_ALGORITHM);
|
|
34
|
+
return withKeys(
|
|
35
|
+
pair.publicKey.export({ type: "spki", format: "der" }).toString("base64"),
|
|
36
|
+
pair.privateKey.export({ type: "pkcs8", format: "der" }).toString("base64"),
|
|
37
|
+
[]
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
async function writeSigningKey(dir, key) {
|
|
41
|
+
const stored = {
|
|
42
|
+
algorithm: SIGNATURE_ALGORITHM,
|
|
43
|
+
public_key: key.publicKey,
|
|
44
|
+
private_key: key.privateKey,
|
|
45
|
+
registered: key.registrations.map((r) => ({ origin: r.origin, key_id: r.keyId }))
|
|
46
|
+
};
|
|
47
|
+
await promises.mkdir(dir, { recursive: true });
|
|
48
|
+
await promises.writeFile(keyPath(dir), `${JSON.stringify(stored, null, 2)}
|
|
49
|
+
`, { mode: KEY_FILE_MODE });
|
|
50
|
+
}
|
|
51
|
+
async function loadSigningKey(dir) {
|
|
52
|
+
try {
|
|
53
|
+
const stored = JSON.parse(await promises.readFile(keyPath(dir), "utf8"));
|
|
54
|
+
if (stored.algorithm !== SIGNATURE_ALGORITHM) return null;
|
|
55
|
+
const registrations = (stored.registered ?? []).map((r) => ({ origin: r.origin, keyId: r.key_id }));
|
|
56
|
+
return withKeys(stored.public_key, stored.private_key, registrations);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err.code === "ENOENT") return null;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function loadOrCreateSigningKey(dir) {
|
|
63
|
+
const existing = await loadSigningKey(dir);
|
|
64
|
+
if (existing !== null) return existing;
|
|
65
|
+
const key = generateSigningKey();
|
|
66
|
+
await writeSigningKey(dir, key);
|
|
67
|
+
return key;
|
|
68
|
+
}
|
|
69
|
+
async function removeSigningKey(dir) {
|
|
70
|
+
await promises.rm(keyPath(dir), { force: true });
|
|
71
|
+
}
|
|
72
|
+
function registrationFor(key, origin) {
|
|
73
|
+
return key.registrations.find((r) => r.origin === origin) ?? null;
|
|
74
|
+
}
|
|
75
|
+
function withRegistration(key, registration) {
|
|
76
|
+
return { ...key, registrations: [...key.registrations.filter((r) => r.origin !== registration.origin), registration] };
|
|
77
|
+
}
|
|
78
|
+
function sourceSignatureMessage(sourceDigest) {
|
|
79
|
+
return Buffer.concat([Buffer.from(SOURCE_SIGNATURE_CONTEXT, "ascii"), Buffer.from(sourceDigest, "hex")]);
|
|
80
|
+
}
|
|
81
|
+
function signSource(key, sourceDigest) {
|
|
82
|
+
const privateKey = createPrivateKey({ key: Buffer.from(key.privateKey, "base64"), format: "der", type: "pkcs8" });
|
|
83
|
+
return sign(null, sourceSignatureMessage(sourceDigest), privateKey).toString("base64");
|
|
84
|
+
}
|
|
85
|
+
function verifySource(rawPublicKey, sourceDigest, signature) {
|
|
86
|
+
try {
|
|
87
|
+
const der = Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(rawPublicKey, "base64")]);
|
|
88
|
+
const key = createPublicKey({ key: der, format: "der", type: "spki" });
|
|
89
|
+
return verify(null, sourceSignatureMessage(sourceDigest), key, Buffer.from(signature, "base64"));
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
var ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
95
|
+
var ID_LENGTH = 16;
|
|
96
|
+
var INTERACTIVE_PREFIX = "ixr_";
|
|
97
|
+
var REVISION_PREFIX = "rev_";
|
|
98
|
+
var KEY_PREFIX = "ixkey_";
|
|
99
|
+
var FAKE_ORIGIN = "fake:";
|
|
100
|
+
var FAKE_ACCOUNT = "acct_fake";
|
|
101
|
+
var KEYS_FILE = "signing-keys.json";
|
|
102
|
+
function randomId() {
|
|
103
|
+
const bytes = randomBytes(ID_LENGTH);
|
|
104
|
+
let out = "";
|
|
105
|
+
for (let i = 0; i < ID_LENGTH; i++) out += ID_ALPHABET[bytes[i] % ID_ALPHABET.length];
|
|
106
|
+
return out;
|
|
107
|
+
}
|
|
108
|
+
var FakeInteractivesRemote = class {
|
|
109
|
+
origin;
|
|
110
|
+
root;
|
|
111
|
+
now;
|
|
112
|
+
newId;
|
|
113
|
+
account;
|
|
114
|
+
verified;
|
|
115
|
+
storageQuotaBytes;
|
|
116
|
+
uploadsBeforeFailure;
|
|
117
|
+
partsReceived = 0;
|
|
118
|
+
publishesBegun = 0;
|
|
119
|
+
constructor(options) {
|
|
120
|
+
this.root = options.root;
|
|
121
|
+
this.origin = `${FAKE_ORIGIN}${options.root}`;
|
|
122
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
123
|
+
this.newId = options.newId ?? randomId;
|
|
124
|
+
this.account = options.account ?? FAKE_ACCOUNT;
|
|
125
|
+
this.verified = options.verified ?? false;
|
|
126
|
+
this.storageQuotaBytes = options.storageQuotaBytes ?? (this.verified ? VERIFIED_SOURCE_STORAGE_QUOTA_BYTES : SOURCE_STORAGE_QUOTA_BYTES);
|
|
127
|
+
this.uploadsBeforeFailure = options.uploadsBeforeFailure ?? null;
|
|
128
|
+
}
|
|
129
|
+
sessionPath(id) {
|
|
130
|
+
return path2.join(this.root, "sessions", `${id}.json`);
|
|
131
|
+
}
|
|
132
|
+
async session(id) {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.parse(await promises.readFile(this.sessionPath(id), "utf8"));
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err.code === "ENOENT") return null;
|
|
137
|
+
throw err;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async sessions() {
|
|
141
|
+
let names;
|
|
142
|
+
try {
|
|
143
|
+
names = await promises.readdir(path2.join(this.root, "sessions"));
|
|
144
|
+
} catch (err) {
|
|
145
|
+
if (err.code === "ENOENT") return [];
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const name of names) {
|
|
150
|
+
const found = await this.session(path2.basename(name, ".json"));
|
|
151
|
+
if (found !== null) out.push(found);
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
uploadPath(digest) {
|
|
156
|
+
return path2.join(this.root, "uploads", `${digestHex(digest)}.json`);
|
|
157
|
+
}
|
|
158
|
+
partPath(digest, part) {
|
|
159
|
+
return path2.join(this.root, "uploads", digestHex(digest), String(part));
|
|
160
|
+
}
|
|
161
|
+
async upload(digest) {
|
|
162
|
+
try {
|
|
163
|
+
return JSON.parse(await promises.readFile(this.uploadPath(digest), "utf8"));
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err.code === "ENOENT") return null;
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async saveUpload(digest, upload) {
|
|
170
|
+
await promises.mkdir(path2.dirname(this.uploadPath(digest)), { recursive: true });
|
|
171
|
+
await promises.writeFile(this.uploadPath(digest), JSON.stringify(upload));
|
|
172
|
+
}
|
|
173
|
+
async usedBytes() {
|
|
174
|
+
let used = 0;
|
|
175
|
+
for (const dir of ["blobs", "uploads"]) {
|
|
176
|
+
let names;
|
|
177
|
+
try {
|
|
178
|
+
names = await promises.readdir(path2.join(this.root, dir));
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err.code === "ENOENT") continue;
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
183
|
+
for (const name of names) {
|
|
184
|
+
if (dir === "blobs") used += (await promises.stat(path2.join(this.root, dir, name))).size;
|
|
185
|
+
else if (name.endsWith(".json")) used += JSON.parse(await promises.readFile(path2.join(this.root, dir, name), "utf8")).size;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return used;
|
|
189
|
+
}
|
|
190
|
+
async reserve(bytes) {
|
|
191
|
+
const used = await this.usedBytes();
|
|
192
|
+
if (used + bytes > this.storageQuotaBytes) {
|
|
193
|
+
throw new RemoteError("quota", `the creator's source storage quota is ${this.storageQuotaBytes} bytes; ${used} are in use and this upload needs ${bytes} more`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async awaitedSize(digest) {
|
|
197
|
+
for (const session of await this.sessions()) {
|
|
198
|
+
const file = session.files.find((f) => f.digest === digest);
|
|
199
|
+
if (file !== void 0) return file.size;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
async sourceLimits() {
|
|
204
|
+
return {
|
|
205
|
+
verified: this.verified,
|
|
206
|
+
sourceSetBytes: this.verified ? VERIFIED_MAX_SOURCE_SET_BYTES : MAX_SOURCE_SET_BYTES,
|
|
207
|
+
sourceFileBytes: MAX_SOURCE_FILE_BYTES,
|
|
208
|
+
captureFileBytes: MAX_CAPTURE_FILE_BYTES,
|
|
209
|
+
storageQuotaBytes: this.storageQuotaBytes,
|
|
210
|
+
storageUsedBytes: await this.usedBytes(),
|
|
211
|
+
uploadBytesPerHour: this.verified ? VERIFIED_UPLOAD_BYTES_PER_HOUR : UPLOAD_BYTES_PER_HOUR,
|
|
212
|
+
partBytes: UPLOAD_PART_BYTES
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
async pendingPublish(sessionId) {
|
|
216
|
+
const session = await this.session(sessionId);
|
|
217
|
+
if (session === null) return null;
|
|
218
|
+
const missing = [];
|
|
219
|
+
for (const file of session.files) if (!missing.includes(file.digest) && !await this.hasBlob(file.digest)) missing.push(file.digest);
|
|
220
|
+
return { receiving: true, missing };
|
|
221
|
+
}
|
|
222
|
+
async openUpload(digest, size, parts) {
|
|
223
|
+
const count = Math.ceil(size / UPLOAD_PART_BYTES);
|
|
224
|
+
if (await this.hasBlob(digest)) return { partBytes: UPLOAD_PART_BYTES, parts: count, received: [], stored: true };
|
|
225
|
+
if (await this.awaitedSize(digest) !== size) throw new RemoteError("not_found", `no revision awaits ${digest} at ${size} bytes`);
|
|
226
|
+
if (parts.length !== count) throw new RemoteError("invalid", `name the SHA-256 of all ${count} parts`);
|
|
227
|
+
const open = await this.upload(digest);
|
|
228
|
+
if (open !== null) {
|
|
229
|
+
if (open.size !== size || open.parts.some((p, i) => p !== parts[i])) throw new RemoteError("invalid", "the parts differ from the upload already open for this file");
|
|
230
|
+
return { partBytes: UPLOAD_PART_BYTES, parts: count, received: open.received, stored: false };
|
|
231
|
+
}
|
|
232
|
+
await this.reserve(size);
|
|
233
|
+
await this.saveUpload(digest, { size, parts: [...parts], received: [] });
|
|
234
|
+
return { partBytes: UPLOAD_PART_BYTES, parts: count, received: [], stored: false };
|
|
235
|
+
}
|
|
236
|
+
async uploadPart(digest, part, bytes) {
|
|
237
|
+
const open = await this.upload(digest);
|
|
238
|
+
if (open === null) throw new RemoteError("not_found", `no open upload for ${digest}`);
|
|
239
|
+
if (open.received.includes(part)) return;
|
|
240
|
+
if (part < 1 || part > open.parts.length || sha256(bytes) !== open.parts[part - 1]) throw new RemoteError("invalid", `part ${part} does not hash to its declared digest`);
|
|
241
|
+
this.spendUpload();
|
|
242
|
+
await promises.mkdir(path2.dirname(this.partPath(digest, part)), { recursive: true });
|
|
243
|
+
await promises.writeFile(this.partPath(digest, part), bytes);
|
|
244
|
+
await this.serialised(digest, async () => {
|
|
245
|
+
const latest = await this.upload(digest);
|
|
246
|
+
if (!latest.received.includes(part)) latest.received.push(part);
|
|
247
|
+
latest.received.sort((a, b) => a - b);
|
|
248
|
+
await this.saveUpload(digest, latest);
|
|
249
|
+
});
|
|
250
|
+
this.partsReceived++;
|
|
251
|
+
}
|
|
252
|
+
spendUpload() {
|
|
253
|
+
if (this.uploadsBeforeFailure === null) return;
|
|
254
|
+
if (this.uploadsBeforeFailure === 0) throw new Error("the connection dropped");
|
|
255
|
+
this.uploadsBeforeFailure--;
|
|
256
|
+
}
|
|
257
|
+
locks = /* @__PURE__ */ new Map();
|
|
258
|
+
serialised(key, work) {
|
|
259
|
+
const next = (this.locks.get(key) ?? Promise.resolve()).then(work);
|
|
260
|
+
this.locks.set(
|
|
261
|
+
key,
|
|
262
|
+
next.catch(() => void 0)
|
|
263
|
+
);
|
|
264
|
+
return next;
|
|
265
|
+
}
|
|
266
|
+
async completeUpload(digest) {
|
|
267
|
+
const open = await this.upload(digest);
|
|
268
|
+
if (open === null) throw new RemoteError("not_found", `no open upload for ${digest}`);
|
|
269
|
+
if (open.received.length !== open.parts.length) throw new RemoteError("invalid", "parts have not all arrived");
|
|
270
|
+
const pieces = [];
|
|
271
|
+
for (let part = 1; part <= open.parts.length; part++) pieces.push(await promises.readFile(this.partPath(digest, part)));
|
|
272
|
+
const whole = Buffer.concat(pieces);
|
|
273
|
+
await promises.rm(path2.join(this.root, "uploads", digestHex(digest)), { recursive: true, force: true });
|
|
274
|
+
await promises.rm(this.uploadPath(digest), { force: true });
|
|
275
|
+
if (sha256(whole) !== digest || whole.length !== open.size) throw new RemoteError("invalid", "the assembled file does not hash to its digest");
|
|
276
|
+
await promises.mkdir(path2.join(this.root, "blobs"), { recursive: true });
|
|
277
|
+
await promises.writeFile(this.blobPath(digest), whole);
|
|
278
|
+
}
|
|
279
|
+
statePath(id) {
|
|
280
|
+
return path2.join(this.root, "interactives", `${id}.json`);
|
|
281
|
+
}
|
|
282
|
+
blobPath(digest) {
|
|
283
|
+
return path2.join(this.root, "blobs", digestHex(digest));
|
|
284
|
+
}
|
|
285
|
+
async load(id) {
|
|
286
|
+
try {
|
|
287
|
+
return JSON.parse(await promises.readFile(this.statePath(id), "utf8"));
|
|
288
|
+
} catch (err) {
|
|
289
|
+
if (err.code === "ENOENT") throw new RemoteError("not_found", `no Interactive ${id}`);
|
|
290
|
+
throw err;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
async save(state) {
|
|
294
|
+
await promises.mkdir(path2.dirname(this.statePath(state.interactive_id)), { recursive: true });
|
|
295
|
+
await promises.writeFile(this.statePath(state.interactive_id), `${JSON.stringify(state, null, 2)}
|
|
296
|
+
`);
|
|
297
|
+
}
|
|
298
|
+
async loadKeys() {
|
|
299
|
+
try {
|
|
300
|
+
return JSON.parse(await promises.readFile(path2.join(this.root, KEYS_FILE), "utf8"));
|
|
301
|
+
} catch (err) {
|
|
302
|
+
if (err.code === "ENOENT") return [];
|
|
303
|
+
throw err;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async saveKeys(keys) {
|
|
307
|
+
await promises.mkdir(this.root, { recursive: true });
|
|
308
|
+
await promises.writeFile(path2.join(this.root, KEYS_FILE), `${JSON.stringify(keys, null, 2)}
|
|
309
|
+
`);
|
|
310
|
+
}
|
|
311
|
+
async hasBlob(digest) {
|
|
312
|
+
try {
|
|
313
|
+
await promises.access(this.blobPath(digest));
|
|
314
|
+
return true;
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
toStatus(state) {
|
|
320
|
+
return {
|
|
321
|
+
interactiveId: state.interactive_id,
|
|
322
|
+
name: state.name,
|
|
323
|
+
live: state.live,
|
|
324
|
+
test: state.test,
|
|
325
|
+
revisions: state.revisions.slice().reverse()
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async createInteractive(request) {
|
|
329
|
+
const id = INTERACTIVE_PREFIX + this.newId();
|
|
330
|
+
await this.save({ interactive_id: id, name: request.name, next_number: 1, live: null, test: null, revisions: [], logs: [], testers: [] });
|
|
331
|
+
return id;
|
|
332
|
+
}
|
|
333
|
+
async registerSigningKey(publicKey, label) {
|
|
334
|
+
const keys = await this.loadKeys();
|
|
335
|
+
const active = keys.find((k) => k.publicKey === publicKey && k.revokedAt === null);
|
|
336
|
+
if (active) return active;
|
|
337
|
+
const record2 = { keyId: KEY_PREFIX + this.newId(), publicKey, label, createdAt: this.now().toISOString(), revokedAt: null };
|
|
338
|
+
await this.saveKeys([...keys, record2]);
|
|
339
|
+
return record2;
|
|
340
|
+
}
|
|
341
|
+
async listSigningKeys() {
|
|
342
|
+
return (await this.loadKeys()).filter((k) => k.revokedAt === null);
|
|
343
|
+
}
|
|
344
|
+
async rotateSigningKey(keyId, publicKey, label) {
|
|
345
|
+
await this.revokeSigningKey(keyId);
|
|
346
|
+
return this.registerSigningKey(publicKey, label);
|
|
347
|
+
}
|
|
348
|
+
async revokeSigningKey(keyId) {
|
|
349
|
+
const keys = await this.loadKeys();
|
|
350
|
+
if (!keys.some((k) => k.keyId === keyId && k.revokedAt === null)) throw new RemoteError("not_found", `no signing key ${keyId}`);
|
|
351
|
+
const at = this.now().toISOString();
|
|
352
|
+
await this.saveKeys(keys.map((k) => k.keyId === keyId ? { ...k, revokedAt: at } : k));
|
|
353
|
+
}
|
|
354
|
+
async beginPublish(request) {
|
|
355
|
+
await this.load(request.interactiveId);
|
|
356
|
+
const keys = await this.listSigningKeys();
|
|
357
|
+
if (!keys.some((k) => verifySource(k.publicKey, request.sourceDigest, request.signature))) {
|
|
358
|
+
throw new RemoteError("invalid", "the source set signature does not verify against a registered signing key");
|
|
359
|
+
}
|
|
360
|
+
const missing = [];
|
|
361
|
+
const seen = /* @__PURE__ */ new Set();
|
|
362
|
+
for (const file of request.files) {
|
|
363
|
+
if (seen.has(file.digest)) continue;
|
|
364
|
+
seen.add(file.digest);
|
|
365
|
+
if (!await this.hasBlob(file.digest)) missing.push(file.digest);
|
|
366
|
+
}
|
|
367
|
+
const unique = new Map(request.files.map((f) => [f.digest, f.size]));
|
|
368
|
+
let missingBytes = 0;
|
|
369
|
+
for (const digest of missing) missingBytes += unique.get(digest) ?? 0;
|
|
370
|
+
const limit = this.verified ? VERIFIED_MAX_SOURCE_SET_BYTES : MAX_SOURCE_SET_BYTES;
|
|
371
|
+
if (request.files.reduce((n, f) => n + f.size, 0) > limit) {
|
|
372
|
+
throw new RemoteError("invalid", this.verified ? "files.size exceed the 2 GiB source set limit together" : "files.size exceed 1 GiB together; verify your creator account to publish source sets up to 2 GiB");
|
|
373
|
+
}
|
|
374
|
+
await this.reserve(missingBytes);
|
|
375
|
+
const sessionId = this.newId();
|
|
376
|
+
await promises.mkdir(path2.dirname(this.sessionPath(sessionId)), { recursive: true });
|
|
377
|
+
await promises.writeFile(this.sessionPath(sessionId), JSON.stringify(request));
|
|
378
|
+
this.publishesBegun++;
|
|
379
|
+
return { sessionId, missing };
|
|
380
|
+
}
|
|
381
|
+
async uploadFile(sessionId, digest, bytes) {
|
|
382
|
+
if (await this.session(sessionId) === null) throw new RemoteError("not_found", `no upload session ${sessionId}`);
|
|
383
|
+
if (sha256(bytes) !== digest) throw new RemoteError("invalid", `uploaded bytes do not match ${digest}`);
|
|
384
|
+
if (await this.hasBlob(digest)) return;
|
|
385
|
+
this.spendUpload();
|
|
386
|
+
await this.reserve(bytes.length);
|
|
387
|
+
await promises.mkdir(path2.join(this.root, "blobs"), { recursive: true });
|
|
388
|
+
await promises.writeFile(this.blobPath(digest), bytes);
|
|
389
|
+
}
|
|
390
|
+
async commitPublish(sessionId) {
|
|
391
|
+
const session = await this.session(sessionId);
|
|
392
|
+
if (!session) throw new RemoteError("not_found", `no upload session ${sessionId}`);
|
|
393
|
+
await promises.rm(this.sessionPath(sessionId), { force: true });
|
|
394
|
+
const state = await this.load(session.interactiveId);
|
|
395
|
+
const files = [];
|
|
396
|
+
for (const file of session.files) {
|
|
397
|
+
if (!await this.hasBlob(file.digest)) throw new RemoteError("invalid", `${file.path} was never uploaded`);
|
|
398
|
+
files.push({ path: file.path, bytes: await promises.readFile(this.blobPath(file.digest)) });
|
|
399
|
+
}
|
|
400
|
+
const set = sourceSetFromFiles(files);
|
|
401
|
+
if (set.digest !== session.sourceDigest) {
|
|
402
|
+
throw new RemoteError("invalid", "the uploaded files do not reproduce the signed source digest");
|
|
403
|
+
}
|
|
404
|
+
const built = buildInteractive(set);
|
|
405
|
+
const at = this.now().toISOString();
|
|
406
|
+
const number = state.next_number;
|
|
407
|
+
const revision = {
|
|
408
|
+
revision: `${REVISION_PREFIX}${number}`,
|
|
409
|
+
number,
|
|
410
|
+
channel: session.channel,
|
|
411
|
+
state: built.ok ? "admitted" : "refused",
|
|
412
|
+
sourceDigest: set.digest,
|
|
413
|
+
packageDigest: built.packageDigest,
|
|
414
|
+
createdAt: at
|
|
415
|
+
};
|
|
416
|
+
const logs = [{ at, revision: revision.revision, level: "info", message: `build started for ${set.files.length} files` }];
|
|
417
|
+
for (const d of built.diagnostics) {
|
|
418
|
+
logs.push({ at, revision: revision.revision, level: d.severity === "error" ? "error" : "warning", message: formatDiagnostic(d) });
|
|
419
|
+
}
|
|
420
|
+
logs.push({
|
|
421
|
+
at,
|
|
422
|
+
revision: revision.revision,
|
|
423
|
+
level: built.ok ? "info" : "error",
|
|
424
|
+
message: built.ok ? `admitted package ${built.packageDigest} to the ${revision.channel} channel` : "admission refused the revision"
|
|
425
|
+
});
|
|
426
|
+
state.next_number = number + 1;
|
|
427
|
+
state.revisions.push(revision);
|
|
428
|
+
for (const l of logs) state.logs.push(l);
|
|
429
|
+
if (built.ok) state[revision.channel] = revision.revision;
|
|
430
|
+
await this.save(state);
|
|
431
|
+
return { revision, logs };
|
|
432
|
+
}
|
|
433
|
+
async status(interactiveId) {
|
|
434
|
+
return this.toStatus(await this.load(interactiveId));
|
|
435
|
+
}
|
|
436
|
+
async logs(interactiveId, query) {
|
|
437
|
+
const state = await this.load(interactiveId);
|
|
438
|
+
const filtered = query.revision === null ? state.logs : state.logs.filter((l) => l.revision === query.revision);
|
|
439
|
+
return filtered.slice(Math.max(0, filtered.length - query.limit));
|
|
440
|
+
}
|
|
441
|
+
async rollback(interactiveId, revision, channel) {
|
|
442
|
+
const state = await this.load(interactiveId);
|
|
443
|
+
const target = state.revisions.find((r) => r.revision === revision);
|
|
444
|
+
if (!target) throw new RemoteError("not_found", `no revision ${revision}`);
|
|
445
|
+
if (target.state !== "admitted") throw new RemoteError("conflict", `revision ${revision} was not admitted and cannot be released`);
|
|
446
|
+
state[channel] = revision;
|
|
447
|
+
state.logs.push({ at: this.now().toISOString(), revision, level: "info", message: `${channel} channel now points at ${revision}` });
|
|
448
|
+
await this.save(state);
|
|
449
|
+
return this.toStatus(state);
|
|
450
|
+
}
|
|
451
|
+
async testers(interactiveId) {
|
|
452
|
+
return (await this.load(interactiveId)).testers;
|
|
453
|
+
}
|
|
454
|
+
async addTester(interactiveId, accountId) {
|
|
455
|
+
const state = await this.load(interactiveId);
|
|
456
|
+
const existing = state.testers.find((t) => t.accountId === accountId);
|
|
457
|
+
if (existing) return existing;
|
|
458
|
+
const tester2 = { accountId, addedBy: this.account, addedAt: this.now().toISOString() };
|
|
459
|
+
state.testers.push(tester2);
|
|
460
|
+
await this.save(state);
|
|
461
|
+
return tester2;
|
|
462
|
+
}
|
|
463
|
+
async removeTester(interactiveId, accountId) {
|
|
464
|
+
const state = await this.load(interactiveId);
|
|
465
|
+
state.testers = state.testers.filter((t) => t.accountId !== accountId);
|
|
466
|
+
await this.save(state);
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
// src/interactives/remote/http.ts
|
|
471
|
+
var POLL_INTERVAL_MS = 1e3;
|
|
472
|
+
var PUBLISH_DEADLINE_MS = 15 * 60 * 1e3;
|
|
473
|
+
var STATUS_REVISIONS = 20;
|
|
474
|
+
var BASE = "/v1/interactives";
|
|
475
|
+
var RECEIVING = "receiving";
|
|
476
|
+
var AWAITING_REVIEW = "awaiting_review";
|
|
477
|
+
var ADMITTED = "admitted";
|
|
478
|
+
var REFUSED = "refused";
|
|
479
|
+
var TERMINAL_STAGES = /* @__PURE__ */ new Set([ADMITTED, REFUSED, AWAITING_REVIEW]);
|
|
480
|
+
function pause(ms) {
|
|
481
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
482
|
+
}
|
|
483
|
+
function stateOf(stage) {
|
|
484
|
+
if (stage === ADMITTED) return "admitted";
|
|
485
|
+
if (stage === REFUSED) return "refused";
|
|
486
|
+
if (stage === AWAITING_REVIEW) return "in_review";
|
|
487
|
+
return "pending";
|
|
488
|
+
}
|
|
489
|
+
function record(body) {
|
|
490
|
+
return {
|
|
491
|
+
revision: body.id,
|
|
492
|
+
number: body.number,
|
|
493
|
+
channel: body.target_channel,
|
|
494
|
+
state: stateOf(body.stage),
|
|
495
|
+
sourceDigest: body.source_digest,
|
|
496
|
+
packageDigest: body.package_digest,
|
|
497
|
+
createdAt: body.created_at
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function refusalText(refusal) {
|
|
501
|
+
return refusal.detail === null ? `refused: ${refusal.reason}` : `refused: ${refusal.reason}: ${refusal.detail}`;
|
|
502
|
+
}
|
|
503
|
+
function entry(at, revision, level, message) {
|
|
504
|
+
return { at, revision, level, message };
|
|
505
|
+
}
|
|
506
|
+
function revisionLogs(body) {
|
|
507
|
+
const out = [entry(body.created_at, body.id, "info", `revision ${body.number} received for the ${body.target_channel} channel`)];
|
|
508
|
+
if (body.admitted_at !== null) out.push(entry(body.admitted_at, body.id, "info", `admitted package ${body.package_digest ?? ""}`));
|
|
509
|
+
if (body.refusal !== null) out.push(entry(body.created_at, body.id, "error", refusalText(body.refusal)));
|
|
510
|
+
if (body.stage === AWAITING_REVIEW) out.push(entry(body.created_at, body.id, "warning", "waiting for review by Cimplify"));
|
|
511
|
+
return out;
|
|
512
|
+
}
|
|
513
|
+
function signingKey(body) {
|
|
514
|
+
return { keyId: body.key_id, publicKey: body.public_key, label: body.label, createdAt: body.created_at, revokedAt: body.revoked_at };
|
|
515
|
+
}
|
|
516
|
+
function tester(body) {
|
|
517
|
+
return { accountId: body.account_id, addedBy: body.added_by, addedAt: body.added_at };
|
|
518
|
+
}
|
|
519
|
+
function segment(value) {
|
|
520
|
+
return encodeURIComponent(value);
|
|
521
|
+
}
|
|
522
|
+
var HttpInteractivesRemote = class {
|
|
523
|
+
constructor(client, origin, options = {}) {
|
|
524
|
+
this.client = client;
|
|
525
|
+
this.origin = origin;
|
|
526
|
+
this.pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
527
|
+
this.deadlineMs = options.deadlineMs ?? PUBLISH_DEADLINE_MS;
|
|
528
|
+
this.sleep = options.sleep ?? pause;
|
|
529
|
+
this.now = options.now ?? Date.now;
|
|
530
|
+
}
|
|
531
|
+
client;
|
|
532
|
+
origin;
|
|
533
|
+
pollIntervalMs;
|
|
534
|
+
deadlineMs;
|
|
535
|
+
sleep;
|
|
536
|
+
now;
|
|
537
|
+
async createInteractive(request) {
|
|
538
|
+
const created = await this.client.post(BASE, {
|
|
539
|
+
name: request.name,
|
|
540
|
+
description: request.description,
|
|
541
|
+
category: request.category,
|
|
542
|
+
remix_allowed: null
|
|
543
|
+
});
|
|
544
|
+
return created.id;
|
|
545
|
+
}
|
|
546
|
+
async registerSigningKey(publicKey, label) {
|
|
547
|
+
return signingKey(await this.client.post(`${BASE}/signing-keys`, { public_key: publicKey, label }));
|
|
548
|
+
}
|
|
549
|
+
async listSigningKeys() {
|
|
550
|
+
return (await this.client.get(`${BASE}/signing-keys`)).map(signingKey);
|
|
551
|
+
}
|
|
552
|
+
async rotateSigningKey(keyId, publicKey, label) {
|
|
553
|
+
return signingKey(await this.client.post(`${BASE}/signing-keys/${segment(keyId)}/rotate`, { public_key: publicKey, label }));
|
|
554
|
+
}
|
|
555
|
+
async revokeSigningKey(keyId) {
|
|
556
|
+
await this.client.delete(`${BASE}/signing-keys/${segment(keyId)}`);
|
|
557
|
+
}
|
|
558
|
+
async beginPublish(request) {
|
|
559
|
+
const receipt = await this.client.post(`${BASE}/publish`, {
|
|
560
|
+
interactive_id: request.interactiveId,
|
|
561
|
+
channel: request.channel,
|
|
562
|
+
files: request.files.map((f) => ({ path: f.path, digest: f.digest, size: f.size })),
|
|
563
|
+
signature: request.signature
|
|
564
|
+
});
|
|
565
|
+
return { sessionId: receipt.revision_id, missing: receipt.missing };
|
|
566
|
+
}
|
|
567
|
+
async uploadFile(_sessionId, digest, bytes) {
|
|
568
|
+
await this.client.putBytes(`${BASE}/sources/${segment(digest)}`, bytes);
|
|
569
|
+
}
|
|
570
|
+
async pendingPublish(sessionId) {
|
|
571
|
+
try {
|
|
572
|
+
const receipt = await this.client.post(`${BASE}/revisions/${segment(sessionId)}/complete`);
|
|
573
|
+
return { receiving: receipt.stage === RECEIVING, missing: receipt.missing };
|
|
574
|
+
} catch (err) {
|
|
575
|
+
if (err instanceof CliError && err.code === CLI_ERROR_CODE.NOT_FOUND) return null;
|
|
576
|
+
throw err;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
async sourceLimits() {
|
|
580
|
+
try {
|
|
581
|
+
const body = await this.client.get(`${BASE}/creator/source-limits`);
|
|
582
|
+
return {
|
|
583
|
+
verified: body.verified,
|
|
584
|
+
sourceSetBytes: body.source_set_bytes,
|
|
585
|
+
sourceFileBytes: body.source_file_bytes,
|
|
586
|
+
captureFileBytes: body.capture_file_bytes,
|
|
587
|
+
storageQuotaBytes: body.storage_quota_bytes,
|
|
588
|
+
storageUsedBytes: body.storage_used_bytes,
|
|
589
|
+
uploadBytesPerHour: body.upload_bytes_per_hour,
|
|
590
|
+
partBytes: body.part_bytes
|
|
591
|
+
};
|
|
592
|
+
} catch (err) {
|
|
593
|
+
if (err instanceof CliError && err.code === CLI_ERROR_CODE.NOT_FOUND) return null;
|
|
594
|
+
throw err;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async openUpload(digest, size, parts) {
|
|
598
|
+
const body = await this.client.post(`${BASE}/sources/${segment(digest)}/upload`, { size, parts });
|
|
599
|
+
return { partBytes: body.part_bytes, parts: body.parts, received: body.received, stored: body.stored };
|
|
600
|
+
}
|
|
601
|
+
async uploadPart(digest, part, bytes) {
|
|
602
|
+
await this.client.putBytes(`${BASE}/sources/${segment(digest)}/upload/parts/${part}`, bytes);
|
|
603
|
+
}
|
|
604
|
+
async completeUpload(digest) {
|
|
605
|
+
await this.client.post(`${BASE}/sources/${segment(digest)}/upload/complete`);
|
|
606
|
+
}
|
|
607
|
+
async commitPublish(sessionId) {
|
|
608
|
+
const receipt = await this.client.post(`${BASE}/revisions/${segment(sessionId)}/complete`);
|
|
609
|
+
if (receipt.stage === RECEIVING) {
|
|
610
|
+
throw new RemoteError("invalid", `${receipt.missing.length} files never arrived: ${receipt.missing.join(", ")}`);
|
|
611
|
+
}
|
|
612
|
+
const deadline = this.now() + this.deadlineMs;
|
|
613
|
+
const logs = [];
|
|
614
|
+
let seen = null;
|
|
615
|
+
for (; ; ) {
|
|
616
|
+
const body = await this.client.get(`${BASE}/revisions/${segment(sessionId)}`);
|
|
617
|
+
if (body.stage !== seen) {
|
|
618
|
+
seen = body.stage;
|
|
619
|
+
logs.push(entry(new Date(this.now()).toISOString(), body.id, "info", `stage ${body.stage}`));
|
|
620
|
+
}
|
|
621
|
+
if (TERMINAL_STAGES.has(body.stage)) {
|
|
622
|
+
for (const line of revisionLogs(body)) if (line.level !== "info") logs.push(line);
|
|
623
|
+
return { revision: record(body), logs };
|
|
624
|
+
}
|
|
625
|
+
if (this.now() >= deadline) {
|
|
626
|
+
throw new CliError(CLI_ERROR_CODE.TIMEOUT, `revision ${body.id} is still at ${body.stage}; follow it with \`cimplify status\``);
|
|
627
|
+
}
|
|
628
|
+
await this.sleep(this.pollIntervalMs);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async revisionBodies(interactiveId, limit) {
|
|
632
|
+
return this.client.get(`${BASE}/${segment(interactiveId)}/revisions`, { query: { limit } });
|
|
633
|
+
}
|
|
634
|
+
async status(interactiveId) {
|
|
635
|
+
const [interactive, revisions] = await Promise.all([
|
|
636
|
+
this.client.get(`${BASE}/${segment(interactiveId)}`),
|
|
637
|
+
this.revisionBodies(interactiveId, STATUS_REVISIONS)
|
|
638
|
+
]);
|
|
639
|
+
return {
|
|
640
|
+
interactiveId: interactive.id,
|
|
641
|
+
name: interactive.name,
|
|
642
|
+
live: interactive.releases.live,
|
|
643
|
+
test: interactive.releases.test,
|
|
644
|
+
revisions: revisions.map(record)
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
async logs(interactiveId, query) {
|
|
648
|
+
const revisions = await this.revisionBodies(interactiveId, STATUS_REVISIONS);
|
|
649
|
+
const lines = [];
|
|
650
|
+
for (let i = revisions.length - 1; i >= 0; i--) {
|
|
651
|
+
if (query.revision === null || revisions[i].id === query.revision) for (const line of revisionLogs(revisions[i])) lines.push(line);
|
|
652
|
+
}
|
|
653
|
+
return lines.slice(Math.max(0, lines.length - query.limit));
|
|
654
|
+
}
|
|
655
|
+
async rollback(interactiveId, revision, channel) {
|
|
656
|
+
await this.client.put(`${BASE}/${segment(interactiveId)}/channels/${channel}`, { revision_id: revision });
|
|
657
|
+
return this.status(interactiveId);
|
|
658
|
+
}
|
|
659
|
+
async testers(interactiveId) {
|
|
660
|
+
return (await this.client.get(`${BASE}/${segment(interactiveId)}/testers`)).map(tester);
|
|
661
|
+
}
|
|
662
|
+
async addTester(interactiveId, accountId) {
|
|
663
|
+
return tester(await this.client.put(`${BASE}/${segment(interactiveId)}/testers/${segment(accountId)}`));
|
|
664
|
+
}
|
|
665
|
+
async removeTester(interactiveId, accountId) {
|
|
666
|
+
await this.client.delete(`${BASE}/${segment(interactiveId)}/testers/${segment(accountId)}`);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/interactives/remote/select.ts
|
|
671
|
+
var REMOTE_ENV = "CIMPLIFY_INTERACTIVES_REMOTE";
|
|
672
|
+
var FAKE_ROOT_ENV = "CIMPLIFY_INTERACTIVES_FAKE_ROOT";
|
|
673
|
+
var FAKE_REMOTE = "fake";
|
|
674
|
+
var HTTP_REMOTE = "http";
|
|
675
|
+
var FAKE_DIR = "interactives-fake";
|
|
676
|
+
function signingKeyDir() {
|
|
677
|
+
return authConfigDir();
|
|
678
|
+
}
|
|
679
|
+
async function remoteFromEnv() {
|
|
680
|
+
const kind = process.env[REMOTE_ENV] ?? HTTP_REMOTE;
|
|
681
|
+
if (kind === FAKE_REMOTE) {
|
|
682
|
+
return new FakeInteractivesRemote({ root: process.env[FAKE_ROOT_ENV] ?? path2.join(authConfigDir(), FAKE_DIR) });
|
|
683
|
+
}
|
|
684
|
+
if (kind !== HTTP_REMOTE) {
|
|
685
|
+
throw new CliError(CLI_ERROR_CODE.INVALID_INPUT, `${REMOTE_ENV}=${kind} is not a known remote; expected ${HTTP_REMOTE} or ${FAKE_REMOTE}`);
|
|
686
|
+
}
|
|
687
|
+
const auth = await readAuthOrNull();
|
|
688
|
+
if (auth === null) {
|
|
689
|
+
throw new CliError(CLI_ERROR_CODE.NOT_LOGGED_IN, "publishing Interactives needs a signed-in account", {
|
|
690
|
+
remediation: `Run \`cimplify login\`, or set ${REMOTE_ENV}=${FAKE_REMOTE} to use the local fake publishing service.`
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
return new HttpInteractivesRemote(ApiClient.fromAuth(auth), auth.apiBaseUrl);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export { generateSigningKey, loadOrCreateSigningKey, loadSigningKey, registrationFor, remoteFromEnv, removeSigningKey, signSource, signingKeyDir, withRegistration, writeSigningKey };
|