@wirecat/zoom-cli 0.21.1 → 0.21.3
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/api.d.ts +1 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +18 -7
- package/dist/api.js.map +1 -1
- package/dist/bin/main.d.ts +2 -0
- package/dist/bin/main.d.ts.map +1 -0
- package/dist/bin/main.js +138 -0
- package/dist/bin/main.js.map +1 -0
- package/dist/bin/zm.js +4 -126
- package/dist/bin/zm.js.map +1 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +2 -0
- package/dist/commands.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -4
- package/dist/config.js.map +1 -1
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +3 -2
- package/dist/execution.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +17 -1
- package/dist/program.js.map +1 -1
- package/dist/recording-archive.d.ts.map +1 -1
- package/dist/recording-archive.js +15 -22
- package/dist/recording-archive.js.map +1 -1
- package/dist/recording-partial-store.d.ts +7 -18
- package/dist/recording-partial-store.d.ts.map +1 -1
- package/dist/recording-partial-store.js +19 -213
- package/dist/recording-partial-store.js.map +1 -1
- package/dist/recording-promotion-store.d.ts +3 -2
- package/dist/recording-promotion-store.d.ts.map +1 -1
- package/dist/recording-promotion-store.js +53 -82
- package/dist/recording-promotion-store.js.map +1 -1
- package/dist/run-log.d.ts +17 -0
- package/dist/run-log.d.ts.map +1 -0
- package/dist/run-log.js +50 -0
- package/dist/run-log.js.map +1 -0
- package/dist/tokens.d.ts +5 -1
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +33 -4
- package/dist/tokens.js.map +1 -1
- package/package.json +3 -3
- package/skills/zoom-cli/SKILL.md +4 -2
|
@@ -1,225 +1,31 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { constants } from "node:fs";
|
|
3
|
-
import { lstat, open, rename, unlink } from "node:fs/promises";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { CliError } from "@wirecat/cli-base";
|
|
6
|
-
import { openOrdinaryFile, syncDirectory } from "@wirecat/cli-core/files";
|
|
1
|
+
import { NativeRecordingPartialStore } from "@wirecat/cli-core/persistence/native";
|
|
7
2
|
import { checkCancelled } from "./errors.js";
|
|
8
|
-
const
|
|
9
|
-
const identity = (locator) => JSON.stringify([locator.provider, locator.account, locator.occurrenceId, locator.assetId]);
|
|
10
|
-
const missing = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
11
|
-
const fingerprint = async (file, signal, length) => {
|
|
3
|
+
const operation = async (run, signal) => {
|
|
12
4
|
checkCancelled(signal);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
try {
|
|
6
|
+
return await run();
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
17
9
|
checkCancelled(signal);
|
|
18
|
-
|
|
19
|
-
if (wanted === 0)
|
|
20
|
-
break;
|
|
21
|
-
const count = (await file.read(buffer, 0, wanted, size)).bytesRead;
|
|
22
|
-
if (count === 0)
|
|
23
|
-
break;
|
|
24
|
-
size += count;
|
|
25
|
-
hash.update(buffer.subarray(0, count));
|
|
10
|
+
throw error;
|
|
26
11
|
}
|
|
27
|
-
return { offset: size, sha256: hash.copy().digest("hex"), hash };
|
|
28
12
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
root
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
persistent;
|
|
35
|
-
maxBytes;
|
|
36
|
-
path;
|
|
37
|
-
completed = null;
|
|
38
|
-
metadataPath;
|
|
39
|
-
constructor(root, locator, signal, persistent = true, maxBytes = 20 * 1024 ** 3) {
|
|
40
|
-
this.root = root;
|
|
41
|
-
this.locator = locator;
|
|
42
|
-
this.signal = signal;
|
|
43
|
-
this.persistent = persistent;
|
|
44
|
-
this.maxBytes = maxBytes;
|
|
45
|
-
this.locator = Object.freeze({
|
|
46
|
-
provider: locator.provider,
|
|
47
|
-
account: locator.account,
|
|
48
|
-
occurrenceId: locator.occurrenceId,
|
|
49
|
-
assetId: locator.assetId,
|
|
50
|
-
});
|
|
51
|
-
const key = persistent ? digest(identity(this.locator)) : randomUUID();
|
|
52
|
-
this.path = join(root, `.zoom-partial-${key}.part`);
|
|
53
|
-
this.metadataPath = join(root, `.zoom-partial-${key}.json`);
|
|
13
|
+
export class RecordingPartialStore extends NativeRecordingPartialStore {
|
|
14
|
+
cancellation;
|
|
15
|
+
constructor(root, locator, cancellation, persistent = true, maxBytes = 20 * 1024 ** 3) {
|
|
16
|
+
super(root, locator, { prefix: ".zoom-partial-", persistent, maxBytes, signal: cancellation });
|
|
17
|
+
this.cancellation = cancellation;
|
|
54
18
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
throw new CliError("validation_error", "recording partial scope changed");
|
|
58
|
-
checkCancelled(this.signal);
|
|
59
|
-
if (!this.persistent)
|
|
60
|
-
return null;
|
|
61
|
-
let metadata;
|
|
62
|
-
try {
|
|
63
|
-
metadata = await openOrdinaryFile(this.metadataPath);
|
|
64
|
-
}
|
|
65
|
-
catch (error) {
|
|
66
|
-
if (missing(error))
|
|
67
|
-
return null;
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
let value;
|
|
71
|
-
try {
|
|
72
|
-
if ((await metadata.stat()).size > 16384)
|
|
73
|
-
throw new Error("metadata limit");
|
|
74
|
-
const bytes = Buffer.alloc(16385);
|
|
75
|
-
const count = (await metadata.read(bytes, 0, bytes.length, 0)).bytesRead;
|
|
76
|
-
if (count > 16384)
|
|
77
|
-
throw new Error("metadata limit");
|
|
78
|
-
value = JSON.parse(bytes.subarray(0, count).toString("utf8"));
|
|
79
|
-
if (Object.keys(value).some((key) => !["locator", "offset", "sha256", "validator", "total"].includes(key)) ||
|
|
80
|
-
identity(value.locator) !== identity(locator) ||
|
|
81
|
-
Object.keys(value.locator).some((key) => !["provider", "account", "occurrenceId", "assetId"].includes(key)) ||
|
|
82
|
-
!Number.isSafeInteger(value.offset) ||
|
|
83
|
-
value.offset < 0 ||
|
|
84
|
-
value.offset > this.maxBytes ||
|
|
85
|
-
!/^[a-f0-9]{64}$/.test(value.sha256))
|
|
86
|
-
throw new Error("metadata shape");
|
|
87
|
-
}
|
|
88
|
-
catch {
|
|
89
|
-
throw new CliError("validation_error", "recording partial metadata is invalid");
|
|
90
|
-
}
|
|
91
|
-
finally {
|
|
92
|
-
await metadata.close();
|
|
93
|
-
}
|
|
94
|
-
const file = await openOrdinaryFile(this.path);
|
|
95
|
-
try {
|
|
96
|
-
const actual = await fingerprint(file, this.signal, value.offset);
|
|
97
|
-
if (actual.offset !== value.offset || actual.sha256 !== value.sha256)
|
|
98
|
-
throw new CliError("validation_error", "recording partial checksum does not match");
|
|
99
|
-
return value;
|
|
100
|
-
}
|
|
101
|
-
finally {
|
|
102
|
-
await file.close();
|
|
103
|
-
}
|
|
19
|
+
load(locator) {
|
|
20
|
+
return operation(() => super.load(locator), this.cancellation);
|
|
104
21
|
}
|
|
105
22
|
async open(locator, options) {
|
|
106
|
-
|
|
107
|
-
throw new CliError("validation_error", "recording partial scope changed");
|
|
108
|
-
checkCancelled(this.signal);
|
|
109
|
-
let file;
|
|
110
|
-
if (options.resume) {
|
|
111
|
-
const witness = await openOrdinaryFile(this.path);
|
|
112
|
-
try {
|
|
113
|
-
file = await open(this.path, constants.O_RDWR |
|
|
114
|
-
(process.platform === "win32" ? 0 : (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0)));
|
|
115
|
-
const a = await witness.stat({ bigint: true });
|
|
116
|
-
const b = await file.stat({ bigint: true });
|
|
117
|
-
const after = await lstat(this.path, { bigint: true });
|
|
118
|
-
if (!b.isFile() || !after.isFile() || a.ino !== b.ino || a.dev !== b.dev || after.ino !== a.ino) {
|
|
119
|
-
await file.close();
|
|
120
|
-
throw new CliError("validation_error", "recording partial changed while opening");
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
finally {
|
|
124
|
-
await witness.close();
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
// Replace the owned partial name atomically without following an existing symlink.
|
|
129
|
-
const temp = join(this.root, `.zoom-partial-${randomUUID()}.tmp`);
|
|
130
|
-
file = await open(temp, "wx+", 0o600);
|
|
131
|
-
try {
|
|
132
|
-
await rename(temp, this.path);
|
|
133
|
-
await unlink(this.metadataPath).catch((error) => {
|
|
134
|
-
if (!missing(error))
|
|
135
|
-
throw error;
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
await file.close();
|
|
140
|
-
await unlink(temp).catch(() => undefined);
|
|
141
|
-
throw error;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
let prefix;
|
|
145
|
-
try {
|
|
146
|
-
prefix = await fingerprint(file, this.signal, options.resume?.offset);
|
|
147
|
-
if (options.resume && (prefix.offset !== options.resume.offset || prefix.sha256 !== options.resume.sha256))
|
|
148
|
-
throw new CliError("validation_error", "recording partial changed before append");
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
await file.close();
|
|
152
|
-
throw error;
|
|
153
|
-
}
|
|
154
|
-
if (options.resume && (await file.stat()).size > prefix.offset)
|
|
155
|
-
await file.truncate(prefix.offset);
|
|
156
|
-
let size = prefix.offset;
|
|
157
|
-
const hash = prefix.hash;
|
|
158
|
-
const checkpoint = async () => {
|
|
159
|
-
await file.sync();
|
|
160
|
-
if (!options.validator || !this.persistent) {
|
|
161
|
-
await syncDirectory(this.root);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const value = {
|
|
165
|
-
locator: {
|
|
166
|
-
provider: locator.provider,
|
|
167
|
-
account: locator.account,
|
|
168
|
-
occurrenceId: locator.occurrenceId,
|
|
169
|
-
assetId: locator.assetId,
|
|
170
|
-
},
|
|
171
|
-
offset: size,
|
|
172
|
-
sha256: hash.copy().digest("hex"),
|
|
173
|
-
validator: options.validator,
|
|
174
|
-
total: options.total,
|
|
175
|
-
};
|
|
176
|
-
const content = JSON.stringify(value);
|
|
177
|
-
if (Buffer.byteLength(content) > 16384)
|
|
178
|
-
throw new CliError("validation_error", "recording partial metadata exceeds its byte limit");
|
|
179
|
-
const temp = join(this.root, `.zoom-partial-${randomUUID()}.tmp`);
|
|
180
|
-
const metadata = await open(temp, "wx", 0o600);
|
|
181
|
-
try {
|
|
182
|
-
try {
|
|
183
|
-
await metadata.writeFile(content);
|
|
184
|
-
await metadata.sync();
|
|
185
|
-
}
|
|
186
|
-
finally {
|
|
187
|
-
await metadata.close();
|
|
188
|
-
}
|
|
189
|
-
await rename(temp, this.metadataPath);
|
|
190
|
-
await syncDirectory(this.root);
|
|
191
|
-
}
|
|
192
|
-
finally {
|
|
193
|
-
await unlink(temp).catch(() => undefined);
|
|
194
|
-
}
|
|
195
|
-
};
|
|
23
|
+
const writer = await operation(() => super.open(locator, options), this.cancellation);
|
|
196
24
|
return {
|
|
197
|
-
write:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (written === 0)
|
|
202
|
-
throw new CliError("configuration_error", "recording partial write did not progress");
|
|
203
|
-
offset += written;
|
|
204
|
-
}
|
|
205
|
-
hash.update(bytes);
|
|
206
|
-
size += bytes.length;
|
|
207
|
-
},
|
|
208
|
-
checkpoint,
|
|
209
|
-
finish: async () => {
|
|
210
|
-
await checkpoint();
|
|
211
|
-
this.completed = options.validator
|
|
212
|
-
? {
|
|
213
|
-
locator: { ...this.locator },
|
|
214
|
-
offset: size,
|
|
215
|
-
sha256: hash.copy().digest("hex"),
|
|
216
|
-
validator: options.validator,
|
|
217
|
-
total: options.total,
|
|
218
|
-
}
|
|
219
|
-
: null;
|
|
220
|
-
return { size, sha256: hash.copy().digest("hex") };
|
|
221
|
-
},
|
|
222
|
-
close: () => file.close(),
|
|
25
|
+
write: (bytes) => operation(() => writer.write(bytes), this.cancellation),
|
|
26
|
+
checkpoint: () => operation(() => writer.checkpoint(), this.cancellation),
|
|
27
|
+
finish: () => operation(() => writer.finish(), this.cancellation),
|
|
28
|
+
close: () => writer.close(),
|
|
223
29
|
};
|
|
224
30
|
}
|
|
225
31
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recording-partial-store.js","sourceRoot":"","sources":["../src/recording-partial-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"recording-partial-store.js","sourceRoot":"","sources":["../src/recording-partial-store.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,MAAM,sCAAsC,CAAA;AAClF,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAE5C,MAAM,SAAS,GAAG,KAAK,EAAK,GAAqB,EAAE,MAAoB,EAAc,EAAE;IACrF,cAAc,CAAC,MAAM,CAAC,CAAA;IACtB,IAAI,CAAC;QACH,OAAO,MAAM,GAAG,EAAE,CAAA;IACpB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,cAAc,CAAC,MAAM,CAAC,CAAA;QACtB,MAAM,KAAK,CAAA;IACb,CAAC;AACH,CAAC,CAAA;AAED,MAAM,OAAO,qBAAsB,SAAQ,2BAA2B;IAIjD,YAAY;IAH/B,YACE,IAAY,EACZ,OAA8B,EACb,YAA0B,EAC3C,UAAU,GAAG,IAAI,EACjB,QAAQ,GAAG,EAAE,GAAG,IAAI,IAAI,CAAC;QAEzB,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,CAAA;4BAJ7E,YAAY;IAK/B,CAAC;IAEQ,IAAI,CAAC,OAA8B;QAC1C,OAAO,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;IAChE,CAAC;IAEQ,KAAK,CAAC,IAAI,CACjB,OAA8B,EAC9B,OAA2D;QAE3D,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;QACrF,OAAO;YACL,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC;YACzE,UAAU,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,EAAE,IAAI,CAAC,YAAY,CAAC;YACzE,MAAM,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,IAAI,CAAC,YAAY,CAAC;YACjE,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;SAC5B,CAAA;IACH,CAAC;CACF"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type RecordingAssetLocator, type RecordingPartial } from "@wirecat/cli-core/meetings";
|
|
2
|
+
import { NativeRecordingPublication } from "@wirecat/cli-core/persistence/native";
|
|
2
3
|
import type { ArchivedFile, ArchiveManifest } from "./recording-archive.js";
|
|
3
4
|
export interface RecordingPromotion {
|
|
4
5
|
version: 1;
|
|
@@ -7,6 +8,6 @@ export interface RecordingPromotion {
|
|
|
7
8
|
file: ArchivedFile;
|
|
8
9
|
validator: string;
|
|
9
10
|
}
|
|
10
|
-
export declare const
|
|
11
|
-
export declare const
|
|
11
|
+
export declare const recordingPublication: (root: string, locator: RecordingAssetLocator, meeting: ArchiveManifest["meeting"], expected: Omit<ArchivedFile, "size" | "sha256">, maxBytes: number, signal?: AbortSignal) => NativeRecordingPublication<RecordingPromotion>;
|
|
12
|
+
export declare const recordingPromotion: (locator: RecordingAssetLocator, meeting: ArchiveManifest["meeting"], file: ArchivedFile, completed: RecordingPartial) => RecordingPromotion;
|
|
12
13
|
//# sourceMappingURL=recording-promotion-store.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recording-promotion-store.d.ts","sourceRoot":"","sources":["../src/recording-promotion-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"recording-promotion-store.d.ts","sourceRoot":"","sources":["../src/recording-promotion-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,qBAAqB,EAAE,KAAK,gBAAgB,EAA4B,MAAM,4BAA4B,CAAA;AACxH,OAAO,EAAE,0BAA0B,EAAE,MAAM,sCAAsC,CAAA;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAE3E,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,CAAC,CAAA;IACV,OAAO,EAAE,qBAAqB,CAAA;IAC9B,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAA;IACnC,IAAI,EAAE,YAAY,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAQD,eAAO,MAAM,oBAAoB,SACzB,MAAM,WACH,qBAAqB,WACrB,eAAe,CAAC,SAAS,CAAC,YACzB,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAAC,YACrC,MAAM,WACP,WAAW,mDA6ClB,CAAA;AAEJ,eAAO,MAAM,kBAAkB,YACpB,qBAAqB,WACrB,eAAe,CAAC,SAAS,CAAC,QAC7B,YAAY,aACP,gBAAgB,KAC1B,kBAWD,CAAA"}
|
|
@@ -1,92 +1,63 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { open, rename, unlink } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { CliError } from "@wirecat/cli-base";
|
|
5
|
-
import { openOrdinaryFile, syncDirectory } from "@wirecat/cli-core/files";
|
|
6
1
|
import { strongRecordingValidator } from "@wirecat/cli-core/meetings";
|
|
7
|
-
|
|
2
|
+
import { NativeRecordingPublication } from "@wirecat/cli-core/persistence/native";
|
|
8
3
|
const scope = (locator) => [
|
|
9
4
|
locator.provider,
|
|
10
5
|
locator.account,
|
|
11
6
|
locator.occurrenceId,
|
|
12
7
|
locator.assetId,
|
|
13
8
|
];
|
|
14
|
-
const name = (root, locator) => join(root, `.zoom-promotion-${createHash("sha256")
|
|
15
|
-
.update(JSON.stringify(scope(locator)))
|
|
16
|
-
.digest("hex")}.json`);
|
|
17
9
|
const keys = (value, allowed) => Object.keys(value).every((key) => allowed.includes(key));
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
export const recordingPublication = (root, locator, meeting, expected, maxBytes, signal) => new NativeRecordingPublication(root, locator, {
|
|
11
|
+
prefix: ".zoom-promotion-",
|
|
12
|
+
filename: expected.path,
|
|
13
|
+
maxBytes,
|
|
14
|
+
metadataLimit: 32768,
|
|
15
|
+
signal,
|
|
16
|
+
codec: {
|
|
17
|
+
encode: (value) => JSON.stringify(value),
|
|
18
|
+
decode: (value) => {
|
|
19
|
+
const record = value;
|
|
20
|
+
if (record.version !== 1 ||
|
|
21
|
+
!keys(record, ["version", "locator", "meeting", "file", "validator"]) ||
|
|
22
|
+
!keys(record.locator, ["provider", "account", "occurrenceId", "assetId"]) ||
|
|
23
|
+
JSON.stringify(scope(record.locator)) !== JSON.stringify(scope(locator)) ||
|
|
24
|
+
!keys(record.meeting, ["id", "uuid", "title", "startedAt"]) ||
|
|
25
|
+
record.meeting.id !== meeting.id ||
|
|
26
|
+
record.meeting.uuid !== meeting.uuid ||
|
|
27
|
+
record.meeting.title !== meeting.title ||
|
|
28
|
+
record.meeting.startedAt !== meeting.startedAt ||
|
|
29
|
+
!keys(record.file, [
|
|
30
|
+
"id",
|
|
31
|
+
"path",
|
|
32
|
+
"remoteSize",
|
|
33
|
+
"type",
|
|
34
|
+
"format",
|
|
35
|
+
"startedAt",
|
|
36
|
+
"endedAt",
|
|
37
|
+
"size",
|
|
38
|
+
"sha256",
|
|
39
|
+
]) ||
|
|
40
|
+
Object.entries(expected).some(([key, value]) => record.file[key] !== value) ||
|
|
41
|
+
!Number.isSafeInteger(record.file.size) ||
|
|
42
|
+
record.file.size < 0 ||
|
|
43
|
+
!/^[a-f0-9]{64}$/.test(record.file.sha256) ||
|
|
44
|
+
!strongRecordingValidator(record.validator))
|
|
45
|
+
throw new Error("record shape");
|
|
46
|
+
return record;
|
|
26
47
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await handle.close();
|
|
43
|
-
}
|
|
44
|
-
await rename(temp, name(root, locator));
|
|
45
|
-
await syncDirectory(root);
|
|
46
|
-
}
|
|
47
|
-
finally {
|
|
48
|
-
await unlink(temp).catch(() => undefined);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
export const readRecordingPromotion = async (root, locator, meeting, expected) => {
|
|
52
|
-
let handle;
|
|
53
|
-
try {
|
|
54
|
-
handle = await openOrdinaryFile(name(root, locator));
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
if (missing(error))
|
|
58
|
-
return null;
|
|
59
|
-
throw error;
|
|
60
|
-
}
|
|
61
|
-
try {
|
|
62
|
-
const buffer = Buffer.alloc(32769);
|
|
63
|
-
const count = (await handle.read(buffer, 0, buffer.length, 0)).bytesRead;
|
|
64
|
-
if (count > 32768)
|
|
65
|
-
throw new Error("metadata limit");
|
|
66
|
-
const record = JSON.parse(buffer.subarray(0, count).toString("utf8"));
|
|
67
|
-
if (record.version !== 1 ||
|
|
68
|
-
!keys(record, ["version", "locator", "meeting", "file", "validator"]) ||
|
|
69
|
-
!keys(record.locator, ["provider", "account", "occurrenceId", "assetId"]) ||
|
|
70
|
-
JSON.stringify(scope(record.locator)) !== JSON.stringify(scope(locator)) ||
|
|
71
|
-
!keys(record.meeting, ["id", "uuid", "title", "startedAt"]) ||
|
|
72
|
-
record.meeting.id !== meeting.id ||
|
|
73
|
-
record.meeting.uuid !== meeting.uuid ||
|
|
74
|
-
record.meeting.title !== meeting.title ||
|
|
75
|
-
record.meeting.startedAt !== meeting.startedAt ||
|
|
76
|
-
!keys(record.file, ["id", "path", "remoteSize", "type", "format", "startedAt", "endedAt", "size", "sha256"]) ||
|
|
77
|
-
Object.entries(expected).some(([key, value]) => record.file[key] !== value) ||
|
|
78
|
-
!Number.isSafeInteger(record.file.size) ||
|
|
79
|
-
record.file.size < 0 ||
|
|
80
|
-
!/^[a-f0-9]{64}$/.test(record.file.sha256) ||
|
|
81
|
-
!strongRecordingValidator(record.validator))
|
|
82
|
-
throw new Error("record shape");
|
|
83
|
-
return record;
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
throw new CliError("validation_error", "recording recovery record does not match this scoped asset");
|
|
87
|
-
}
|
|
88
|
-
finally {
|
|
89
|
-
await handle.close();
|
|
90
|
-
}
|
|
91
|
-
};
|
|
48
|
+
},
|
|
49
|
+
evidence: (value) => ({ size: value.file.size, sha256: value.file.sha256, validator: value.validator }),
|
|
50
|
+
});
|
|
51
|
+
export const recordingPromotion = (locator, meeting, file, completed) => ({
|
|
52
|
+
version: 1,
|
|
53
|
+
locator: {
|
|
54
|
+
provider: locator.provider,
|
|
55
|
+
account: locator.account,
|
|
56
|
+
occurrenceId: locator.occurrenceId,
|
|
57
|
+
assetId: locator.assetId,
|
|
58
|
+
},
|
|
59
|
+
meeting: { ...meeting },
|
|
60
|
+
file: { ...file },
|
|
61
|
+
validator: completed.validator,
|
|
62
|
+
});
|
|
92
63
|
//# sourceMappingURL=recording-promotion-store.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recording-promotion-store.js","sourceRoot":"","sources":["../src/recording-promotion-store.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"recording-promotion-store.js","sourceRoot":"","sources":["../src/recording-promotion-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Logger } from "@wirecat/cli-base";
|
|
2
|
+
import { startRun } from "@wirecat/cli-core/cli";
|
|
3
|
+
type RunStatus = Parameters<ReturnType<typeof startRun>["finish"]>[0];
|
|
4
|
+
export interface RunLogOptions {
|
|
5
|
+
runsDir: string;
|
|
6
|
+
command: string;
|
|
7
|
+
version: string;
|
|
8
|
+
allow: readonly string[];
|
|
9
|
+
onError?: (error: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
export interface RunLog {
|
|
12
|
+
log: Logger;
|
|
13
|
+
finish(status: RunStatus): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare const lazyRunLog: (options: RunLogOptions) => RunLog;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=run-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-log.d.ts","sourceRoot":"","sources":["../src/run-log.ts"],"names":[],"mappings":"AACA,OAAO,EAAmB,KAAK,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAEhD,KAAK,SAAS,GAAG,UAAU,CAAC,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AAErE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACjC;AACD,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACzC;AAKD,eAAO,MAAM,UAAU,YAAa,aAAa,KAAG,MA2CnD,CAAA"}
|
package/dist/run-log.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { createRecordLog } from "@wirecat/cli-base";
|
|
3
|
+
import { startRun } from "@wirecat/cli-core/cli";
|
|
4
|
+
// Opens the run on its first event, so a command that logs nothing leaves no run directory behind.
|
|
5
|
+
// startRun's own logger allows only cli-core's keys, so zm's records go through createRecordLog with
|
|
6
|
+
// zm's allow-list, into the same events.jsonl.
|
|
7
|
+
export const lazyRunLog = (options) => {
|
|
8
|
+
let opened;
|
|
9
|
+
let failed = false;
|
|
10
|
+
const open = () => {
|
|
11
|
+
if (failed)
|
|
12
|
+
return undefined;
|
|
13
|
+
try {
|
|
14
|
+
if (!opened) {
|
|
15
|
+
const resource = { service: "zm", version: options.version };
|
|
16
|
+
const run = startRun({
|
|
17
|
+
runsDir: options.runsDir,
|
|
18
|
+
command: options.command,
|
|
19
|
+
profile: "",
|
|
20
|
+
service: resource.service,
|
|
21
|
+
cliVersion: options.version,
|
|
22
|
+
...(options.onError ? { onError: options.onError } : {}),
|
|
23
|
+
});
|
|
24
|
+
const log = createRecordLog({
|
|
25
|
+
to: join(run.dir, "events.jsonl"),
|
|
26
|
+
runId: run.id,
|
|
27
|
+
command: options.command,
|
|
28
|
+
resource,
|
|
29
|
+
allow: options.allow,
|
|
30
|
+
...(options.onError ? { onError: options.onError } : {}),
|
|
31
|
+
});
|
|
32
|
+
opened = { finish: (status) => run.finish(status), log };
|
|
33
|
+
}
|
|
34
|
+
return opened.log;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
failed = true;
|
|
38
|
+
options.onError?.(error instanceof Error ? error : new Error("could not open the run log"));
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const at = (level) => (event) => open()?.[level](event);
|
|
43
|
+
return {
|
|
44
|
+
log: { debug: at("debug"), info: at("info"), warn: at("warn"), error: at("error") },
|
|
45
|
+
finish: async (status) => {
|
|
46
|
+
await opened?.finish(status);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=run-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-log.js","sourceRoot":"","sources":["../src/run-log.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAChC,OAAO,EAAE,eAAe,EAAe,MAAM,mBAAmB,CAAA;AAChE,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAgBhD,mGAAmG;AACnG,qGAAqG;AACrG,+CAA+C;AAC/C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAsB,EAAU,EAAE;IAC3D,IAAI,MAAiF,CAAA;IACrF,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,MAAM,IAAI,GAAG,GAAuB,EAAE;QACpC,IAAI,MAAM;YAAE,OAAO,SAAS,CAAA;QAC5B,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,QAAQ,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAA;gBAC5D,MAAM,GAAG,GAAG,QAAQ,CAAC;oBACnB,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,OAAO,EAAE,EAAE;oBACX,OAAO,EAAE,QAAQ,CAAC,OAAO;oBACzB,UAAU,EAAE,OAAO,CAAC,OAAO;oBAC3B,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACzD,CAAC,CAAA;gBACF,MAAM,GAAG,GAAG,eAAe,CAAC;oBAC1B,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,cAAc,CAAC;oBACjC,KAAK,EAAE,GAAG,CAAC,EAAE;oBACb,OAAO,EAAE,OAAO,CAAC,OAAO;oBACxB,QAAQ;oBACR,KAAK,EAAE,OAAO,CAAC,KAAK;oBACpB,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;iBACzD,CAAC,CAAA;gBACF,MAAM,GAAG,EAAE,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,CAAA;YAC1D,CAAC;YACD,OAAO,MAAM,CAAC,GAAG,CAAA;QACnB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,GAAG,IAAI,CAAA;YACb,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC,CAAA;YAC3F,OAAO,SAAS,CAAA;QAClB,CAAC;IACH,CAAC,CAAA;IACD,MAAM,EAAE,GACN,CAAC,KAAmB,EAAE,EAAE,CACxB,CAAC,KAAa,EAAQ,EAAE,CACtB,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAA;IAC1B,OAAO;QACL,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,EAAE;QACnF,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YACvB,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAA;QAC9B,CAAC;KACF,CAAA;AACH,CAAC,CAAA"}
|
package/dist/tokens.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KeyringStore } from "@wirecat/cli-base";
|
|
1
|
+
import type { KeyringStore, Logger } from "@wirecat/cli-base";
|
|
2
2
|
export interface Tokens {
|
|
3
3
|
accessToken: string;
|
|
4
4
|
refreshToken: string;
|
|
@@ -12,7 +12,11 @@ export interface TokenOptions {
|
|
|
12
12
|
fetch: typeof fetch;
|
|
13
13
|
now?: () => number;
|
|
14
14
|
signal?: AbortSignal;
|
|
15
|
+
log?: Logger;
|
|
15
16
|
}
|
|
17
|
+
/** Keys a token record carries beyond cli-base's own; names only, nothing from the token itself. */
|
|
18
|
+
export declare const TOKEN_LOG_ALLOW: readonly ["rotated", "saved", "pid"];
|
|
19
|
+
export declare const TOKEN_LOG_EVENT = "zoom.token.exchange";
|
|
16
20
|
export declare class TokenManager {
|
|
17
21
|
readonly options: TokenOptions;
|
|
18
22
|
private refreshState;
|
package/dist/tokens.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAI7D,MAAM,WAAW,MAAM;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,EAAE,CAAA;CACjB;AACD,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,YAAY,CAAA;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,OAAO,KAAK,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AACD,oGAAoG;AACpG,eAAO,MAAM,eAAe,YAAI,SAAS,EAAE,OAAO,EAAE,KAAK,CAAU,CAAA;AACnE,eAAO,MAAM,eAAe,wBAAwB,CAAA;AAKpD,qBAAa,YAAY;IAEX,QAAQ,CAAC,OAAO,EAAE,YAAY;IAD1C,OAAO,CAAC,YAAY,CAAqC;IACzD,YAAqB,OAAO,EAAE,YAAY,EAAI;IAC9C,UAAU,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,OAAO,GAAE,OAAO,KAA0B,GAAG,YAAY,CAiBzF;IACD,IAAI,CAAC,QAAQ,SAAwB,GAAG,MAAM,GAAG,IAAI,CA8BpD;IACD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAwB,GAAG,IAAI,CAM3D;IACK,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,QAAQ,SAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgFnH;IACK,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,CAoB9B;CACF"}
|