@sasjs/api 0.5.0 → 0.6.1

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.
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Deploying a file tree, and the cap that bounds it.
3
+ *
4
+ * `POST /SASjsApi/drive/deploy` writes a whole tree in one request, which makes
5
+ * it the one drive operation where a caller can fill the host's storage: the
6
+ * others write a single file at a time, bounded by the host's upload limit. So
7
+ * the tree is measured and its names checked BEFORE anything is written, and a
8
+ * refusal leaves the drive exactly as it was.
9
+ *
10
+ * The numbers live here rather than in a host, because both hosts answer the
11
+ * same `413` and a corpus case pins the wording: a limit stated twice is a limit
12
+ * that disagrees with itself in the release nobody re-read.
13
+ *
14
+ * Nothing here imports a runtime. A base64 file member is decoded by hand and a
15
+ * service's size is counted as UTF-8 by hand, because the same build runs in a
16
+ * service worker where `Buffer` does not exist and the package compiles against
17
+ * `lib: ES2020` with no ambient types.
18
+ */
19
+ import type { DriveStore } from '../types.js';
20
+ /** The three member kinds the wire format carries. */
21
+ export type DeployMemberType = 'folder' | 'service' | 'file';
22
+ export interface DeployFolderMember {
23
+ name: string;
24
+ type: 'folder';
25
+ members: DeployMember[];
26
+ }
27
+ /** A program; written as `<name>.sas`, its `code` being the program text. */
28
+ export interface DeployServiceMember {
29
+ name: string;
30
+ type: 'service';
31
+ code: string;
32
+ }
33
+ /** A file; `code` is base64 of the bytes to write. */
34
+ export interface DeployFileMember {
35
+ name: string;
36
+ type: 'file';
37
+ code: string;
38
+ }
39
+ export type DeployMember = DeployFolderMember | DeployServiceMember | DeployFileMember;
40
+ export interface DeployTree {
41
+ members: DeployMember[];
42
+ }
43
+ /**
44
+ * The deploy cap, in binary megabytes (the unit a host's upload limit reports)
45
+ * and compared inclusively: a tree of exactly 10,000 entries and exactly 100 MB
46
+ * passes.
47
+ *
48
+ * The three dimensions fail differently and so are separate: an estate is many
49
+ * small files (`entries`), a compiled service is one large file (`fileBytes`),
50
+ * and the total is what the host holds in memory before the tree reaches the
51
+ * drive (`totalBytes`).
52
+ */
53
+ export interface DeployLimits {
54
+ readonly entries: number;
55
+ readonly totalBytes: number;
56
+ readonly fileBytes: number;
57
+ }
58
+ export declare const DEPLOY_LIMITS: DeployLimits;
59
+ /**
60
+ * The largest a deploy tree can be on the wire, which is what a host's transport
61
+ * has to admit.
62
+ *
63
+ * The cap is stated in DECODED bytes; the tree travels as JSON in which a file
64
+ * member's contents are base64 - 4/3 of what they write - and a service's are
65
+ * escaped text (~1.04). A tree at `totalBytes` therefore reaches the parser at
66
+ * up to ~134MB, and a transport that admits less refuses first with its own
67
+ * shape, so the cap it was meant to enforce is never consulted. 140MB leaves
68
+ * room for the tree's JSON scaffolding.
69
+ *
70
+ * Only the deploy routes use it: everywhere else a smaller limit applies,
71
+ * because a body is buffered before authentication and nothing else needs the
72
+ * headroom.
73
+ */
74
+ export declare const DEPLOY_PAYLOAD_BYTES: number;
75
+ /**
76
+ * The body a deploy answers success with.
77
+ *
78
+ * The wording is the published contract - a client may key on the message - so
79
+ * it is one constant here rather than a string in each host. It names
80
+ * `@sasjs/server` because that is the document's example, which predates this
81
+ * package; changing it is a document change, not a host's choice.
82
+ */
83
+ export declare const DEPLOY_SUCCESS: {
84
+ readonly status: "success";
85
+ readonly message: "Files deployed successfully to @sasjs/server.";
86
+ };
87
+ /** The refusal a deploy earns, in the shape the deploy routes render. */
88
+ export interface DeployRefusal {
89
+ code: 400 | 413;
90
+ status: 'failure';
91
+ message: string;
92
+ }
93
+ export interface DeployMeasurements {
94
+ entries: number;
95
+ totalBytes: number;
96
+ /** The largest single member, by decoded size, with its path in the tree. */
97
+ largest: {
98
+ path: string;
99
+ bytes: number;
100
+ };
101
+ }
102
+ /**
103
+ * How deep a tree may nest.
104
+ *
105
+ * A deployment is an app: `services/`, `web/`, a folder per service. A few dozen
106
+ * levels is generous, and the number matters because the walk recurses once per
107
+ * level - without a ceiling, a tree nested deeply enough exhausts the stack and
108
+ * throws a `RangeError`, which is neither the `400` nor the `413` this route
109
+ * documents, on input the cap exists to bound.
110
+ */
111
+ export declare const MAX_DEPTH = 32;
112
+ /** The bytes a base64 body decodes to, for a store that writes bytes. */
113
+ export declare const decodeBase64: (value: string) => Uint8Array;
114
+ /**
115
+ * Measures a tree without touching a drive.
116
+ *
117
+ * Every member is counted, whatever its name: this answers "how big is this
118
+ * tree", and a caller that also needs the names checked is `checkDeployTree`.
119
+ */
120
+ export declare const measureDeployTree: (members: DeployMember[]) => DeployMeasurements;
121
+ /**
122
+ * The refusal a file tree earns, or `null` when it can be written.
123
+ *
124
+ * Names are checked first, then the dimensions in a fixed order - entries,
125
+ * total, single file - so a tree breaching two of them is refused for the same
126
+ * one every time, which is what lets a caller key on the message and a test pin
127
+ * it.
128
+ *
129
+ * `limits` defaults to `DEPLOY_LIMITS`; a host may pass its own, stricter set.
130
+ */
131
+ export declare const checkDeployTree: (members: DeployMember[], limits?: DeployLimits) => DeployRefusal | null;
132
+ /**
133
+ * The tree's shape, as the wire format states it.
134
+ *
135
+ * A port of `isFileTree` from `@sasjs/utils`, which is what sasjs/server
136
+ * validates with: a tree is an object with a `members` array, and every member
137
+ * is a folder with its own members, or a service/file with a string `code` and
138
+ * a string `name`. Anything else is the `400` the routes already answer.
139
+ */
140
+ export declare const isDeployTree: (value: unknown) => value is DeployTree;
141
+ /**
142
+ * Deploys a tree into `appLoc`, or refuses without writing.
143
+ *
144
+ * The order is the promise: shape, then containment, then the cap and the names,
145
+ * and only then the first write. `appLoc` is caller-supplied and every segment
146
+ * must stay a segment, so it is resolved against the drive root and refused when
147
+ * it leaves it - joined naively, a `..` would write outside the drive.
148
+ *
149
+ * A write that fails part-way answers the deploy routes' `500`; the tree before
150
+ * the failure is already on the drive, which the message does not hide.
151
+ */
152
+ export declare const deployTree: (store: DriveStore, appLoc: unknown, tree: unknown, limits?: DeployLimits) => Promise<void>;
153
+ //# sourceMappingURL=deploy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deploy.d.ts","sourceRoot":"","sources":["../../../src/drive/deploy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAI7C,sDAAsD;AACtD,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;AAE5D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,EAAE,YAAY,EAAE,CAAA;CACxB;AAED,6EAA6E;AAC7E,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,SAAS,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb;AAED,sDAAsD;AACtD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,YAAY,GACtB,kBAAkB,GAAG,mBAAmB,GAAG,gBAAgB,CAAA;AAE7D,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,YAAY,EAAE,CAAA;CACxB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,eAAO,MAAM,aAAa,EAAE,YAI3B,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,QAAoB,CAAA;AAErD;;;;;;;GAOG;AACH,eAAO,MAAM,cAAc;;;CAGjB,CAAA;AAEV,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,GAAG,GAAG,GAAG,CAAA;IACf,MAAM,EAAE,SAAS,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,6EAA6E;IAC7E,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAA;CACzC;AASD;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,KAAK,CAAA;AAY3B,yEAAyE;AACzE,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,UAmB5C,CAAA;AA8HD;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,SAAS,YAAY,EAAE,KACtB,kBAUF,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAC1B,SAAS,YAAY,EAAE,EACvB,SAAQ,YAA4B,KACnC,aAAa,GAAG,IAoClB,CAAA;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,UAMtD,CAAA;AAkBD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,GACrB,OAAO,UAAU,EACjB,QAAQ,OAAO,EACf,MAAM,OAAO,EACb,SAAQ,YAA4B,KACnC,OAAO,CAAC,IAAI,CAiCd,CAAA"}
@@ -0,0 +1,301 @@
1
+ import { ApiError } from '../types.js';
2
+ import { normaliseDrivePath } from './paths.js';
3
+ export const DEPLOY_LIMITS = {
4
+ entries: 10000,
5
+ totalBytes: 100 * 1024 * 1024,
6
+ fileBytes: 20 * 1024 * 1024
7
+ };
8
+ /**
9
+ * The largest a deploy tree can be on the wire, which is what a host's transport
10
+ * has to admit.
11
+ *
12
+ * The cap is stated in DECODED bytes; the tree travels as JSON in which a file
13
+ * member's contents are base64 - 4/3 of what they write - and a service's are
14
+ * escaped text (~1.04). A tree at `totalBytes` therefore reaches the parser at
15
+ * up to ~134MB, and a transport that admits less refuses first with its own
16
+ * shape, so the cap it was meant to enforce is never consulted. 140MB leaves
17
+ * room for the tree's JSON scaffolding.
18
+ *
19
+ * Only the deploy routes use it: everywhere else a smaller limit applies,
20
+ * because a body is buffered before authentication and nothing else needs the
21
+ * headroom.
22
+ */
23
+ export const DEPLOY_PAYLOAD_BYTES = 140 * 1024 * 1024;
24
+ /**
25
+ * The body a deploy answers success with.
26
+ *
27
+ * The wording is the published contract - a client may key on the message - so
28
+ * it is one constant here rather than a string in each host. It names
29
+ * `@sasjs/server` because that is the document's example, which predates this
30
+ * package; changing it is a document change, not a host's choice.
31
+ */
32
+ export const DEPLOY_SUCCESS = {
33
+ status: 'success',
34
+ message: 'Files deployed successfully to @sasjs/server.'
35
+ };
36
+ /**
37
+ * How deep a tree may nest.
38
+ *
39
+ * A deployment is an app: `services/`, `web/`, a folder per service. A few dozen
40
+ * levels is generous, and the number matters because the walk recurses once per
41
+ * level - without a ceiling, a tree nested deeply enough exhausts the stack and
42
+ * throws a `RangeError`, which is neither the `400` nor the `413` this route
43
+ * documents, on input the cap exists to bound.
44
+ */
45
+ export const MAX_DEPTH = 32;
46
+ const BASE64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
47
+ /** A base64 body without its padding, which is what the arithmetic needs. */
48
+ const base64Body = (value) => value.replace(/[^A-Za-z0-9+/]/g, '');
49
+ /** The bytes a base64 body decodes to, without decoding it. */
50
+ const base64Length = (value) => Math.floor((base64Body(value).length * 3) / 4);
51
+ /** The bytes a base64 body decodes to, for a store that writes bytes. */
52
+ export const decodeBase64 = (value) => {
53
+ const body = base64Body(value);
54
+ const bytes = new Uint8Array(Math.floor((body.length * 3) / 4));
55
+ let buffer = 0;
56
+ let bits = 0;
57
+ let at = 0;
58
+ for (const char of body) {
59
+ buffer = (buffer << 6) | BASE64_ALPHABET.indexOf(char);
60
+ bits += 6;
61
+ if (bits >= 8) {
62
+ bits -= 8;
63
+ bytes[at++] = (buffer >> bits) & 0xff;
64
+ }
65
+ }
66
+ return bytes.subarray(0, at);
67
+ };
68
+ /** The bytes a string occupies as UTF-8, without an encoding API. */
69
+ const utf8Length = (value) => {
70
+ let bytes = 0;
71
+ for (let at = 0; at < value.length; at++) {
72
+ const code = value.charCodeAt(at);
73
+ if (code < 0x80)
74
+ bytes += 1;
75
+ else if (code < 0x800)
76
+ bytes += 2;
77
+ else if (code >= 0xd800 && code <= 0xdbff && at + 1 < value.length) {
78
+ // a surrogate pair is one code point of four bytes
79
+ bytes += 4;
80
+ at++;
81
+ }
82
+ else
83
+ bytes += 3;
84
+ }
85
+ return bytes;
86
+ };
87
+ /** The name a member is written under - a service gains the extension. */
88
+ const memberName = (member) => member.type === 'service' ? `${member.name}.sas` : member.name;
89
+ /** The bytes a member writes. */
90
+ const memberBytes = (member) => member.type === 'file' ? base64Length(member.code) : utf8Length(member.code);
91
+ const MEGABYTE = 1024 * 1024;
92
+ /**
93
+ * A MEASURED size, rounded UP to one decimal in binary megabytes.
94
+ *
95
+ * Up, not to nearest: a tree one byte over a 100 MB cap must not read as "100 MB
96
+ * (maximum 100 MB)", which is a refusal that states no overage at all. Rounding
97
+ * up is the direction that never understates what is being refused.
98
+ *
99
+ * Bytes below a megabyte are printed as bytes, because a limit small enough to
100
+ * be measured in bytes should not read as "0 MB" - which is what a host passing
101
+ * a tighter set would otherwise report.
102
+ */
103
+ const describeSize = (bytes) => bytes < MEGABYTE
104
+ ? `${bytes} bytes`
105
+ : `${Math.ceil((bytes / MEGABYTE) * 10) / 10} MB`;
106
+ /**
107
+ * A LIMIT, printed as it is stated: `DEPLOY_LIMITS` holds exact megabytes, so
108
+ * rounding cannot make it disagree with itself.
109
+ */
110
+ const describeLimit = (bytes) => bytes < MEGABYTE
111
+ ? `${bytes} bytes`
112
+ : `${Math.round((bytes / MEGABYTE) * 10) / 10} MB`;
113
+ /** A walk's starting point. */
114
+ const emptyWalk = () => ({
115
+ entries: 0,
116
+ totalBytes: 0,
117
+ largest: { path: '', bytes: 0 }
118
+ });
119
+ /**
120
+ * Every member name becomes a path SEGMENT under the deployment root, so a name
121
+ * carrying '/', '\' or '..' is a traversal attempt rather than a name.
122
+ */
123
+ const isSafeSegment = (name) => name !== '' &&
124
+ name !== '.' &&
125
+ name !== '..' &&
126
+ !/[/\\]/.test(name) &&
127
+ // a Windows drive or a NUL byte is never a segment either
128
+ !/[\0]/.test(name);
129
+ /**
130
+ * Walks a tree, counting it and noting the first unsafe name.
131
+ *
132
+ * A name problem is RECORDED rather than returned, so the walk always finishes:
133
+ * a caller that only wants the numbers gets the size of the whole tree, and the
134
+ * validator decides whether the name matters. Returning early would report a
135
+ * truncated tree as if it were the real one.
136
+ *
137
+ * The one thing that stops the walk is depth: past `MAX_DEPTH` it does not
138
+ * descend, because descending is the work that would exhaust the stack. The
139
+ * level it reached is recorded so the refusal can state it.
140
+ */
141
+ const walk = (members, parentPath, acc, depth = 1) => {
142
+ for (const member of members) {
143
+ const name = memberName(member);
144
+ const memberPath = parentPath ? `${parentPath}/${name}` : name;
145
+ if (!isSafeSegment(name) && !acc.nameRefusal)
146
+ acc.nameRefusal = {
147
+ code: 400,
148
+ status: 'failure',
149
+ message: `Invalid member name: '${name}'. Member names must be single path segments (no '/', '\\', '..').`
150
+ };
151
+ acc.entries++;
152
+ if (member.type === 'folder') {
153
+ if (depth + 1 > MAX_DEPTH) {
154
+ if (!acc.tooDeep)
155
+ acc.tooDeep = depth + 1;
156
+ continue;
157
+ }
158
+ walk(member.members, memberPath, acc, depth + 1);
159
+ continue;
160
+ }
161
+ const bytes = memberBytes(member);
162
+ acc.totalBytes += bytes;
163
+ if (bytes > acc.largest.bytes)
164
+ acc.largest = { path: memberPath, bytes };
165
+ }
166
+ };
167
+ /**
168
+ * Measures a tree without touching a drive.
169
+ *
170
+ * Every member is counted, whatever its name: this answers "how big is this
171
+ * tree", and a caller that also needs the names checked is `checkDeployTree`.
172
+ */
173
+ export const measureDeployTree = (members) => {
174
+ const acc = emptyWalk();
175
+ walk(members, '', acc);
176
+ return {
177
+ entries: acc.entries,
178
+ totalBytes: acc.totalBytes,
179
+ largest: acc.largest
180
+ };
181
+ };
182
+ /**
183
+ * The refusal a file tree earns, or `null` when it can be written.
184
+ *
185
+ * Names are checked first, then the dimensions in a fixed order - entries,
186
+ * total, single file - so a tree breaching two of them is refused for the same
187
+ * one every time, which is what lets a caller key on the message and a test pin
188
+ * it.
189
+ *
190
+ * `limits` defaults to `DEPLOY_LIMITS`; a host may pass its own, stricter set.
191
+ */
192
+ export const checkDeployTree = (members, limits = DEPLOY_LIMITS) => {
193
+ const measured = emptyWalk();
194
+ walk(members, '', measured);
195
+ if (measured.nameRefusal)
196
+ return measured.nameRefusal;
197
+ if (measured.tooDeep)
198
+ return {
199
+ code: 413,
200
+ status: 'failure',
201
+ message: `The file tree is nested too deeply: ${measured.tooDeep} levels (maximum ${MAX_DEPTH}).`
202
+ };
203
+ if (measured.entries > limits.entries)
204
+ return {
205
+ code: 413,
206
+ status: 'failure',
207
+ message: `The file tree exceeds the deploy limit: ${measured.entries} entries (maximum ${limits.entries}).`
208
+ };
209
+ if (measured.totalBytes > limits.totalBytes)
210
+ return {
211
+ code: 413,
212
+ status: 'failure',
213
+ message: `The file tree exceeds the deploy limit: ${describeSize(measured.totalBytes)} (maximum ${describeLimit(limits.totalBytes)}).`
214
+ };
215
+ if (measured.largest.bytes > limits.fileBytes)
216
+ return {
217
+ code: 413,
218
+ status: 'failure',
219
+ message: `A file in the tree exceeds the deploy limit: ${measured.largest.path} is ${describeSize(measured.largest.bytes)} (maximum ${describeLimit(limits.fileBytes)} per file).`
220
+ };
221
+ return null;
222
+ };
223
+ /**
224
+ * The tree's shape, as the wire format states it.
225
+ *
226
+ * A port of `isFileTree` from `@sasjs/utils`, which is what sasjs/server
227
+ * validates with: a tree is an object with a `members` array, and every member
228
+ * is a folder with its own members, or a service/file with a string `code` and
229
+ * a string `name`. Anything else is the `400` the routes already answer.
230
+ */
231
+ export const isDeployTree = (value) => {
232
+ if (!value || typeof value !== 'object')
233
+ return false;
234
+ const members = value.members;
235
+ return Array.isArray(members) && members.every(isDeployMember);
236
+ };
237
+ const isDeployMember = (value) => {
238
+ if (!value || typeof value !== 'object')
239
+ return false;
240
+ const member = value;
241
+ if (typeof member.name !== 'string')
242
+ return false;
243
+ if (member.type === 'folder')
244
+ return Array.isArray(member.members) && member.members.every(isDeployMember);
245
+ if (member.type === 'service' || member.type === 'file')
246
+ return typeof member.code === 'string';
247
+ return false;
248
+ };
249
+ /**
250
+ * Deploys a tree into `appLoc`, or refuses without writing.
251
+ *
252
+ * The order is the promise: shape, then containment, then the cap and the names,
253
+ * and only then the first write. `appLoc` is caller-supplied and every segment
254
+ * must stay a segment, so it is resolved against the drive root and refused when
255
+ * it leaves it - joined naively, a `..` would write outside the drive.
256
+ *
257
+ * A write that fails part-way answers the deploy routes' `500`; the tree before
258
+ * the failure is already on the drive, which the message does not hide.
259
+ */
260
+ export const deployTree = async (store, appLoc, tree, limits = DEPLOY_LIMITS) => {
261
+ if (!isDeployTree(tree))
262
+ throw new ApiError(400, 'Provided not supported data format.', {
263
+ status: 'failure',
264
+ message: 'Provided not supported data format.'
265
+ });
266
+ const root = normaliseDrivePath(appLoc);
267
+ if (root === null)
268
+ throw new ApiError(400, 'appLoc cannot be outside drive.', {
269
+ status: 'failure',
270
+ message: 'appLoc cannot be outside drive.'
271
+ });
272
+ const refusal = checkDeployTree(tree.members, limits);
273
+ // the body is the envelope WITHOUT the refusal's own `code`: that field is
274
+ // how a host turns the refusal into a status, and it is not sent
275
+ if (refusal)
276
+ throw new ApiError(refusal.code, refusal.message, {
277
+ status: refusal.status,
278
+ message: refusal.message
279
+ });
280
+ try {
281
+ await writeMembers(store, tree.members, root);
282
+ }
283
+ catch {
284
+ throw new ApiError(500, 'Deployment failed!', {
285
+ status: 'failure',
286
+ message: 'Deployment failed!'
287
+ });
288
+ }
289
+ };
290
+ /** Writes every member under `parentPath`, depth first. */
291
+ const writeMembers = async (store, members, parentPath) => {
292
+ for (const member of members) {
293
+ const path = `${parentPath}/${memberName(member)}`;
294
+ if (member.type === 'folder') {
295
+ await store.mkdirp(path);
296
+ await writeMembers(store, member.members, path);
297
+ continue;
298
+ }
299
+ await store.write(path, member.type === 'file' ? decodeBase64(member.code) : member.code);
300
+ }
301
+ };
@@ -37,6 +37,14 @@ export interface DriveService {
37
37
  rmdir(folderPath: string): Promise<DriveResult>;
38
38
  rename(oldPath: string, newPath: string): Promise<DriveResult>;
39
39
  tree(folderPath?: string): Promise<DriveTree>;
40
+ /**
41
+ * Writes a whole tree under `appLoc`, or refuses without writing.
42
+ *
43
+ * `tree` is `unknown` on purpose: the wire payload is the caller's JSON, and
44
+ * the shape is one of the things this decides - a mistyped tree earns the
45
+ * routes' `400` rather than a crash in a host that trusted it.
46
+ */
47
+ deploy(appLoc: string, tree: unknown): Promise<DriveResult>;
40
48
  }
41
49
  export declare const createDriveService: (store: DriveStore) => DriveService;
42
50
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/drive/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAWvD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,SAAS,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;IAC7B,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,SAAS,CAAA;IACjB,IAAI,EAAE,QAAQ,CAAA;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAChD,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC3E,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7E,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC9C,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC9D,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;CAC9C;AAqBD,eAAO,MAAM,kBAAkB,GAAI,OAAO,UAAU,KAAG,YAoJtD,CAAA"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../../src/drive/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAYvD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,SAAS,CAAA;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;IAC7B,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,SAAS,CAAA;IACjB,IAAI,EAAE,QAAQ,CAAA;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAChD,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC3E,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC7E,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC9C,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC/C,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;IAC9D,IAAI,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA;IAC7C;;;;;;OAMG;IACH,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,WAAW,CAAC,CAAA;CAC5D;AAqBD,eAAO,MAAM,kBAAkB,GAAI,OAAO,UAAU,KAAG,YA0JtD,CAAA"}
@@ -1,4 +1,5 @@
1
1
  import { ApiError } from '../types.js';
2
+ import { deployTree } from './deploy.js';
2
3
  import { basename, checkFilePath, dirname, hasBlockedExtension, isRoot, normaliseDrivePath } from './paths.js';
3
4
  /** A path a file operation may act on, or the refusal the operation answers. */
4
5
  const filePath = (input, outside) => {
@@ -142,6 +143,10 @@ export const createDriveService = (store) => {
142
143
  if (!stat.directory)
143
144
  throw new ApiError(400, 'Not a Folder.');
144
145
  return { status: 'success', tree: await treeOf(path) };
146
+ },
147
+ async deploy(appLoc, tree) {
148
+ await deployTree(store, appLoc, tree);
149
+ return { status: 'success' };
145
150
  }
146
151
  };
147
152
  };
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAGnD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;CAC3B;AAsCD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CA8HrB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EAiBlC,CAAA"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../../../src/host/dispatch.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AAInD,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,oFAAoF;IACpF,IAAI,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,GAAG,UAAU,CAAA;KAAE,CAAA;IACtD;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iEAAiE;IACjE,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,mBAAmB;IACnB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAAA;CAC/B;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,YAAY,CAAA;IACnB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,eAAe,CAAA;CAC3B;AAsCD,eAAO,MAAM,QAAQ,GACnB,UAAU,QAAQ,EAClB,SAAS,MAAM,EACf,UAAS,UAAe,KACvB,OAAO,CAAC,WAAW,CAsIrB,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,cAAc,EAAE,MAAM,EAkBlC,CAAA"}
@@ -1,5 +1,6 @@
1
1
  import { routeById } from '../routes/index.js';
2
2
  import { ApiError } from '../types.js';
3
+ import { DEPLOY_SUCCESS } from '../drive/deploy.js';
3
4
  /**
4
5
  * The parameters the table marks required, checked before the handler runs.
5
6
  *
@@ -126,6 +127,12 @@ export const dispatch = async (services, routeId, request = {}) => {
126
127
  status: 200,
127
128
  body: await drive.rename(String(request.body?.oldPath || ''), String(request.body?.newPath || ''))
128
129
  };
130
+ case 'POST /SASjsApi/drive/deploy': {
131
+ // `streamWebFolder` is the host's: publishing an app stream is not a drive
132
+ // operation, and a host that has no app stream ignores it.
133
+ await drive.deploy(String(body.appLoc ?? ''), body.fileTree);
134
+ return { status: 200, body: DEPLOY_SUCCESS };
135
+ }
129
136
  case 'GET /SASjsApi/drive/filetree':
130
137
  return { status: 200, body: await drive.tree(query._folderPath) };
131
138
  default: {
@@ -148,6 +155,7 @@ export const HANDLED_ROUTES = [
148
155
  'DELETE /SASjsApi/drive/folder',
149
156
  'POST /SASjsApi/drive/rename',
150
157
  'GET /SASjsApi/drive/filetree',
158
+ 'POST /SASjsApi/drive/deploy',
151
159
  'POST /SASjsApi/auth/token',
152
160
  'POST /SASjsApi/auth/refresh',
153
161
  'POST /SASjsApi/auth/logout',
@@ -16,6 +16,8 @@
16
16
  export * from './types.js';
17
17
  export { normaliseDrivePath, hasBlockedExtension, dirname, basename, isRoot } from './drive/paths.js';
18
18
  export { createDriveService } from './drive/service.js';
19
+ export { DEPLOY_LIMITS, DEPLOY_PAYLOAD_BYTES, DEPLOY_SUCCESS, checkDeployTree, decodeBase64, deployTree, isDeployTree, measureDeployTree } from './drive/deploy.js';
20
+ export type { DeployLimits, DeployMeasurements, DeployMember, DeployRefusal, DeployTree } from './drive/deploy.js';
19
21
  export type { DriveService, DriveResult, DriveListing, DriveFile, DriveTree } from './drive/service.js';
20
22
  export { MemoryDriveStore } from './drive/memory-store.js';
21
23
  export { ROUTES, routeById, findRoute } from './routes/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAC7D,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,OAAO,EACP,SAAS,EACV,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,cAAc,YAAY,CAAA;AAC1B,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,OAAO,EACP,QAAQ,EACR,MAAM,EACP,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACvD,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EAClB,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,UAAU,EACX,MAAM,mBAAmB,CAAA;AAC1B,YAAY,EACV,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,SAAS,EACT,SAAS,EACV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAA;AAC1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAA;AAChE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACvD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAC7D,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,YAAY,EACV,eAAe,EACf,WAAW,EACX,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,aAAa,EACd,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,uBAAuB,CAAA;AAC9B,YAAY,EACV,cAAc,EACd,aAAa,EACb,YAAY,EACZ,OAAO,EACP,SAAS,EACV,MAAM,qBAAqB,CAAA;AAC5B,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAA;AACtD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AACzD,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA"}
@@ -16,6 +16,7 @@
16
16
  export * from './types.js';
17
17
  export { normaliseDrivePath, hasBlockedExtension, dirname, basename, isRoot } from './drive/paths.js';
18
18
  export { createDriveService } from './drive/service.js';
19
+ export { DEPLOY_LIMITS, DEPLOY_PAYLOAD_BYTES, DEPLOY_SUCCESS, checkDeployTree, decodeBase64, deployTree, isDeployTree, measureDeployTree } from './drive/deploy.js';
19
20
  export { MemoryDriveStore } from './drive/memory-store.js';
20
21
  export { ROUTES, routeById, findRoute } from './routes/index.js';
21
22
  export { OVERLAY, TAG_CAPABILITIES } from './routes/overlay.js';
@@ -1 +1 @@
1
- {"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../../src/routes/generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,eAAO,MAAM,gBAAgB,EAAE,cAAc,EAirD5C,CAAA"}
1
+ {"version":3,"file":"generated.d.ts","sourceRoot":"","sources":["../../../src/routes/generated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEjD,eAAO,MAAM,gBAAgB,EAAE,cAAc,EAyrD5C,CAAA"}
@@ -469,6 +469,10 @@ export const GENERATED_ROUTES = [
469
469
  status: 400,
470
470
  description: 'Invalid Format'
471
471
  },
472
+ {
473
+ status: 413,
474
+ description: 'File tree exceeds the deploy limit'
475
+ },
472
476
  {
473
477
  status: 500,
474
478
  description: 'Execution Error'
@@ -499,6 +503,10 @@ export const GENERATED_ROUTES = [
499
503
  status: 400,
500
504
  description: 'Invalid Format'
501
505
  },
506
+ {
507
+ status: 413,
508
+ description: 'File tree exceeds the deploy limit'
509
+ },
502
510
  {
503
511
  status: 500,
504
512
  description: 'Execution Error'
@@ -1 +1 @@
1
- {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAEzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AAiDD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAkNhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../src/routes/overlay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAGzD,yDAAyD;AACzD,UAAU,aAAa;IACrB,YAAY,CAAC,EAAE,UAAU,EAAE,CAAA;IAC3B;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,aAAa,GACtC,CAAC;IAAE,WAAW,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,WAAW,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAElE,kFAAkF;AAClF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAczD,CAAA;AAiDD,eAAO,MAAM,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAmOhD,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,OAAO,EAAE,UAAU,EAA8C,CAAA"}
@@ -1,3 +1,4 @@
1
+ import { DEPLOY_SUCCESS } from '../drive/deploy.js';
1
2
  /** The default capability for each tag, overridden per route where it differs. */
2
3
  export const TAG_CAPABILITIES = {
3
4
  Drive: ['storage'],
@@ -29,7 +30,7 @@ const HOST_CONFIG = "The body is the host's own configuration - ports, run times
29
30
  const PERMISSIONS = "The route inventory is the host's permission model, which this package does not hold.";
30
31
  const HOST_OWNED = "The host owns this report: it describes the host's own process and files, which a browser build has no equivalent of.";
31
32
  const WEB_BUNDLE = 'The host serves its own web bundle; there is no API decision here for this package to answer.';
32
- const DEPLOY_CAP = "Writing a whole tree in one request can exhaust the host's storage, so the deploy routes land with the size cap that bounds it.";
33
+ const ARCHIVE = "Expanding the uploaded archive is the host's transport work - multer and a zip library on the server, `DecompressionStream` in a worker - and the host hands this package the tree it produces, which is the same tree `POST /SASjsApi/drive/deploy` takes.";
33
34
  const UNPUBLISH = 'Unpublishing inverts the deploy route, so the two land together.';
34
35
  const success = { status: 'success' };
35
36
  const user = {
@@ -132,10 +133,27 @@ export const OVERLAY = {
132
133
  implemented: true,
133
134
  responses: { 200: { status: 'success', tree: {} } }
134
135
  },
135
- 'POST /SASjsApi/drive/deploy': { implemented: false, reason: DEPLOY_CAP },
136
+ // The deploy routes answer an ENVELOPE, unlike the file routes above: their
137
+ // `400` is `{status:'failure', message}` rather than the message as text, and
138
+ // `413` is the cap refusing the tree before anything is written.
139
+ // `"appLoc" is required` and `"fileTree" is required` are the other `400` this
140
+ // route sends - a missing body property, worded by the table's own
141
+ // validation - so the body annotated here is the substantial one.
142
+ 'POST /SASjsApi/drive/deploy': {
143
+ implemented: true,
144
+ responses: {
145
+ 200: DEPLOY_SUCCESS,
146
+ 400: {
147
+ status: 'failure',
148
+ message: 'Provided not supported data format.'
149
+ },
150
+ 413: 'The file tree exceeds the deploy limit.',
151
+ 500: { status: 'failure', message: 'Deployment failed!' }
152
+ }
153
+ },
136
154
  'POST /SASjsApi/drive/deploy/upload': {
137
155
  implemented: false,
138
- reason: DEPLOY_CAP
156
+ reason: ARCHIVE
139
157
  },
140
158
  // `404` is a pair - the registry name and the appLoc it was published from -
141
159
  // that matches no entry, and the drive's content is deliberately left alone.