@synopackageland/cli 0.1.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.
Files changed (170) hide show
  1. package/README.md +13 -0
  2. package/dist/attestation/app-package.d.ts +86 -0
  3. package/dist/attestation/app-package.js +199 -0
  4. package/dist/attestation/install-on-host.d.ts +14 -0
  5. package/dist/attestation/install-on-host.js +52 -0
  6. package/dist/attestation/keyring.d.ts +21 -0
  7. package/dist/attestation/keyring.js +168 -0
  8. package/dist/bin.d.ts +2 -0
  9. package/dist/bin.js +14 -0
  10. package/dist/build-spk/dsm-arch.d.ts +4 -0
  11. package/dist/build-spk/dsm-arch.js +32 -0
  12. package/dist/build-spk/dsm-icons.d.ts +2 -0
  13. package/dist/build-spk/dsm-icons.js +67 -0
  14. package/dist/build-spk/generator.d.ts +43 -0
  15. package/dist/build-spk/generator.js +653 -0
  16. package/dist/build-spk/native-payload.d.ts +22 -0
  17. package/dist/build-spk/native-payload.js +654 -0
  18. package/dist/build-spk/reproducible-archive.d.ts +8 -0
  19. package/dist/build-spk/reproducible-archive.js +34 -0
  20. package/dist/cli.d.ts +1 -0
  21. package/dist/cli.js +345 -0
  22. package/dist/compose/analyze.d.ts +25 -0
  23. package/dist/compose/analyze.js +346 -0
  24. package/dist/compose/env.d.ts +7 -0
  25. package/dist/compose/env.js +36 -0
  26. package/dist/compose/privilege.d.ts +14 -0
  27. package/dist/compose/privilege.js +102 -0
  28. package/dist/compose/runner.d.ts +32 -0
  29. package/dist/compose/runner.js +84 -0
  30. package/dist/contract/load.d.ts +25 -0
  31. package/dist/contract/load.js +87 -0
  32. package/dist/contract/schema-validate.d.ts +3 -0
  33. package/dist/contract/schema-validate.js +50 -0
  34. package/dist/contract/validate.d.ts +3 -0
  35. package/dist/contract/validate.js +80 -0
  36. package/dist/contract/web-launch.d.ts +19 -0
  37. package/dist/contract/web-launch.js +94 -0
  38. package/dist/deploy/catalog-publish.d.ts +3 -0
  39. package/dist/deploy/catalog-publish.js +7 -0
  40. package/dist/deploy/deploy.d.ts +22 -0
  41. package/dist/deploy/deploy.js +136 -0
  42. package/dist/deploy/identity-shared.d.ts +3 -0
  43. package/dist/deploy/identity-shared.js +14 -0
  44. package/dist/deploy/identity.d.ts +7 -0
  45. package/dist/deploy/identity.js +20 -0
  46. package/dist/deploy/package-source.d.ts +7 -0
  47. package/dist/deploy/package-source.js +17 -0
  48. package/dist/deploy/run-as-values.d.ts +18 -0
  49. package/dist/deploy/run-as-values.js +48 -0
  50. package/dist/deploy/share-values.d.ts +20 -0
  51. package/dist/deploy/share-values.js +60 -0
  52. package/dist/deploy/spk-info.d.ts +12 -0
  53. package/dist/deploy/spk-info.js +41 -0
  54. package/dist/deploy/spk-version.d.ts +4 -0
  55. package/dist/deploy/spk-version.js +64 -0
  56. package/dist/dev.d.ts +13 -0
  57. package/dist/dev.js +31 -0
  58. package/dist/discovery.d.ts +8 -0
  59. package/dist/discovery.js +22 -0
  60. package/dist/errors.d.ts +6 -0
  61. package/dist/errors.js +18 -0
  62. package/dist/host/broker-paths.d.ts +1 -0
  63. package/dist/host/broker-paths.js +1 -0
  64. package/dist/host/catalog-install.d.ts +59 -0
  65. package/dist/host/catalog-install.js +326 -0
  66. package/dist/host/credentials.d.ts +2 -0
  67. package/dist/host/credentials.js +37 -0
  68. package/dist/host/digests.d.ts +5 -0
  69. package/dist/host/digests.js +55 -0
  70. package/dist/host/dsm-client.d.ts +6 -0
  71. package/dist/host/dsm-client.js +290 -0
  72. package/dist/host/dsm-http.d.ts +1 -0
  73. package/dist/host/dsm-http.js +87 -0
  74. package/dist/host/fake-nas.d.ts +16 -0
  75. package/dist/host/fake-nas.js +116 -0
  76. package/dist/host/open-url.d.ts +5 -0
  77. package/dist/host/open-url.js +13 -0
  78. package/dist/host/share-lookup.d.ts +3 -0
  79. package/dist/host/share-lookup.js +144 -0
  80. package/dist/host/types.d.ts +67 -0
  81. package/dist/host/types.js +1 -0
  82. package/dist/index.d.ts +19 -0
  83. package/dist/index.js +14 -0
  84. package/dist/info.d.ts +17 -0
  85. package/dist/info.js +34 -0
  86. package/dist/init.d.ts +4 -0
  87. package/dist/init.js +31 -0
  88. package/dist/inspect.d.ts +13 -0
  89. package/dist/inspect.js +102 -0
  90. package/dist/load-env.d.ts +1 -0
  91. package/dist/load-env.js +31 -0
  92. package/dist/log.d.ts +13 -0
  93. package/dist/log.js +20 -0
  94. package/dist/mock-broker/bootstrap.d.ts +2 -0
  95. package/dist/mock-broker/bootstrap.js +8 -0
  96. package/dist/mock-broker/files.d.ts +60 -0
  97. package/dist/mock-broker/files.js +408 -0
  98. package/dist/mock-broker/oidc.d.ts +31 -0
  99. package/dist/mock-broker/oidc.js +161 -0
  100. package/dist/mock-broker/path.d.ts +9 -0
  101. package/dist/mock-broker/path.js +50 -0
  102. package/dist/mock-broker/server.d.ts +19 -0
  103. package/dist/mock-broker/server.js +350 -0
  104. package/dist/publish/build-ledger.d.ts +22 -0
  105. package/dist/publish/build-ledger.js +26 -0
  106. package/dist/publish/community-path.d.ts +6 -0
  107. package/dist/publish/community-path.js +41 -0
  108. package/dist/publish/curated-release.d.ts +78 -0
  109. package/dist/publish/curated-release.js +693 -0
  110. package/dist/publish/file-build-ledger.d.ts +18 -0
  111. package/dist/publish/file-build-ledger.js +268 -0
  112. package/dist/publish/github-pr.d.ts +25 -0
  113. package/dist/publish/github-pr.js +85 -0
  114. package/dist/publish/github-repo.d.ts +8 -0
  115. package/dist/publish/github-repo.js +21 -0
  116. package/dist/publish/publish.d.ts +23 -0
  117. package/dist/publish/publish.js +128 -0
  118. package/dist/publish/sync-definition.d.ts +4 -0
  119. package/dist/publish/sync-definition.js +48 -0
  120. package/dist/skill.d.ts +10 -0
  121. package/dist/skill.js +50 -0
  122. package/dist/templates/compose-import/compose.yaml +11 -0
  123. package/dist/templates/compose-import/synology-app.yaml +15 -0
  124. package/dist/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
  125. package/dist/templates/hello-files/compose.yaml +8 -0
  126. package/dist/templates/hello-files/synology-app.yaml +13 -0
  127. package/dist/templates/hello-web/compose.yaml +7 -0
  128. package/dist/templates/hello-web/html/app.js +23 -0
  129. package/dist/templates/hello-web/html/index.html +15 -0
  130. package/dist/templates/hello-web/html/sdk.js +68 -0
  131. package/dist/templates/hello-web/synology-app.yaml +13 -0
  132. package/dist/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
  133. package/dist/templates/hello-web-page/compose.yaml +7 -0
  134. package/dist/templates/hello-web-page/html/app.js +23 -0
  135. package/dist/templates/hello-web-page/html/index.html +15 -0
  136. package/dist/templates/hello-web-page/html/sdk.js +68 -0
  137. package/dist/templates/hello-web-page/synology-app.yaml +13 -0
  138. package/dist/test-command.d.ts +13 -0
  139. package/dist/test-command.js +26 -0
  140. package/dist/test-host.d.ts +13 -0
  141. package/dist/test-host.js +24 -0
  142. package/dist/types.d.ts +59 -0
  143. package/dist/types.js +1 -0
  144. package/dist/validate.d.ts +8 -0
  145. package/dist/validate.js +103 -0
  146. package/package.json +38 -0
  147. package/schema/synology-app.schema.json +137 -0
  148. package/skills/SKILL.md +51 -0
  149. package/skills/reference/create.md +34 -0
  150. package/skills/reference/deploy.md +41 -0
  151. package/skills/reference/diagnose.md +42 -0
  152. package/skills/reference/files.md +37 -0
  153. package/skills/reference/identity.md +31 -0
  154. package/skills/reference/wrap.md +35 -0
  155. package/templates/compose-import/compose.yaml +11 -0
  156. package/templates/compose-import/synology-app.yaml +15 -0
  157. package/templates/hello-files/com.synology.hello.files.dev.spk +0 -0
  158. package/templates/hello-files/compose.yaml +8 -0
  159. package/templates/hello-files/synology-app.yaml +13 -0
  160. package/templates/hello-web/compose.yaml +7 -0
  161. package/templates/hello-web/html/app.js +23 -0
  162. package/templates/hello-web/html/index.html +15 -0
  163. package/templates/hello-web/html/sdk.js +68 -0
  164. package/templates/hello-web/synology-app.yaml +13 -0
  165. package/templates/hello-web-page/com.synology.hello.web.page.dev.spk +0 -0
  166. package/templates/hello-web-page/compose.yaml +7 -0
  167. package/templates/hello-web-page/html/app.js +23 -0
  168. package/templates/hello-web-page/html/index.html +15 -0
  169. package/templates/hello-web-page/html/sdk.js +68 -0
  170. package/templates/hello-web-page/synology-app.yaml +13 -0
@@ -0,0 +1,408 @@
1
+ import { copyFileSync, createReadStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { dirname, join, posix } from "node:path";
3
+ import { normalizeLogicalPath, parentAndName, PathValidationError } from "./path.js";
4
+ export class FileStoreError extends Error {
5
+ code;
6
+ status;
7
+ constructor(code, status, message) {
8
+ super(message);
9
+ this.name = "FileStoreError";
10
+ this.code = code;
11
+ this.status = status;
12
+ }
13
+ }
14
+ const ERROR_MESSAGES = {
15
+ NOT_FOUND: "找不到這個路徑。",
16
+ CONFLICT: "目標已存在。",
17
+ INVALID_PATH: "路徑不合法。",
18
+ TRASH_UNAVAILABLE: "目標沒有可用的 trash/recycle bin,檔案未被刪除。",
19
+ };
20
+ function notFound() {
21
+ throw new FileStoreError("NOT_FOUND", 404, ERROR_MESSAGES.NOT_FOUND);
22
+ }
23
+ function conflict() {
24
+ throw new FileStoreError("CONFLICT", 409, ERROR_MESSAGES.CONFLICT);
25
+ }
26
+ function invalidPath() {
27
+ throw new FileStoreError("INVALID_PATH", 400, ERROR_MESSAGES.INVALID_PATH);
28
+ }
29
+ function trashUnavailable() {
30
+ throw new FileStoreError("TRASH_UNAVAILABLE", 409, ERROR_MESSAGES.TRASH_UNAVAILABLE);
31
+ }
32
+ function seedDefaultTree() {
33
+ const root = { type: "directory", children: new Map() };
34
+ const home = ensureDirNode(root, "/home");
35
+ home.children.set("docs", { type: "directory", children: new Map() });
36
+ home.children.set("notes", { type: "directory", children: new Map() });
37
+ home.children.set("readme.txt", {
38
+ type: "file",
39
+ content: Buffer.alloc(42),
40
+ });
41
+ return root;
42
+ }
43
+ function ensureDirNode(root, path) {
44
+ let node = root;
45
+ for (const part of path.split("/").filter(Boolean)) {
46
+ node.children ??= new Map();
47
+ let child = node.children.get(part);
48
+ if (!child) {
49
+ child = { type: "directory", children: new Map() };
50
+ node.children.set(part, child);
51
+ }
52
+ if (child.type !== "directory") {
53
+ conflict();
54
+ }
55
+ node = child;
56
+ }
57
+ return node;
58
+ }
59
+ function getNode(root, path) {
60
+ if (path === "/") {
61
+ return root;
62
+ }
63
+ let node = root;
64
+ for (const part of path.split("/").filter(Boolean)) {
65
+ node = node?.children?.get(part);
66
+ if (!node) {
67
+ return undefined;
68
+ }
69
+ }
70
+ return node;
71
+ }
72
+ function toItem(path, node) {
73
+ const name = path.split("/").filter(Boolean).at(-1) ?? path;
74
+ if (node.type === "directory") {
75
+ return { name, type: "directory", path };
76
+ }
77
+ return {
78
+ name,
79
+ type: "file",
80
+ path,
81
+ size: node.content?.length ?? 0,
82
+ };
83
+ }
84
+ function mapLogicalToAbsolute(logicalPath, filesRoot) {
85
+ const trimmed = logicalPath.replace(/\\/g, "/").replace(/\/+$/, "") || "/";
86
+ if (trimmed === "/") {
87
+ return filesRoot;
88
+ }
89
+ const normalized = normalizeLogicalPath(logicalPath);
90
+ const relative = normalized.replace(/^\//, "");
91
+ return join(filesRoot, relative);
92
+ }
93
+ export class MockFileStore {
94
+ filesRoot;
95
+ recycleBinDisabled;
96
+ treeRoot;
97
+ constructor(options = {}) {
98
+ this.filesRoot = options.filesRoot;
99
+ this.recycleBinDisabled = options.recycleBinDisabled ?? false;
100
+ this.treeRoot = seedDefaultTree();
101
+ }
102
+ list(rawPath) {
103
+ const path = this.resolveListPath(rawPath);
104
+ if (this.filesRoot) {
105
+ return this.listMapped(path);
106
+ }
107
+ return this.listMemory(path);
108
+ }
109
+ stat(rawPath) {
110
+ const path = this.resolveOperationPath(rawPath);
111
+ if (this.filesRoot) {
112
+ return this.statMapped(path);
113
+ }
114
+ return this.statMemory(path);
115
+ }
116
+ download(rawPath, range) {
117
+ const path = this.resolveOperationPath(rawPath);
118
+ if (this.filesRoot) {
119
+ return this.downloadMapped(path, range);
120
+ }
121
+ return this.downloadMemory(path, range);
122
+ }
123
+ upload(rawPath, body, overwrite = false) {
124
+ const path = this.resolveOperationPath(rawPath);
125
+ if (this.filesRoot) {
126
+ this.uploadMapped(path, body, overwrite);
127
+ return;
128
+ }
129
+ this.uploadMemory(path, body, overwrite);
130
+ }
131
+ mkdir(rawPath) {
132
+ const path = this.resolveOperationPath(rawPath);
133
+ if (this.filesRoot) {
134
+ this.mkdirMapped(path);
135
+ return;
136
+ }
137
+ this.mkdirMemory(path);
138
+ }
139
+ rename(rawPath, rawTo, overwrite = false) {
140
+ this.move(rawPath, rawTo, overwrite);
141
+ }
142
+ move(rawPath, rawTo, overwrite = false) {
143
+ const path = this.resolveOperationPath(rawPath);
144
+ const to = this.resolveOperationPath(rawTo);
145
+ if (this.filesRoot) {
146
+ this.moveMapped(path, to, overwrite);
147
+ return;
148
+ }
149
+ this.moveMemory(path, to, overwrite);
150
+ }
151
+ copy(rawPath, rawTo, overwrite = false) {
152
+ const path = this.resolveOperationPath(rawPath);
153
+ const to = this.resolveOperationPath(rawTo);
154
+ if (this.filesRoot) {
155
+ this.copyMapped(path, to, overwrite);
156
+ return;
157
+ }
158
+ this.copyMemory(path, to, overwrite);
159
+ }
160
+ trash(rawPath) {
161
+ if (this.recycleBinDisabled) {
162
+ trashUnavailable();
163
+ }
164
+ const path = this.resolveOperationPath(rawPath);
165
+ if (this.filesRoot) {
166
+ this.trashMapped(path);
167
+ return;
168
+ }
169
+ this.trashMemory(path);
170
+ }
171
+ resolveListPath(rawPath) {
172
+ const trimmed = rawPath.trim();
173
+ if (trimmed === "/" || trimmed === "") {
174
+ return "/";
175
+ }
176
+ try {
177
+ return normalizeLogicalPath(trimmed);
178
+ }
179
+ catch (error) {
180
+ if (error instanceof PathValidationError) {
181
+ invalidPath();
182
+ }
183
+ throw error;
184
+ }
185
+ }
186
+ resolveOperationPath(rawPath) {
187
+ try {
188
+ return normalizeLogicalPath(rawPath);
189
+ }
190
+ catch (error) {
191
+ if (error instanceof PathValidationError) {
192
+ invalidPath();
193
+ }
194
+ throw error;
195
+ }
196
+ }
197
+ listMemory(path) {
198
+ const node = getNode(this.treeRoot, path);
199
+ if (!node || node.type !== "directory") {
200
+ return [];
201
+ }
202
+ const items = [];
203
+ for (const [name, child] of node.children ?? []) {
204
+ items.push(toItem(posix.join(path, name), child));
205
+ }
206
+ return items;
207
+ }
208
+ statMemory(path) {
209
+ const node = getNode(this.treeRoot, path);
210
+ if (!node) {
211
+ notFound();
212
+ }
213
+ return toItem(path, node);
214
+ }
215
+ downloadMemory(path, range) {
216
+ const node = getNode(this.treeRoot, path);
217
+ if (!node || node.type !== "file") {
218
+ notFound();
219
+ }
220
+ const original = node.content ?? Buffer.alloc(0);
221
+ if (!range) {
222
+ return { buffer: original, size: original.length, totalSize: original.length };
223
+ }
224
+ const end = range.end ?? original.length - 1;
225
+ const slice = original.subarray(range.start, end + 1);
226
+ return { buffer: slice, size: slice.length, totalSize: original.length };
227
+ }
228
+ uploadMemory(path, body, overwrite) {
229
+ if (getNode(this.treeRoot, path) && !overwrite) {
230
+ conflict();
231
+ }
232
+ const { parent, name } = parentAndName(path);
233
+ const dir = ensureDirNode(this.treeRoot, parent);
234
+ dir.children ??= new Map();
235
+ dir.children.set(name, { type: "file", content: Buffer.from(body) });
236
+ }
237
+ mkdirMemory(path) {
238
+ if (getNode(this.treeRoot, path)) {
239
+ conflict();
240
+ }
241
+ ensureDirNode(this.treeRoot, path);
242
+ }
243
+ moveMemory(path, to, overwrite) {
244
+ const node = getNode(this.treeRoot, path);
245
+ if (!node) {
246
+ notFound();
247
+ }
248
+ if (getNode(this.treeRoot, to) && !overwrite) {
249
+ conflict();
250
+ }
251
+ const from = parentAndName(path);
252
+ const dest = parentAndName(to);
253
+ const fromDir = getNode(this.treeRoot, from.parent);
254
+ if (!fromDir?.children?.has(from.name)) {
255
+ notFound();
256
+ }
257
+ fromDir.children.delete(from.name);
258
+ const toDir = ensureDirNode(this.treeRoot, dest.parent);
259
+ toDir.children ??= new Map();
260
+ toDir.children.set(dest.name, node);
261
+ }
262
+ copyMemory(path, to, overwrite) {
263
+ const node = getNode(this.treeRoot, path);
264
+ if (!node) {
265
+ notFound();
266
+ }
267
+ if (getNode(this.treeRoot, to) && !overwrite) {
268
+ conflict();
269
+ }
270
+ const dest = parentAndName(to);
271
+ const toDir = ensureDirNode(this.treeRoot, dest.parent);
272
+ toDir.children ??= new Map();
273
+ toDir.children.set(dest.name, cloneNode(node));
274
+ }
275
+ trashMemory(path) {
276
+ const node = getNode(this.treeRoot, path);
277
+ if (!node) {
278
+ notFound();
279
+ }
280
+ const { parent, name } = parentAndName(path);
281
+ const parentNode = getNode(this.treeRoot, parent);
282
+ parentNode?.children?.delete(name);
283
+ }
284
+ listMapped(path) {
285
+ const absolute = mapLogicalToAbsolute(path, this.filesRoot);
286
+ let entries;
287
+ try {
288
+ entries = readdirSync(absolute);
289
+ }
290
+ catch {
291
+ return [];
292
+ }
293
+ const normalized = path.replace(/\/+$/, "") || "/";
294
+ return entries.map((name) => {
295
+ const childAbsolute = join(absolute, name);
296
+ const childLogical = posix.join(normalized === "/" ? "" : normalized, name);
297
+ const displayPath = childLogical.startsWith("/") ? childLogical : `/${childLogical}`;
298
+ const stat = statSync(childAbsolute);
299
+ if (stat.isDirectory()) {
300
+ return { name, type: "directory", path: displayPath };
301
+ }
302
+ return {
303
+ name,
304
+ type: "file",
305
+ path: displayPath,
306
+ size: stat.size,
307
+ };
308
+ });
309
+ }
310
+ statMapped(path) {
311
+ const absolute = mapLogicalToAbsolute(path, this.filesRoot);
312
+ if (!existsSync(absolute)) {
313
+ notFound();
314
+ }
315
+ const stat = statSync(absolute);
316
+ const name = path.split("/").filter(Boolean).at(-1) ?? path;
317
+ if (stat.isDirectory()) {
318
+ return { name, type: "directory", path };
319
+ }
320
+ return { name, type: "file", path, size: stat.size };
321
+ }
322
+ downloadMapped(path, range) {
323
+ const absolute = mapLogicalToAbsolute(path, this.filesRoot);
324
+ if (!existsSync(absolute)) {
325
+ notFound();
326
+ }
327
+ const stat = statSync(absolute);
328
+ if (!stat.isFile()) {
329
+ notFound();
330
+ }
331
+ const totalSize = stat.size;
332
+ if (!range) {
333
+ return {
334
+ stream: createReadStream(absolute),
335
+ size: totalSize,
336
+ totalSize,
337
+ };
338
+ }
339
+ const end = range.end ?? totalSize - 1;
340
+ const size = end - range.start + 1;
341
+ return {
342
+ stream: createReadStream(absolute, { start: range.start, end }),
343
+ size,
344
+ totalSize,
345
+ };
346
+ }
347
+ uploadMapped(path, body, overwrite) {
348
+ const absolute = mapLogicalToAbsolute(path, this.filesRoot);
349
+ if (existsSync(absolute) && !overwrite) {
350
+ conflict();
351
+ }
352
+ mkdirSync(dirname(absolute), { recursive: true });
353
+ writeFileSync(absolute, body);
354
+ }
355
+ mkdirMapped(path) {
356
+ const absolute = mapLogicalToAbsolute(path, this.filesRoot);
357
+ if (existsSync(absolute)) {
358
+ conflict();
359
+ }
360
+ mkdirSync(absolute, { recursive: true });
361
+ }
362
+ moveMapped(path, to, overwrite) {
363
+ const fromAbsolute = mapLogicalToAbsolute(path, this.filesRoot);
364
+ const toAbsolute = mapLogicalToAbsolute(to, this.filesRoot);
365
+ if (!existsSync(fromAbsolute)) {
366
+ notFound();
367
+ }
368
+ if (existsSync(toAbsolute) && !overwrite) {
369
+ conflict();
370
+ }
371
+ mkdirSync(dirname(toAbsolute), { recursive: true });
372
+ renameSync(fromAbsolute, toAbsolute);
373
+ }
374
+ copyMapped(path, to, overwrite) {
375
+ const fromAbsolute = mapLogicalToAbsolute(path, this.filesRoot);
376
+ const toAbsolute = mapLogicalToAbsolute(to, this.filesRoot);
377
+ if (!existsSync(fromAbsolute)) {
378
+ notFound();
379
+ }
380
+ if (existsSync(toAbsolute) && !overwrite) {
381
+ conflict();
382
+ }
383
+ mkdirSync(dirname(toAbsolute), { recursive: true });
384
+ copyFileSync(fromAbsolute, toAbsolute);
385
+ }
386
+ trashMapped(path) {
387
+ const absolute = mapLogicalToAbsolute(path, this.filesRoot);
388
+ if (!existsSync(absolute)) {
389
+ notFound();
390
+ }
391
+ rmSync(absolute, { recursive: true, force: true });
392
+ }
393
+ }
394
+ function cloneNode(node) {
395
+ if (node.type === "file") {
396
+ return { type: "file", content: node.content ? Buffer.from(node.content) : Buffer.alloc(0) };
397
+ }
398
+ const children = new Map();
399
+ for (const [name, child] of node.children ?? []) {
400
+ children.set(name, cloneNode(child));
401
+ }
402
+ return { type: "directory", children };
403
+ }
404
+ /** @deprecated Use MockFileStore.list instead */
405
+ export function listFakeFiles(path, filesRoot) {
406
+ const store = new MockFileStore({ filesRoot });
407
+ return store.list(path);
408
+ }
@@ -0,0 +1,31 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare const MOCK_OIDC_CLIENT_SECRET = "mock-oidc-secret";
3
+ export declare const MOCK_OIDC_REDIRECT_PATH = "/api/auth/sso_callback";
4
+ export declare const MOCK_OIDC_KID_PREFIX = "oidc-";
5
+ export interface MockOidcUser {
6
+ subject: string;
7
+ username: string;
8
+ displayName: string;
9
+ }
10
+ export interface MockOidcOptions {
11
+ clientId?: string;
12
+ clientSecret?: string;
13
+ redirectUri?: string;
14
+ }
15
+ interface AuthCode {
16
+ code: string;
17
+ clientId: string;
18
+ redirectUri: string;
19
+ consumed: boolean;
20
+ }
21
+ export interface MockOidcContext {
22
+ packageId: string;
23
+ clientId: string;
24
+ clientSecret: string;
25
+ redirectUri: string;
26
+ user: MockOidcUser;
27
+ codes: Map<string, AuthCode>;
28
+ }
29
+ export declare function createMockOidcContext(packageId: string, user: MockOidcUser, options?: MockOidcOptions): MockOidcContext;
30
+ export declare function handleMockOidc(req: IncomingMessage, res: ServerResponse, url: URL, ctx: MockOidcContext): Promise<boolean>;
31
+ export {};
@@ -0,0 +1,161 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import { exportJWK, generateKeyPair, SignJWT } from "jose";
3
+ export const MOCK_OIDC_CLIENT_SECRET = "mock-oidc-secret";
4
+ export const MOCK_OIDC_REDIRECT_PATH = "/api/auth/sso_callback";
5
+ export const MOCK_OIDC_KID_PREFIX = "oidc-";
6
+ let oidcKeyPromise = null;
7
+ async function oidcKeys() {
8
+ if (!oidcKeyPromise) {
9
+ oidcKeyPromise = generateKeyPair("RS256");
10
+ }
11
+ return oidcKeyPromise;
12
+ }
13
+ function sendJson(res, status, body) {
14
+ res.statusCode = status;
15
+ res.setHeader("content-type", "application/json; charset=utf-8");
16
+ res.end(JSON.stringify(body));
17
+ }
18
+ function sendOAuthError(res, status, error) {
19
+ sendJson(res, status, { error });
20
+ }
21
+ function secretsEqual(left, right) {
22
+ return left.length === right.length && left === right;
23
+ }
24
+ function issuerFromRequest(req) {
25
+ const host = req.headers.host ?? "127.0.0.1";
26
+ return `http://${host}/oidc`;
27
+ }
28
+ function discovery(issuer) {
29
+ const base = issuer.replace(/\/$/, "");
30
+ return {
31
+ issuer: base,
32
+ authorization_endpoint: `${base}/authorize`,
33
+ token_endpoint: `${base}/token`,
34
+ jwks_uri: `${base}/jwks`,
35
+ response_types_supported: ["code"],
36
+ grant_types_supported: ["authorization_code"],
37
+ subject_types_supported: ["public"],
38
+ id_token_signing_alg_values_supported: ["RS256"],
39
+ token_endpoint_auth_methods_supported: ["client_secret_post"],
40
+ scopes_supported: ["openid"],
41
+ claims_supported: ["iss", "sub", "aud", "exp", "iat", "preferred_username", "name"],
42
+ };
43
+ }
44
+ async function oidcKid() {
45
+ const { publicKey } = await oidcKeys();
46
+ const jwk = await exportJWK(publicKey);
47
+ return `${MOCK_OIDC_KID_PREFIX}${createHash("sha256")
48
+ .update(JSON.stringify(jwk))
49
+ .digest("hex")
50
+ .slice(0, 16)}`;
51
+ }
52
+ async function issueIdToken(issuer, clientId, user) {
53
+ const { privateKey } = await oidcKeys();
54
+ const kid = await oidcKid();
55
+ return new SignJWT({
56
+ preferred_username: user.username,
57
+ name: user.username,
58
+ mock: true,
59
+ })
60
+ .setProtectedHeader({ alg: "RS256", kid })
61
+ .setIssuer(issuer)
62
+ .setAudience(clientId)
63
+ .setSubject(user.subject)
64
+ .setIssuedAt()
65
+ .setExpirationTime("15m")
66
+ .sign(privateKey);
67
+ }
68
+ function readBody(req) {
69
+ return new Promise((resolve, reject) => {
70
+ const chunks = [];
71
+ req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
72
+ req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
73
+ req.on("error", reject);
74
+ });
75
+ }
76
+ export function createMockOidcContext(packageId, user, options) {
77
+ return {
78
+ packageId,
79
+ clientId: options?.clientId ?? packageId,
80
+ clientSecret: options?.clientSecret ?? MOCK_OIDC_CLIENT_SECRET,
81
+ redirectUri: options?.redirectUri ?? `http://127.0.0.1${MOCK_OIDC_REDIRECT_PATH}`,
82
+ user,
83
+ codes: new Map(),
84
+ };
85
+ }
86
+ export async function handleMockOidc(req, res, url, ctx) {
87
+ const pathname = (url.pathname.replace(/\/$/, "") || "/");
88
+ if (!pathname.startsWith("/oidc")) {
89
+ return false;
90
+ }
91
+ const issuer = issuerFromRequest(req);
92
+ if (req.method === "GET" && pathname === "/oidc/.well-known/openid-configuration") {
93
+ sendJson(res, 200, discovery(issuer));
94
+ return true;
95
+ }
96
+ if (req.method === "GET" && pathname === "/oidc/jwks") {
97
+ const { publicKey } = await oidcKeys();
98
+ const jwk = await exportJWK(publicKey);
99
+ jwk.alg = "RS256";
100
+ jwk.use = "sig";
101
+ jwk.kid = await oidcKid();
102
+ sendJson(res, 200, { keys: [jwk] });
103
+ return true;
104
+ }
105
+ if (req.method === "GET" && pathname === "/oidc/authorize") {
106
+ const clientId = url.searchParams.get("client_id") ?? "";
107
+ const redirectUri = url.searchParams.get("redirect_uri") ?? "";
108
+ const responseType = url.searchParams.get("response_type") ?? "";
109
+ const state = url.searchParams.get("state");
110
+ if (clientId !== ctx.clientId || redirectUri !== ctx.redirectUri) {
111
+ sendOAuthError(res, 400, "invalid_request");
112
+ return true;
113
+ }
114
+ if (responseType !== "code") {
115
+ sendOAuthError(res, 400, "unsupported_response_type");
116
+ return true;
117
+ }
118
+ const code = randomUUID();
119
+ ctx.codes.set(code, { code, clientId, redirectUri, consumed: false });
120
+ const location = new URL(redirectUri);
121
+ location.searchParams.set("code", code);
122
+ if (state) {
123
+ location.searchParams.set("state", state);
124
+ }
125
+ res.statusCode = 302;
126
+ res.setHeader("location", location.toString());
127
+ res.end();
128
+ return true;
129
+ }
130
+ if (req.method === "POST" && pathname === "/oidc/token") {
131
+ const text = await readBody(req);
132
+ const body = Object.fromEntries(new URLSearchParams(text).entries());
133
+ if (body.client_id !== ctx.clientId || !secretsEqual(body.client_secret ?? "", ctx.clientSecret)) {
134
+ sendOAuthError(res, 401, "invalid_client");
135
+ return true;
136
+ }
137
+ if (body.grant_type !== "authorization_code") {
138
+ sendOAuthError(res, 400, "unsupported_grant_type");
139
+ return true;
140
+ }
141
+ const record = ctx.codes.get(body.code ?? "");
142
+ if (!record ||
143
+ record.consumed ||
144
+ record.clientId !== body.client_id ||
145
+ record.redirectUri !== (body.redirect_uri ?? "")) {
146
+ sendOAuthError(res, 400, "invalid_grant");
147
+ return true;
148
+ }
149
+ record.consumed = true;
150
+ const idToken = await issueIdToken(issuer, ctx.clientId, ctx.user);
151
+ sendJson(res, 200, {
152
+ id_token: idToken,
153
+ access_token: randomBytes(24).toString("base64url"),
154
+ token_type: "Bearer",
155
+ expires_in: 15 * 60,
156
+ });
157
+ return true;
158
+ }
159
+ sendOAuthError(res, 404, "invalid_request");
160
+ return true;
161
+ }
@@ -0,0 +1,9 @@
1
+ export declare class PathValidationError extends Error {
2
+ readonly code: "INVALID_PATH";
3
+ constructor();
4
+ }
5
+ export declare function normalizeLogicalPath(raw: string): string;
6
+ export declare function parentAndName(logicalPath: string): {
7
+ parent: string;
8
+ name: string;
9
+ };
@@ -0,0 +1,50 @@
1
+ export class PathValidationError extends Error {
2
+ code = "INVALID_PATH";
3
+ constructor() {
4
+ super("路徑不合法。");
5
+ this.name = "PathValidationError";
6
+ }
7
+ }
8
+ const MAX_PATH_LENGTH = 4096;
9
+ export function normalizeLogicalPath(raw) {
10
+ if (typeof raw !== "string" || raw.length === 0) {
11
+ throw new PathValidationError();
12
+ }
13
+ if (raw.includes("\0") || raw.length > MAX_PATH_LENGTH) {
14
+ throw new PathValidationError();
15
+ }
16
+ const unified = raw.replace(/\\/g, "/");
17
+ if (!unified.startsWith("/")) {
18
+ throw new PathValidationError();
19
+ }
20
+ if (/^\/volume\d+/i.test(unified) ||
21
+ unified.startsWith("/volumeUSB") ||
22
+ unified.startsWith("/volumeSATA")) {
23
+ throw new PathValidationError();
24
+ }
25
+ const parts = [];
26
+ for (const segment of unified.split("/")) {
27
+ if (segment === "" || segment === ".") {
28
+ continue;
29
+ }
30
+ if (segment === "..") {
31
+ throw new PathValidationError();
32
+ }
33
+ parts.push(segment);
34
+ }
35
+ if (parts.length === 0) {
36
+ throw new PathValidationError();
37
+ }
38
+ return `/${parts.join("/")}`;
39
+ }
40
+ export function parentAndName(logicalPath) {
41
+ const normalized = normalizeLogicalPath(logicalPath);
42
+ const idx = normalized.lastIndexOf("/");
43
+ if (idx <= 0) {
44
+ throw new PathValidationError();
45
+ }
46
+ return {
47
+ parent: normalized.slice(0, idx) || "/",
48
+ name: normalized.slice(idx + 1),
49
+ };
50
+ }
@@ -0,0 +1,19 @@
1
+ import { type Server } from "node:http";
2
+ import type { SynologyUser } from "@synopackageland/dev-sdk/node";
3
+ import { type MockOidcOptions } from "./oidc.js";
4
+ export declare const FAKE_USER: SynologyUser;
5
+ export interface MockBrokerOptions {
6
+ packageId: string;
7
+ filesRoot?: string;
8
+ recycleBinDisabled?: boolean;
9
+ port?: number;
10
+ oidc?: MockOidcOptions;
11
+ }
12
+ export interface MockBrokerHandle {
13
+ baseUrl: string;
14
+ jwksUrl: string;
15
+ packageId: string;
16
+ server: Server;
17
+ close(): Promise<void>;
18
+ }
19
+ export declare function startMockBroker(options: MockBrokerOptions): Promise<MockBrokerHandle>;