lambder 8.1.1 → 8.1.2
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/CHANGELOG.md +28 -0
- package/dist/build/ContractTypePrinter.d.ts +39 -9
- package/dist/build/ContractTypePrinter.js +89 -31
- package/dist/build/writeApiContract.d.ts +2 -1
- package/dist/build/writeApiContract.js +2 -1
- package/dist/client/LambderUploadRunner.d.ts +4 -4
- package/dist/client/LambderUploadRunner.js +12 -12
- package/dist/shared/util/escapeXmlText.d.ts +8 -0
- package/dist/shared/util/escapeXmlText.js +8 -0
- package/dist/shared/wire/LambderUploadObjectFields.js +2 -2
- package/dist/stores/LambderMemoryUploadBucket.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,34 @@ sit on its first published patch, and later patches list only what they changed.
|
|
|
9
9
|
Releases up to 3.2.6 carry git tags; the ones after it were published without
|
|
10
10
|
one, so versions are not cross-linked to tag comparisons here.
|
|
11
11
|
|
|
12
|
+
## [8.1.2] - 2026-09-26
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **`writeApiContract` names no longer follow the order APIs are registered
|
|
17
|
+
in.** Two types that want one name (an interface `Row` in two modules) were
|
|
18
|
+
told apart by a number given in the order the printer met them, so
|
|
19
|
+
reordering registrations, or adding an API that reaches one of them first,
|
|
20
|
+
swapped `Row` and `Row2` and reported every API using either as changed.
|
|
21
|
+
The printer now finds every declaration first and numbers them by where
|
|
22
|
+
each is declared, the one declared first (by file, then position) keeping
|
|
23
|
+
the name, and a number never takes the name another type is declared
|
|
24
|
+
under. A file already written may be renamed once, the first time it is
|
|
25
|
+
written with this version.
|
|
26
|
+
- **`LambderUploadRunner` randomises its first wait too.** The ceiling of the
|
|
27
|
+
first wait before trying storage again was the base itself, so browsers
|
|
28
|
+
dropped together all came back exactly `baseDelayMs` later. Each wait is
|
|
29
|
+
now between `baseDelayMs` and twice it, doubling with every failed attempt
|
|
30
|
+
up to `maxDelayMs`.
|
|
31
|
+
- **`LambderUploadRunner` renews a ticket on `ExpiredToken`,** which S3
|
|
32
|
+
answers when the temporary credentials that signed the ticket ran out
|
|
33
|
+
before the ticket did. It is renewed like an expired ticket, at once and
|
|
34
|
+
spending no attempt; before, the upload failed as `storageRejected`.
|
|
35
|
+
- **`LambderUploadRunner` no longer holds the file's bytes through the
|
|
36
|
+
upload.** The buffer it hashes, as large as the file, was kept in a local
|
|
37
|
+
for the whole post and every retry; it is now read straight into the
|
|
38
|
+
digest.
|
|
39
|
+
|
|
12
40
|
## [8.1.1] - 2026-09-26
|
|
13
41
|
|
|
14
42
|
Two additions, and three breaking changes a minor line carries here on
|
|
@@ -22,18 +22,40 @@ export declare class ContractTypePrinter {
|
|
|
22
22
|
private readonly program;
|
|
23
23
|
private readonly checker;
|
|
24
24
|
private readonly style;
|
|
25
|
-
private
|
|
26
|
-
/** Every
|
|
27
|
-
private
|
|
25
|
+
private failures;
|
|
26
|
+
/** Every declaration by the type it stands for. */
|
|
27
|
+
private declarations;
|
|
28
|
+
/** The name each declaration is printed under, settled from all of them; empty on the pass that finds them. */
|
|
29
|
+
private settledNames;
|
|
28
30
|
/** Names a declaration may not take: the default library's, and the contract's own. */
|
|
29
|
-
private readonly
|
|
31
|
+
private readonly reservedNames;
|
|
30
32
|
/** The anonymous types being printed: meeting one again inside itself is recursion, and it needs a name. */
|
|
31
33
|
private readonly inProgress;
|
|
32
34
|
/** Under exactOptionalPropertyTypes an optional member's type carries the compiler's own "missing" undefined, which its source never wrote. */
|
|
33
35
|
private readonly exactOptionalProperties;
|
|
34
36
|
constructor(ts: typeof import("typescript"), program: ts.Program, checker: ts.TypeChecker, style: ContractPrintStyle, contractName: string);
|
|
35
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Prints each member of the contract type, sorted by name, and every
|
|
39
|
+
* declaration they refer to.
|
|
40
|
+
*
|
|
41
|
+
* In two passes: the first finds every type that needs a declaration,
|
|
42
|
+
* and the second prints with their names settled from all of them. Named
|
|
43
|
+
* as the printer met them, two types wanting one name would trade it
|
|
44
|
+
* whenever the APIs were registered in another order, or a union's
|
|
45
|
+
* members created in another, and the file would move with no API
|
|
46
|
+
* changed.
|
|
47
|
+
*/
|
|
36
48
|
printContract(contract: ts.Type): PrintedContract;
|
|
49
|
+
/**
|
|
50
|
+
* A name for every declaration the first pass found. Of the types that
|
|
51
|
+
* want one name, the one declared first (by file, then position) keeps
|
|
52
|
+
* it, and the others take the lowest free number after it once every
|
|
53
|
+
* type has claimed its own name, so a number never takes the name
|
|
54
|
+
* another type is declared under. Types declared at one place (two
|
|
55
|
+
* instantiations of a generic) keep the order the entries reached them
|
|
56
|
+
* in, by entry name.
|
|
57
|
+
*/
|
|
58
|
+
private settleNames;
|
|
37
59
|
/**
|
|
38
60
|
* `label value`, with a union too long for one line starting on the next
|
|
39
61
|
* line, one member per line: what a property, an index signature and a
|
|
@@ -43,16 +65,25 @@ export declare class ContractTypePrinter {
|
|
|
43
65
|
private print;
|
|
44
66
|
/** A union, an intersection or an object: printed in place, or as a reference to a declaration of its own. */
|
|
45
67
|
private printComposite;
|
|
46
|
-
/**
|
|
47
|
-
private
|
|
68
|
+
/** A type's declaration, under the name it is settled to once the first pass has settled them. */
|
|
69
|
+
private declare;
|
|
70
|
+
/** The name a type is declared under and where, when it is one to print as a declaration: non-generic, and not the default library's. */
|
|
71
|
+
private ownDeclarationOf;
|
|
72
|
+
/** A symbol's name to declare a type under, and where the symbol is declared. */
|
|
73
|
+
private declaredAs;
|
|
74
|
+
/** Where a symbol is declared, as text that orders by file and then position; empty for one declared nowhere. */
|
|
75
|
+
private originOf;
|
|
48
76
|
/** The name a symbol is declared under, read off its declaration, so `export default interface Customer` is Customer; undefined when it has none to print. */
|
|
49
77
|
private nameOf;
|
|
50
78
|
/**
|
|
51
79
|
* A name for a type that refers to itself and has none of its own: what it
|
|
52
80
|
* instantiates followed by its arguments (a JSON mapping of a Tree is
|
|
53
81
|
* `JsonOfTree`, a `Tree<string>` is `TreeString`), or `RecursiveType`.
|
|
82
|
+
* Its origin is where what it instantiates is declared, then where each
|
|
83
|
+
* argument is, so two instantiations named alike are told apart by their
|
|
84
|
+
* arguments.
|
|
54
85
|
*/
|
|
55
|
-
private
|
|
86
|
+
private recursiveDeclarationOf;
|
|
56
87
|
private printStructure;
|
|
57
88
|
private printUnion;
|
|
58
89
|
private printUnionOf;
|
|
@@ -80,6 +111,5 @@ export declare class ContractTypePrinter {
|
|
|
80
111
|
private wrapped;
|
|
81
112
|
keyOf(name: string): string;
|
|
82
113
|
private quoted;
|
|
83
|
-
private takeName;
|
|
84
114
|
private fail;
|
|
85
115
|
}
|
|
@@ -37,10 +37,12 @@ export class ContractTypePrinter {
|
|
|
37
37
|
checker;
|
|
38
38
|
style;
|
|
39
39
|
failures = [];
|
|
40
|
-
/** Every
|
|
40
|
+
/** Every declaration by the type it stands for. */
|
|
41
41
|
declarations = new Map();
|
|
42
|
+
/** The name each declaration is printed under, settled from all of them; empty on the pass that finds them. */
|
|
43
|
+
settledNames = new Map();
|
|
42
44
|
/** Names a declaration may not take: the default library's, and the contract's own. */
|
|
43
|
-
|
|
45
|
+
reservedNames = new Set();
|
|
44
46
|
/** The anonymous types being printed: meeting one again inside itself is recursion, and it needs a name. */
|
|
45
47
|
inProgress = new Set();
|
|
46
48
|
/** Under exactOptionalPropertyTypes an optional member's type carries the compiler's own "missing" undefined, which its source never wrote. */
|
|
@@ -50,7 +52,7 @@ export class ContractTypePrinter {
|
|
|
50
52
|
this.program = program;
|
|
51
53
|
this.checker = checker;
|
|
52
54
|
this.style = style;
|
|
53
|
-
this.
|
|
55
|
+
this.reservedNames.add(contractName);
|
|
54
56
|
this.exactOptionalProperties = !!program.getCompilerOptions().exactOptionalPropertyTypes;
|
|
55
57
|
// A declaration named after a global the printed text refers to by
|
|
56
58
|
// name (Date) would shadow it.
|
|
@@ -60,20 +62,65 @@ export class ContractTypePrinter {
|
|
|
60
62
|
for (const statement of sourceFile.statements) {
|
|
61
63
|
if ((ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isClassDeclaration(statement) || ts.isModuleDeclaration(statement))
|
|
62
64
|
&& statement.name && ts.isIdentifier(statement.name))
|
|
63
|
-
this.
|
|
65
|
+
this.reservedNames.add(statement.name.text);
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
}
|
|
67
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Prints each member of the contract type, sorted by name, and every
|
|
71
|
+
* declaration they refer to.
|
|
72
|
+
*
|
|
73
|
+
* In two passes: the first finds every type that needs a declaration,
|
|
74
|
+
* and the second prints with their names settled from all of them. Named
|
|
75
|
+
* as the printer met them, two types wanting one name would trade it
|
|
76
|
+
* whenever the APIs were registered in another order, or a union's
|
|
77
|
+
* members created in another, and the file would move with no API
|
|
78
|
+
* changed.
|
|
79
|
+
*/
|
|
68
80
|
printContract(contract) {
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
81
|
+
const members = [...this.checker.getPropertiesOfType(contract)].sort((a, b) => byCodeUnits(a.name, b.name));
|
|
82
|
+
const printEntries = () => members.map((entry) => ({ name: entry.name, text: this.print(this.checker.getTypeOfSymbol(entry), this.keyOf(entry.name)).text }));
|
|
83
|
+
printEntries();
|
|
84
|
+
this.settledNames = this.settleNames();
|
|
85
|
+
this.failures = [];
|
|
86
|
+
this.declarations = new Map();
|
|
87
|
+
const entries = printEntries();
|
|
72
88
|
const declarations = [...this.declarations.values()]
|
|
73
89
|
.map(({ name, text }) => ({ name, text: text ?? "never" }))
|
|
74
90
|
.sort((a, b) => byCodeUnits(a.name, b.name));
|
|
75
91
|
return { entries, declarations, failures: this.failures };
|
|
76
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* A name for every declaration the first pass found. Of the types that
|
|
95
|
+
* want one name, the one declared first (by file, then position) keeps
|
|
96
|
+
* it, and the others take the lowest free number after it once every
|
|
97
|
+
* type has claimed its own name, so a number never takes the name
|
|
98
|
+
* another type is declared under. Types declared at one place (two
|
|
99
|
+
* instantiations of a generic) keep the order the entries reached them
|
|
100
|
+
* in, by entry name.
|
|
101
|
+
*/
|
|
102
|
+
settleNames() {
|
|
103
|
+
const wanting = [...this.declarations].sort(([, a], [, b]) => byCodeUnits(a.base, b.base) || byCodeUnits(a.origin, b.origin));
|
|
104
|
+
const taken = new Set(this.reservedNames);
|
|
105
|
+
const settled = new Map();
|
|
106
|
+
const numbered = [];
|
|
107
|
+
for (const [type, { base }] of wanting) {
|
|
108
|
+
if (taken.has(base)) {
|
|
109
|
+
numbered.push([type, base]);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
taken.add(base);
|
|
113
|
+
settled.set(type, base);
|
|
114
|
+
}
|
|
115
|
+
for (const [type, base] of numbered) {
|
|
116
|
+
let suffix = 2;
|
|
117
|
+
while (taken.has(`${base}${suffix}`))
|
|
118
|
+
suffix++;
|
|
119
|
+
taken.add(`${base}${suffix}`);
|
|
120
|
+
settled.set(type, `${base}${suffix}`);
|
|
121
|
+
}
|
|
122
|
+
return settled;
|
|
123
|
+
}
|
|
77
124
|
/**
|
|
78
125
|
* `label value`, with a union too long for one line starting on the next
|
|
79
126
|
* line, one member per line: what a property, an index signature and a
|
|
@@ -142,20 +189,16 @@ export class ContractTypePrinter {
|
|
|
142
189
|
const known = this.declarations.get(type);
|
|
143
190
|
if (known)
|
|
144
191
|
return atom(known.name);
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
192
|
+
const own = this.ownDeclarationOf(type);
|
|
193
|
+
if (own) {
|
|
147
194
|
// Registered before the body is printed, so a reference to itself
|
|
148
195
|
// inside the body finds the name.
|
|
149
|
-
const declaration =
|
|
150
|
-
this.declarations.set(type, declaration);
|
|
196
|
+
const declaration = this.declare(type, own);
|
|
151
197
|
declaration.text = this.printStructure(type, path).text;
|
|
152
198
|
return atom(declaration.name);
|
|
153
199
|
}
|
|
154
|
-
if (this.inProgress.has(type))
|
|
155
|
-
|
|
156
|
-
this.declarations.set(type, declaration);
|
|
157
|
-
return atom(declaration.name);
|
|
158
|
-
}
|
|
200
|
+
if (this.inProgress.has(type))
|
|
201
|
+
return atom(this.declare(type, this.recursiveDeclarationOf(type)).name);
|
|
159
202
|
this.inProgress.add(type);
|
|
160
203
|
const printed = this.printStructure(type, path);
|
|
161
204
|
this.inProgress.delete(type);
|
|
@@ -166,11 +209,17 @@ export class ContractTypePrinter {
|
|
|
166
209
|
recursive.text = printed.text;
|
|
167
210
|
return atom(recursive.name);
|
|
168
211
|
}
|
|
169
|
-
/**
|
|
170
|
-
|
|
212
|
+
/** A type's declaration, under the name it is settled to once the first pass has settled them. */
|
|
213
|
+
declare(type, { base, origin }) {
|
|
214
|
+
const declaration = { base, origin, name: this.settledNames.get(type) ?? base, text: null };
|
|
215
|
+
this.declarations.set(type, declaration);
|
|
216
|
+
return declaration;
|
|
217
|
+
}
|
|
218
|
+
/** The name a type is declared under and where, when it is one to print as a declaration: non-generic, and not the default library's. */
|
|
219
|
+
ownDeclarationOf(type) {
|
|
171
220
|
const { ObjectFlags, TypeFlags } = this.ts;
|
|
172
221
|
if (type.aliasSymbol) {
|
|
173
|
-
return type.aliasTypeArguments?.length || this.isDefaultLibrary(type.aliasSymbol) ? undefined : this.
|
|
222
|
+
return type.aliasTypeArguments?.length || this.isDefaultLibrary(type.aliasSymbol) ? undefined : this.declaredAs(type.aliasSymbol);
|
|
174
223
|
}
|
|
175
224
|
if (!(type.flags & TypeFlags.Object))
|
|
176
225
|
return undefined;
|
|
@@ -182,7 +231,17 @@ export class ContractTypePrinter {
|
|
|
182
231
|
// it instead, and are printed in place.
|
|
183
232
|
if (objectFlags & ObjectFlags.Reference && (type.target !== type || type.typeParameters?.length))
|
|
184
233
|
return undefined;
|
|
185
|
-
return this.isDefaultLibrary(type.symbol) ? undefined : this.
|
|
234
|
+
return this.isDefaultLibrary(type.symbol) ? undefined : this.declaredAs(type.symbol);
|
|
235
|
+
}
|
|
236
|
+
/** A symbol's name to declare a type under, and where the symbol is declared. */
|
|
237
|
+
declaredAs(symbol) {
|
|
238
|
+
const base = this.nameOf(symbol);
|
|
239
|
+
return base === undefined ? undefined : { base, origin: this.originOf(symbol) };
|
|
240
|
+
}
|
|
241
|
+
/** Where a symbol is declared, as text that orders by file and then position; empty for one declared nowhere. */
|
|
242
|
+
originOf(symbol) {
|
|
243
|
+
const declaration = symbol?.declarations?.[0];
|
|
244
|
+
return declaration ? `${declaration.getSourceFile().fileName}\0${String(declaration.pos).padStart(10, "0")}` : "";
|
|
186
245
|
}
|
|
187
246
|
/** The name a symbol is declared under, read off its declaration, so `export default interface Customer` is Customer; undefined when it has none to print. */
|
|
188
247
|
nameOf(symbol) {
|
|
@@ -195,8 +254,11 @@ export class ContractTypePrinter {
|
|
|
195
254
|
* A name for a type that refers to itself and has none of its own: what it
|
|
196
255
|
* instantiates followed by its arguments (a JSON mapping of a Tree is
|
|
197
256
|
* `JsonOfTree`, a `Tree<string>` is `TreeString`), or `RecursiveType`.
|
|
257
|
+
* Its origin is where what it instantiates is declared, then where each
|
|
258
|
+
* argument is, so two instantiations named alike are told apart by their
|
|
259
|
+
* arguments.
|
|
198
260
|
*/
|
|
199
|
-
|
|
261
|
+
recursiveDeclarationOf(type) {
|
|
200
262
|
const { ObjectFlags, TypeFlags } = this.ts;
|
|
201
263
|
let instantiated;
|
|
202
264
|
if (type.aliasSymbol) {
|
|
@@ -209,11 +271,14 @@ export class ContractTypePrinter {
|
|
|
209
271
|
}
|
|
210
272
|
const base = instantiated && this.nameOf(instantiated.symbol);
|
|
211
273
|
if (!instantiated || !base)
|
|
212
|
-
return "RecursiveType";
|
|
274
|
+
return { base: "RecursiveType", origin: this.originOf(type.symbol) };
|
|
213
275
|
const argumentNames = instantiated.typeArguments
|
|
214
276
|
.map((argument) => (argument.aliasSymbol && this.nameOf(argument.aliasSymbol)) ?? (argument.symbol && this.nameOf(argument.symbol)) ?? this.checker.typeToString(argument))
|
|
215
277
|
.filter((name) => IDENTIFIER.test(name));
|
|
216
|
-
return
|
|
278
|
+
return {
|
|
279
|
+
base: `${base}${argumentNames.map((name) => name[0].toUpperCase() + name.slice(1)).join("")}`,
|
|
280
|
+
origin: [instantiated.symbol, ...instantiated.typeArguments.map((argument) => argument.aliasSymbol ?? argument.symbol)].map((symbol) => this.originOf(symbol)).join("\n"),
|
|
281
|
+
};
|
|
217
282
|
}
|
|
218
283
|
printStructure(type, path) {
|
|
219
284
|
const { TypeFlags } = this.ts;
|
|
@@ -388,13 +453,6 @@ export class ContractTypePrinter {
|
|
|
388
453
|
// never the tail of an escaped backslash.
|
|
389
454
|
return `'${doubleQuoted.slice(1, -1).replace(/\\"/g, "\"").replace(/'/g, "\\'")}'`;
|
|
390
455
|
}
|
|
391
|
-
takeName(base) {
|
|
392
|
-
let name = base;
|
|
393
|
-
for (let suffix = 2; this.takenNames.has(name); suffix++)
|
|
394
|
-
name = `${base}${suffix}`;
|
|
395
|
-
this.takenNames.add(name);
|
|
396
|
-
return name;
|
|
397
|
-
}
|
|
398
456
|
fail(path, reason) {
|
|
399
457
|
this.failures.push(`${path}: ${reason}`);
|
|
400
458
|
return atom("unknown");
|
|
@@ -47,7 +47,8 @@ export type LambderApiContractFileResult = {
|
|
|
47
47
|
* written module imports nothing, not even lambder, and exports one type
|
|
48
48
|
* alias, `typeName`. Only the default library's interfaces (Date) are printed
|
|
49
49
|
* by name. A non-generic named type is printed once, as a declaration of its
|
|
50
|
-
* own that the entries refer to
|
|
50
|
+
* own that the entries refer to; two that want one name are numbered by where
|
|
51
|
+
* each is declared, never by the order the APIs were registered in.
|
|
51
52
|
*
|
|
52
53
|
* Anything with no plain form fails the call and names where it sits: a
|
|
53
54
|
* function, a symbol-keyed property, an enum, a class's private member, or a
|
|
@@ -24,7 +24,8 @@ const DIAGNOSTIC_LIMIT = 20;
|
|
|
24
24
|
* written module imports nothing, not even lambder, and exports one type
|
|
25
25
|
* alias, `typeName`. Only the default library's interfaces (Date) are printed
|
|
26
26
|
* by name. A non-generic named type is printed once, as a declaration of its
|
|
27
|
-
* own that the entries refer to
|
|
27
|
+
* own that the entries refer to; two that want one name are numbered by where
|
|
28
|
+
* each is declared, never by the order the APIs were registered in.
|
|
28
29
|
*
|
|
29
30
|
* Anything with no plain form fails the call and names where it sits: a
|
|
30
31
|
* function, a symbol-keyed property, an enum, a class's private member, or a
|
|
@@ -48,10 +48,10 @@ export type LambderUploadRunnerOptions<Reference, Receipt> = {
|
|
|
48
48
|
/**
|
|
49
49
|
* How storage is tried again when it cannot be reached, stalls, or answers
|
|
50
50
|
* a failure a retry can cure (a 5xx, RequestTimeout, SlowDown). Each wait
|
|
51
|
-
* is a random time between `baseDelayMs` and a ceiling
|
|
52
|
-
* every failed attempt
|
|
53
|
-
* together do not come back in step
|
|
54
|
-
* second to 15.
|
|
51
|
+
* is a random time between `baseDelayMs` and a ceiling of twice that,
|
|
52
|
+
* doubling with every failed attempt and never past `maxDelayMs`, so many
|
|
53
|
+
* browsers dropped together do not come back in step, not even the first
|
|
54
|
+
* time. Default: 4 attempts, waits from one second to 15.
|
|
55
55
|
*/
|
|
56
56
|
storageRetry?: {
|
|
57
57
|
attempts?: number;
|
|
@@ -59,18 +59,16 @@ export class LambderUploadRunner {
|
|
|
59
59
|
};
|
|
60
60
|
stopIfCancelled();
|
|
61
61
|
report("hashing");
|
|
62
|
-
let bytes;
|
|
63
|
-
try {
|
|
64
|
-
bytes = new Uint8Array(await file.arrayBuffer());
|
|
65
|
-
}
|
|
66
|
-
catch (cause) {
|
|
67
|
-
throw new LambderUploadError("fileUnreadable", { cause });
|
|
68
|
-
}
|
|
69
62
|
const fileFacts = {
|
|
70
63
|
fileName: file.name,
|
|
71
64
|
mimeType: file.type,
|
|
72
65
|
byteSize: file.size,
|
|
73
|
-
|
|
66
|
+
// Read straight into the digest, never into a variable: the buffer
|
|
67
|
+
// is the size of the file, and a local would keep it through
|
|
68
|
+
// every await of the upload that follows.
|
|
69
|
+
sha256Base64: await sha256Base64Of(new Uint8Array(await file.arrayBuffer().catch((cause) => {
|
|
70
|
+
throw new LambderUploadError("fileUnreadable", { cause });
|
|
71
|
+
}))),
|
|
74
72
|
};
|
|
75
73
|
stopIfCancelled();
|
|
76
74
|
const requestTicket = async () => {
|
|
@@ -120,7 +118,7 @@ export class LambderUploadRunner {
|
|
|
120
118
|
await this.options.discardUpload?.(receipt);
|
|
121
119
|
}
|
|
122
120
|
waitBeforeRetry(failedAttempts, signal) {
|
|
123
|
-
const ceiling = Math.max(this.baseDelayMs, Math.min(this.baseDelayMs * 2 **
|
|
121
|
+
const ceiling = Math.max(this.baseDelayMs, Math.min(this.baseDelayMs * 2 ** failedAttempts, this.maxDelayMs));
|
|
124
122
|
return new Promise((resolve, reject) => {
|
|
125
123
|
if (signal?.aborted)
|
|
126
124
|
return reject(new LambderUploadError("cancelled"));
|
|
@@ -218,8 +216,10 @@ const xmlText = (text) => text.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (entity, na
|
|
|
218
216
|
/**
|
|
219
217
|
* Storage explains a refusal in XML, `<Error><Code/><Message/></Error>`,
|
|
220
218
|
* read here without a DOM so the runner works wherever fetch does. A
|
|
221
|
-
* transient code is tried again like a dropped connection
|
|
222
|
-
*
|
|
219
|
+
* transient code is tried again like a dropped connection. An expired ticket
|
|
220
|
+
* is the one refusal a new ticket cures, and so is ExpiredToken: the
|
|
221
|
+
* temporary credentials that signed the ticket ran out before it did, and
|
|
222
|
+
* the next one is signed with fresh ones.
|
|
223
223
|
*/
|
|
224
224
|
const rejectedOutcome = (status, body) => {
|
|
225
225
|
const code = xmlText(/<Code>([^<]*)<\/Code>/.exec(body)?.[1] ?? "") || `HTTP ${status}`;
|
|
@@ -228,7 +228,7 @@ const rejectedOutcome = (status, body) => {
|
|
|
228
228
|
return { kind: "unreachable" };
|
|
229
229
|
return {
|
|
230
230
|
kind: "rejected",
|
|
231
|
-
ticketExpired: code === "AccessDenied" && /expired/i.test(message),
|
|
231
|
+
ticketExpired: code === "ExpiredToken" || (code === "AccessDenied" && /expired/i.test(message)),
|
|
232
232
|
detail: message ? `${code}: ${message}` : code,
|
|
233
233
|
};
|
|
234
234
|
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text made safe to place inside an XML element or a quoted attribute: the
|
|
3
|
+
* five characters XML gives meaning to become numeric references. For the
|
|
4
|
+
* small documents storage speaks, a tag set a ticket carries and an error a
|
|
5
|
+
* memory bucket answers with, which LambderHtml's `xml` template would also
|
|
6
|
+
* escape but no module in wire/ may import.
|
|
7
|
+
*/
|
|
8
|
+
export declare const escapeXmlText: (text: string) => string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text made safe to place inside an XML element or a quoted attribute: the
|
|
3
|
+
* five characters XML gives meaning to become numeric references. For the
|
|
4
|
+
* small documents storage speaks, a tag set a ticket carries and an error a
|
|
5
|
+
* memory bucket answers with, which LambderHtml's `xml` template would also
|
|
6
|
+
* escape but no module in wire/ may import.
|
|
7
|
+
*/
|
|
8
|
+
export const escapeXmlText = (text) => text.replace(/[<>&'"]/g, (character) => `&#${character.charCodeAt(0)};`);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { contentDispositionHeader } from "../util/LambderContentDisposition.js";
|
|
2
|
-
|
|
2
|
+
import { escapeXmlText } from "../util/escapeXmlText.js";
|
|
3
3
|
/**
|
|
4
4
|
* What a ticket's form carries for the stored object, in the fields S3's
|
|
5
5
|
* presigned POST reads them from: the tag set as the XML `tagging` field,
|
|
@@ -12,7 +12,7 @@ export const uploadObjectFormFields = (object) => {
|
|
|
12
12
|
const fields = {};
|
|
13
13
|
const tags = Object.entries(object?.tags ?? {});
|
|
14
14
|
if (tags.length) {
|
|
15
|
-
fields.tagging = `<Tagging><TagSet>${tags.map(([key, value]) => `<Tag><Key>${
|
|
15
|
+
fields.tagging = `<Tagging><TagSet>${tags.map(([key, value]) => `<Tag><Key>${escapeXmlText(key)}</Key><Value>${escapeXmlText(value)}</Value></Tag>`).join("")}</TagSet></Tagging>`;
|
|
16
16
|
}
|
|
17
17
|
for (const [name, value] of Object.entries(object?.metadata ?? {}))
|
|
18
18
|
fields[`x-amz-meta-${name.toLowerCase()}`] = value;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { assertObjectOptions, assertPinnedObjectKey, assertSignatureLifetime, } from "../shared/contracts/LambderUploadBucket.js";
|
|
2
2
|
import { contentDispositionHeader } from "../shared/util/LambderContentDisposition.js";
|
|
3
|
+
import { escapeXmlText } from "../shared/util/escapeXmlText.js";
|
|
3
4
|
import { uploadObjectFormFields } from "../shared/wire/LambderUploadObjectFields.js";
|
|
4
5
|
import { refuseUnacceptedUpload } from "../shared/wire/LambderUploadRefusal.js";
|
|
5
6
|
import { sha256Base64Of } from "../shared/util/LambderTextDigest.js";
|
|
@@ -214,6 +215,5 @@ export class LambderMemoryUploadBucket {
|
|
|
214
215
|
/** A key as a URL path: each segment escaped, the slashes kept, as S3 addresses an object. */
|
|
215
216
|
const encodeObjectKey = (objectKey) => objectKey.split("/").map(encodeURIComponent).join("/");
|
|
216
217
|
const decodeObjectKey = (path) => path.split("/").map(decodeURIComponent).join("/");
|
|
217
|
-
const escapeXml = (text) => text.replace(/[<>&'"]/g, (character) => `&#${character.charCodeAt(0)};`);
|
|
218
218
|
/** An error the way S3 writes one: the status, and `<Error><Code/><Message/></Error>` as XML. */
|
|
219
|
-
const storageError = (status, code, message) => new Response(`<?xml version="1.0" encoding="UTF-8"?>\n<Error><Code>${
|
|
219
|
+
const storageError = (status, code, message) => new Response(`<?xml version="1.0" encoding="UTF-8"?>\n<Error><Code>${escapeXmlText(code)}</Code><Message>${escapeXmlText(message)}</Message></Error>`, { status, headers: { "content-type": "application/xml" } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "8.1.
|
|
3
|
+
"version": "8.1.2",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"description": "Opinionated serverless web framework for TypeScript on AWS Lambda: type-safe APIs from Zod schemas, DynamoDB sessions, and declarative rate limits, authorization guards and idempotency.",
|
|
6
6
|
"keywords": [
|