lambder 8.0.2 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/README.md +7 -2
  3. package/dist/build/ContractTypePrinter.d.ts +115 -0
  4. package/dist/build/ContractTypePrinter.js +460 -0
  5. package/dist/build/moduleLocation.d.ts +11 -0
  6. package/dist/build/moduleLocation.js +6 -0
  7. package/dist/build/writeApiContract.d.ts +79 -0
  8. package/dist/build/writeApiContract.js +303 -0
  9. package/dist/build/writeApiSignatures.d.ts +32 -27
  10. package/dist/build/writeApiSignatures.js +37 -42
  11. package/dist/build/writeFileAtomically.d.ts +8 -0
  12. package/dist/build/writeFileAtomically.js +22 -0
  13. package/dist/build.d.ts +8 -3
  14. package/dist/build.js +6 -3
  15. package/dist/client/LambderUploadRunner.d.ts +96 -0
  16. package/dist/client/LambderUploadRunner.js +234 -0
  17. package/dist/client.d.ts +4 -0
  18. package/dist/client.js +4 -0
  19. package/dist/core/Lambder.d.ts +9 -10
  20. package/dist/core/Lambder.js +9 -10
  21. package/dist/index.d.ts +11 -1
  22. package/dist/index.js +8 -0
  23. package/dist/mock/lambderMockMswHandler.d.ts +10 -4
  24. package/dist/mock/lambderMockUploadMswHandler.d.ts +26 -0
  25. package/dist/mock/lambderMockUploadMswHandler.js +28 -0
  26. package/dist/mock.d.ts +3 -0
  27. package/dist/mock.js +4 -0
  28. package/dist/shared/contracts/LambderUploadBucket.d.ts +154 -0
  29. package/dist/shared/contracts/LambderUploadBucket.js +74 -0
  30. package/dist/shared/util/LambderContentDisposition.d.ts +10 -0
  31. package/dist/shared/util/LambderContentDisposition.js +13 -0
  32. package/dist/shared/util/LambderTextDigest.d.ts +7 -5
  33. package/dist/shared/util/LambderTextDigest.js +11 -5
  34. package/dist/shared/util/escapeXmlText.d.ts +8 -0
  35. package/dist/shared/util/escapeXmlText.js +8 -0
  36. package/dist/shared/wire/LambderApiContract.d.ts +10 -40
  37. package/dist/shared/wire/LambderApiRefusal.d.ts +6 -0
  38. package/dist/shared/wire/LambderApiRefusal.js +6 -0
  39. package/dist/shared/wire/LambderUploadObjectFields.d.ts +10 -0
  40. package/dist/shared/wire/LambderUploadObjectFields.js +24 -0
  41. package/dist/shared/wire/LambderUploadRefusal.d.ts +9 -0
  42. package/dist/shared/wire/LambderUploadRefusal.js +18 -0
  43. package/dist/shared/wire/LambderUploadSchemas.d.ts +12 -0
  44. package/dist/shared/wire/LambderUploadSchemas.js +30 -0
  45. package/dist/stores/LambderDdbSdk.js +1 -5
  46. package/dist/stores/LambderMemoryUploadBucket.d.ts +99 -0
  47. package/dist/stores/LambderMemoryUploadBucket.js +219 -0
  48. package/dist/stores/LambderS3UploadBucket.d.ts +73 -0
  49. package/dist/stores/LambderS3UploadBucket.js +144 -0
  50. package/dist/stores/LambderSdkInstallHint.d.ts +11 -0
  51. package/dist/stores/LambderSdkInstallHint.js +14 -0
  52. package/dist/testing/LambderTestApp.d.ts +4 -4
  53. package/dist/testing.d.ts +2 -0
  54. package/dist/testing.js +1 -0
  55. package/package.json +15 -1
package/CHANGELOG.md CHANGED
@@ -9,7 +9,149 @@ 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.0.1] - 2026-09-25
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
+
40
+ ## [8.1.1] - 2026-09-26
41
+
42
+ Two additions, and three breaking changes a minor line carries here on
43
+ purpose. An app's contract can now be written out as a generated file of
44
+ plain types for its clients to import, which does by default what
45
+ `LambderFlattenContract` asked each app to spell out, so that helper is gone.
46
+ And direct uploads, files posted from the browser straight to S3 on tickets
47
+ the server signs, are part of the framework.
48
+
49
+ ### Added
50
+
51
+ - **`writeApiContract` in `lambder/build`: the contract as a generated file.**
52
+ A client that imports `typeof lambder.ApiContract` from the server compiles
53
+ the server to get it, every endpoint's schemas and the libraries they infer
54
+ through included, and reads it as the intersection chaining built. In a
55
+ 193-endpoint app that was 17 of the frontend check's 19 million type
56
+ instantiations and 2.4 of its 4.8 GB. `writeApiContract` reads the
57
+ `ApiContract` of the instance a module exports (`module`, `exportName`)
58
+ through the TypeScript compiler, under the server's own tsconfig and
59
+ without running any of it, and writes the contract as one object type with
60
+ plain members to a module that imports nothing. The app declares nothing
61
+ for it, and its clients (a frontend, another service's
62
+ `LambderInvokeCaller`, its own tests through `lambderTestApp`) import the
63
+ type from there: the same frontend check fell to 1.8 million
64
+ instantiations and 2.4 GB.
65
+ - Every type is printed as the structure it resolves to. The default
66
+ library's interfaces (`Date`) keep their names, a non-generic named type
67
+ (an alias, an interface, a class) is printed once as a declaration the
68
+ entries refer to, which is also how a recursive type refers to itself,
69
+ and properties keep the order they are written in, so the file changes
70
+ only when an API does.
71
+ - Anything with no plain form fails the call and names where it sits: a
72
+ function, a symbol key, an enum, a class's private member, an open type
73
+ parameter, and a type a compile error left unresolved, wherever in the
74
+ server's sources the error is.
75
+ - A write compiles the new text beside the server's sources and checks each
76
+ entry against the contract in both directions before it touches the file.
77
+ `check: true` writes nothing and fails a stale file. Both name the APIs
78
+ that moved, counting a change to a shared declaration against every API
79
+ that reaches it.
80
+ - `typescript` 5.4 or later is an optional peer dependency, loaded only when
81
+ `writeApiContract` runs. It needs the compiler API, so 5.x or 6.x:
82
+ TypeScript 7 ships none, and the call says so when it finds a 7.
83
+
84
+ See [the contract as a generated file](./docs/apis.md#the-contract-as-a-generated-file).
85
+
86
+ - **Direct uploads.** A file too large for an API payload goes from the
87
+ browser straight to object storage, on a ticket the app's endpoint signs
88
+ for exactly that file: its key, byte size, content type and SHA-256, all
89
+ enforced by storage, and verified by the server before the app's record
90
+ counts it as uploaded.
91
+ - `LambderUploadBucket`, the interface a bucket implements: sign a ticket,
92
+ verify what arrived, sign a download link, and read, write, copy and
93
+ delete objects for the rest of their life. `LambderS3UploadBucket`
94
+ implements it over an S3 presigned POST whose policy pins every fact;
95
+ `@aws-sdk/s3-presigned-post` and `@aws-sdk/s3-request-presigner` join
96
+ `@aws-sdk/client-s3` as optional peers, each loaded on first use.
97
+ `writeObject` sends the checksum it is given or has the SDK compute one.
98
+ - Lifetimes at both levels: `ticketLifetimeSeconds` and
99
+ `downloadLifetimeSeconds` on the bucket, `lifetimeSeconds` on a ticket or
100
+ a link, each held to S3's seven days. What a stored object carries,
101
+ `object` on a ticket or a write: tags (how an object gets a time to live,
102
+ through a lifecycle rule), metadata, `Cache-Control` and a
103
+ `Content-Disposition`, all pinned in the ticket's policy. A download link
104
+ takes its own `contentDisposition`, to save a file under its name.
105
+ - `LambderUploadRunner` in `lambder/client`, the browser half: checks the
106
+ file against the rule, hashes it, asks for a ticket, posts it with
107
+ progress, tries again after a dropped, failing, timed-out or stalled
108
+ connection with a growing wait, asks for a new ticket when storage says
109
+ the old one expired, stops on an abort signal it also hands to the app's
110
+ own calls, and has the server confirm. A failure is a `LambderUploadError`
111
+ whose `reason` a screen words. It posts over XMLHttpRequest for progress,
112
+ and over fetch where that is all there is.
113
+ - `LambderMemoryUploadBucket`, the same bucket in memory, holding a post to
114
+ the rules S3 holds a presigned POST to and refusing with S3's statuses and
115
+ XML errors, so a runner takes the same path against it as against S3; and
116
+ `lambderMockUploadMswHandler` in `lambder/mock`, which puts it behind MSW
117
+ for a mock app's uploads.
118
+ - `LambderUploadFileFactsSchema` and `LambderUploadTicketSchema`, the zod
119
+ schemas of the two shapes that cross the app's own API; and
120
+ `checkUploadRule` and `refuseUnacceptedUpload`, for a bucket of an app's
121
+ own to refuse a file as Lambder's do.
122
+ - Three refusal codes for a file a rule does not accept:
123
+ `lambder/upload-empty`, `lambder/upload-type-rejected` and
124
+ `lambder/upload-too-large`.
125
+
126
+ See [Direct uploads](./docs/uploads.md).
127
+
128
+ ### Removed
129
+
130
+ - **`LambderFlattenContract`.** It collapsed the chained contract into an
131
+ interface an app had to declare by hand for its clients' type checks to stay
132
+ cheap. A client that needs that now imports the contract `writeApiContract`
133
+ generates, which is flat by construction, and the server's own reads of its
134
+ contract cost it little. Replace
135
+ `export interface ApiContractType extends LambderFlattenContract<typeof lambder.ApiContract> {}`
136
+ with `export type ApiContractType = typeof lambder.ApiContract`, or drop it
137
+ and point the clients at the generated file.
138
+
139
+ ### Changed
140
+
141
+ - **`writeApiSignatures({ module, exportName, file })`.** It takes the module
142
+ that exports the instance, as `writeApiContract` does, and imports it,
143
+ rather than the instance and then its module again for the fresh-process
144
+ check. That check now runs by default; `verifyInFreshProcess: false` skips
145
+ it. Replace `writeApiSignatures(lambder, { file, verifyInFreshProcess: {
146
+ module, exportName } })` with `writeApiSignatures({ module, exportName,
147
+ file })`. Both generators take `module` as a path or a file URL
148
+ (`LambderModuleLocation`).
149
+ - **`LambderMswModule` names `http.all` beside `http.post`.** It is the one
150
+ description of the msw module both mock adapters take, the API's and an
151
+ upload bucket's. The real `msw` module fits it as before; a hand-built
152
+ stand-in for it needs an `all` as well.
153
+
154
+ ## [8.0.2] - 2026-09-25
13
155
 
14
156
  A major, out of a review of 7.3.1. Most of it closes holes: session writes
15
157
  that could undo a logout or a password change, a login any website could
package/README.md CHANGED
@@ -67,6 +67,10 @@ const company = await caller.api("getCompany", { slug: "acme" });
67
67
  - **Frontend hosting.** Serve a build from a folder, S3, R2 or any HTTP
68
68
  origin, with an app shell rendered through a build-pipeline-safe template
69
69
  engine.
70
+ - **Direct uploads.** Files go from the browser straight to S3 on tickets
71
+ that pin their size, type and SHA-256, with a browser runner that hashes,
72
+ retries and reports progress, and a memory bucket that holds tests and the
73
+ mock to the same rules.
70
74
  - **Runs anywhere Lambda does.** API Gateway REST APIs (payload v1), HTTP APIs
71
75
  (payload v2) and Lambda Function URLs; the payload format is detected per
72
76
  event.
@@ -112,7 +116,7 @@ The package ships five entry points; pick by where the code runs:
112
116
  | `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `html`/`xml` tagged templates, `createLambderI18n` |
113
117
  | `lambder/mock` | Browser and Node, in development and tests | `LambderMockApp`, the mock runtime: your typed contract served from mock handlers over the real API pipeline and memory stores |
114
118
  | `lambder/testing` | Node, in tests | `lambderTestApp`: your real instance under test in this process, memory stores put under it in place, simulated browsers with typed callers in front of it, and the outcome assertions |
115
- | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance |
119
+ | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiContract`: the contract as plain types a client compiles instead of the server |
116
120
 
117
121
  Frontends and shared isomorphic packages should import from `lambder/client`
118
122
  only; the entry's module graph contains no AWS SDK, Node built-ins, or server
@@ -156,6 +160,7 @@ guide that matches what you are building. The full index lives in
156
160
  | [The API core](./docs/api-core.md) | `LambderApiPipeline`: the one pipeline the server and the mock runtime run, the store interfaces, the transports |
157
161
  | [Frontend client](./docs/client.md) | `LambderCaller`: typed calls, failure outcomes, timeouts, guard inputs, request compression, transports |
158
162
  | [Frontend hosting](./docs/frontend-hosting.md) | File sources, `servePublicFiles`, `serveIndexHtml`, `res.templateFile` |
163
+ | [Direct uploads](./docs/uploads.md) | Files the browser posts straight to S3 with tickets the server signs, verified before they count |
159
164
  | [Templating](./docs/templating.md) | `html`/`xml` tagged templates and `LambderTemplatingEngine` |
160
165
  | [Translations](./docs/i18n.md) | `createLambderI18n`: typed keys, extension, detection, on-demand languages, runtime dictionaries |
161
166
  | [The mock runtime](./docs/mock.md) | `LambderMockApp`: the typed contract served from mock handlers over the real pipeline, in the browser and in tests |
@@ -184,7 +189,7 @@ review of 7.3.1: session writes that cannot undo a logout, API calls that must
184
189
  be JSON, output schemas applied at runtime, rate limits that count IPv6 callers
185
190
  by their /64 and custom keys after the guards, idempotency keys bound to the
186
191
  request they were first sent with, and the same behavior on every gateway and
187
- in the mock. Every break and what to do about it is in the 8.0.1 entry. The
192
+ in the mock. Every break and what to do about it is in the 8.0.2 entry. The
188
193
  compiler finds most of them. Fourteen it cannot are named there: hand-built
189
194
  calls without a JSON Content-Type, hand-built answers without `apiVersion`,
190
195
  handlers whose payload does not match their output schema, code that decoded
@@ -0,0 +1,115 @@
1
+ import type ts from "typescript";
2
+ /** How the printed module is punctuated. */
3
+ export type ContractPrintStyle = {
4
+ quote: "'" | "\"";
5
+ semicolon: "" | ";";
6
+ };
7
+ /** The contract as text: each entry by name, and the named types the entries refer to. */
8
+ export type PrintedContract = {
9
+ entries: {
10
+ name: string;
11
+ text: string;
12
+ }[];
13
+ declarations: {
14
+ name: string;
15
+ text: string;
16
+ }[];
17
+ /** Where the contract holds something with no plain form, and what it is. */
18
+ failures: string[];
19
+ };
20
+ export declare class ContractTypePrinter {
21
+ private readonly ts;
22
+ private readonly program;
23
+ private readonly checker;
24
+ private readonly style;
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;
30
+ /** Names a declaration may not take: the default library's, and the contract's own. */
31
+ private readonly reservedNames;
32
+ /** The anonymous types being printed: meeting one again inside itself is recursion, and it needs a name. */
33
+ private readonly inProgress;
34
+ /** Under exactOptionalPropertyTypes an optional member's type carries the compiler's own "missing" undefined, which its source never wrote. */
35
+ private readonly exactOptionalProperties;
36
+ constructor(ts: typeof import("typescript"), program: ts.Program, checker: ts.TypeChecker, style: ContractPrintStyle, contractName: string);
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
+ */
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;
59
+ /**
60
+ * `label value`, with a union too long for one line starting on the next
61
+ * line, one member per line: what a property, an index signature and a
62
+ * declaration all print through.
63
+ */
64
+ labeledValue(label: string, value: string): string;
65
+ private print;
66
+ /** A union, an intersection or an object: printed in place, or as a reference to a declaration of its own. */
67
+ private printComposite;
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;
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. */
77
+ private nameOf;
78
+ /**
79
+ * A name for a type that refers to itself and has none of its own: what it
80
+ * instantiates followed by its arguments (a JSON mapping of a Tree is
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.
85
+ */
86
+ private recursiveDeclarationOf;
87
+ private printStructure;
88
+ private printUnion;
89
+ private printUnionOf;
90
+ private printIntersection;
91
+ private printObject;
92
+ private printMembers;
93
+ /**
94
+ * An optional member's type as its source wrote it. Under
95
+ * exactOptionalPropertyTypes the compiler adds its own "missing"
96
+ * undefined, a different type from the `undefined` a source writes;
97
+ * printed as `| undefined` it would let the member be undefined, which
98
+ * the source does not.
99
+ */
100
+ private printOptionalMember;
101
+ private printTuple;
102
+ private printTemplateLiteral;
103
+ /** Why a property cannot be printed as a plain member, or undefined when it can. */
104
+ private unprintableProperty;
105
+ /** An object type printed member by member: not an array, a tuple, a function or a default library interface. */
106
+ private isPlainObject;
107
+ private isCallable;
108
+ private isDefaultLibraryInterface;
109
+ /** Declared in the default library, even where a package augments it (as @types/node does some globals). */
110
+ private isDefaultLibrary;
111
+ private wrapped;
112
+ keyOf(name: string): string;
113
+ private quoted;
114
+ private fail;
115
+ }