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 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 readonly failures;
26
- /** Every named declaration by the type it stands for; its text is null while it is being printed. */
27
- private readonly declarations;
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 takenNames;
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
- /** Prints each member of the contract type, sorted by name, and every declaration they refer to. */
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
- /** The name a type is declared under, when it is one to print as a declaration: non-generic, and not the default library's. */
47
- private declaredNameOf;
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 recursiveNameOf;
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 named declaration by the type it stands for; its text is null while it is being printed. */
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
- takenNames = new Set();
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.takenNames.add(contractName);
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.takenNames.add(statement.name.text);
65
+ this.reservedNames.add(statement.name.text);
64
66
  }
65
67
  }
66
68
  }
67
- /** Prints each member of the contract type, sorted by name, and every declaration they refer to. */
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 entries = this.checker.getPropertiesOfType(contract)
70
- .map((entry) => ({ name: entry.name, text: this.print(this.checker.getTypeOfSymbol(entry), this.keyOf(entry.name)).text }))
71
- .sort((a, b) => byCodeUnits(a.name, b.name));
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 ownName = this.declaredNameOf(type);
146
- if (ownName !== undefined) {
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 = { name: this.takeName(ownName), text: null };
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
- const declaration = { name: this.takeName(this.recursiveNameOf(type)), text: null };
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
- /** The name a type is declared under, when it is one to print as a declaration: non-generic, and not the default library's. */
170
- declaredNameOf(type) {
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.nameOf(type.aliasSymbol);
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.nameOf(type.symbol);
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
- recursiveNameOf(type) {
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 `${base}${argumentNames.map((name) => name[0].toUpperCase() + name.slice(1)).join("")}`;
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 that doubles with
52
- * every failed attempt, never past `maxDelayMs`, so many browsers dropped
53
- * together do not come back in step. Default: 4 attempts, waits from one
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
- sha256Base64: await sha256Base64Of(bytes),
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 ** (failedAttempts - 1), this.maxDelayMs));
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, and an expired
222
- * ticket is the one refusal a new ticket cures.
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
- const escapeXml = (text) => text.replace(/[<>&'"]/g, (character) => `&#${character.charCodeAt(0)};`);
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>${escapeXml(key)}</Key><Value>${escapeXml(value)}</Value></Tag>`).join("")}</TagSet></Tagging>`;
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>${escapeXml(code)}</Code><Message>${escapeXml(message)}</Message></Error>`, { status, headers: { "content-type": "application/xml" } });
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.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": [