lambder 13.0.1 → 14.0.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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,45 @@ 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
+ ## [14.0.1] - 2026-09-30
13
+
14
+ A major: `writeApiGuardParams` writes one export per API in place of one
15
+ table, so a browser carries only the guard parameters its screens import,
16
+ and none of the API names behind them.
17
+
18
+ ### Changed (breaking)
19
+
20
+ - **`writeApiGuardParams` writes one export per API** that declares the
21
+ guard, in place of the `guardParams` table: `orders.list` exports
22
+ `ordersListGuardParam`, holding the parameter as declared (`true` for the
23
+ guard named without one) and typed
24
+ `LambderApiGuardParam<"orders.list", "store", literal>`. A bundler keeps
25
+ only the exports a client imports, and none of them carries its API's name,
26
+ which sits in the export's type and doc comment alone, so a page no longer
27
+ ships the name of every API behind the guard and what each asks. The
28
+ module imports the tag type from `lambder/client`, as a type. Two APIs
29
+ whose names would export under one identifier fail the write, and so does
30
+ a `null` parameter, which the tag cannot carry; a guard no API declares
31
+ writes an empty module. A file still in the table form reads as one to
32
+ regenerate, on a check and on a write.
33
+ - To move: regenerate the file, and import the exports a screen gates on
34
+ in place of reading `guardParams[name]`. A function that takes any of a
35
+ guard's parameters reads their union off the module with a type-only
36
+ import (`(typeof params)[keyof typeof params]`, over
37
+ `import type * as params`). Code that needs every API's name, a test
38
+ walking them all, reads it from the `writeApiOptions` file.
39
+
40
+ ### Added
41
+
42
+ - **`LambderApiGuardParam`** (`lambder/client`, `lambder`): the type of each
43
+ export `writeApiGuardParams` writes, a guard's parameter tagged in types
44
+ alone with the API's name and the guard's, so a function typed to a
45
+ guard's parameters refuses a hand-written literal.
46
+ - **`apiGuardParamExportName`** (`lambder/client`, `lambder`): the identifier
47
+ `writeApiGuardParams` exports an API's parameter under, for code that reads
48
+ a generated module by API name, such as a test comparing it with the
49
+ server.
50
+
12
51
  ## [13.0.1] - 2026-09-30
13
52
 
14
53
  A major: session data is checked against a schema on every read, and
package/README.md CHANGED
@@ -140,7 +140,7 @@ The package ships five entry points; pick by where the code runs:
140
140
  | `lambder/client` | Browser and isomorphic shared code | `LambderCaller`, `LambderApiRefusal`/`refuse`, the API contract and envelope types, `LambderUploadRunner`, `LambderBackoffTimer`, `LambderSignedClaims`, `html`/`xml` tagged templates, `createLambderI18n` |
141
141
  | `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 |
142
142
  | `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; the store conformance suites, to hold a store you write to the rules Lambder's own meet |
143
- | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiOptions`: every API's declared options, policies and guard declarations as plain data, for the code that decides with them; `writeApiGuardParams`: one guard's parameters and nothing else, what a browser gating on that guard carries; `writeApiContract`: the contract as plain types a client compiles instead of the server; `generateApiFiles`: every one of them, for every app a script names, in one call |
143
+ | `lambder/build` | Node, in a build step | `writeApiSignatures`: the signature file both sides ship, written or checked from your instance; `writeApiOptions`: every API's declared options, policies and guard declarations as plain data, for the code that decides with them; `writeApiGuardParams`: one guard's parameters, one export per API and nothing else, so a browser gating on that guard carries only the ones it imports; `writeApiContract`: the contract as plain types a client compiles instead of the server; `generateApiFiles`: every one of them, for every app a script names, in one call |
144
144
 
145
145
  Frontends and shared isomorphic packages should import from `lambder/client`
146
146
  only; the entry's module graph contains no AWS SDK, Node built-ins, or server
@@ -52,7 +52,7 @@ export declare const settleGeneratedFile: (run: {
52
52
  previousText: string | null;
53
53
  /** Whether that text held the tables as written; false for a file rewritten by hand. */
54
54
  readBack: boolean;
55
- /** Each table the file holds, under its exported name: how many entries it has now, and which of them moved. */
55
+ /** Each table the file holds, under the name its lines print (a table's exported name): how many entries it has now, and which of them moved. */
56
56
  tables: readonly {
57
57
  name: string;
58
58
  count: number;
@@ -12,13 +12,13 @@ export type LambderApiGuardParamsFileOptions = {
12
12
  exportName?: string;
13
13
  /** The guard whose parameters the file holds, as the server declares it. A name the server declares no guard under throws. */
14
14
  guard: string;
15
- /** The TypeScript module to write, exporting `guardParams`. Relative to the working directory. */
15
+ /** The TypeScript module to write, one export per API that declares the guard. Relative to the working directory. */
16
16
  file: string;
17
17
  /** Write nothing, and answer whether the file on disk holds what the instance reports now. Default: false. */
18
18
  check?: boolean;
19
19
  /** The comment the file opens with, one `//` line per line. It should name what generates the file. Default: a note naming writeApiGuardParams() and the guard. */
20
20
  header?: string;
21
- /** End the generated statement with a semicolon. Default: true. The table itself is JSON, double quotes included, whatever the project's style: that is what lets a check read it back. */
21
+ /** End each generated statement with a semicolon. Default: true. The values are JSON, double quotes included, whatever the project's style: that is what lets a check read them back. */
22
22
  semicolons?: boolean;
23
23
  };
24
24
  export type LambderApiGuardParamsFileResult = {
@@ -26,7 +26,7 @@ export type LambderApiGuardParamsFileResult = {
26
26
  ok: boolean;
27
27
  /** The absolute path. */
28
28
  file: string;
29
- /** True when the file was (re)written; a file that already holds this table is left untouched, however it is formatted. */
29
+ /** True when the file was (re)written; a file that already holds these parameters is left untouched, however it is formatted. */
30
30
  written: boolean;
31
31
  /** How many APIs declare the guard. */
32
32
  count: number;
@@ -36,8 +36,8 @@ export type LambderApiGuardParamsFileResult = {
36
36
  lines: string[];
37
37
  };
38
38
  /**
39
- * Writes one guard's parameters as a module of plain data, or checks the one
40
- * on disk, and says which APIs moved.
39
+ * Writes one guard's parameters as a module of one export per API, or checks
40
+ * the one on disk, and says which APIs moved.
41
41
  *
42
42
  * ```ts
43
43
  * import { writeApiGuardParams } from "lambder/build";
@@ -51,10 +51,11 @@ export type LambderApiGuardParamsFileResult = {
51
51
  * });
52
52
  * ```
53
53
  *
54
- * The module exports `guardParams`, `as const`, so a client reads its types
55
- * straight off it: `keyof typeof guardParams` is the APIs behind the guard,
56
- * and `(typeof guardParams)[K]` the literal API K declared, `true` for a
57
- * guard named without a parameter (the string and list forms). A parameter
58
- * that is not plain data fails the write, as it does for writeApiOptions.
54
+ * API `orders.list` exports `ordersListGuardParam`: the literal it declared,
55
+ * `true` for a guard named without a parameter (the string and list forms),
56
+ * typed LambderApiGuardParam<"orders.list", "store", literal>. Two APIs whose
57
+ * names export under one identifier fail the write, and so does a parameter
58
+ * that is not plain data, as it does for writeApiOptions, or that is null,
59
+ * which no tag can carry.
59
60
  */
60
61
  export declare const writeApiGuardParams: (options: LambderApiGuardParamsFileOptions) => Promise<LambderApiGuardParamsFileResult>;
@@ -1,32 +1,128 @@
1
1
  import { existsSync, readFileSync } from "fs";
2
2
  import { resolve } from "path";
3
- import { apiGuardParam } from "../shared/wire/LambderApiOptionEntries.js";
4
- import { headerLinesOf, loadApiOptionEntries, nameChangesOf, readGeneratedTable, settleGeneratedFile, tableStatementLines, } from "./generatedTables.js";
3
+ import { apiGuardParam, apiGuardParamExportName } from "../shared/wire/LambderApiOptionEntries.js";
4
+ import { headerLinesOf, loadApiOptionEntries, nameChangesOf, settleGeneratedFile, } from "./generatedTables.js";
5
5
  /*
6
- * One guard's parameters as a generated module a browser may carry: for each
7
- * API that declares the guard, the parameter it gives it, and nothing else
8
- * about any API.
6
+ * One guard's parameters as a generated module a browser may carry: one
7
+ * export per API that declares the guard, holding the parameter it gives it,
8
+ * and nothing else about any API.
9
9
  *
10
10
  * The file writeApiOptions writes holds every declaration, because a mock
11
11
  * and a test need every declaration. A screen that decides whether to offer
12
12
  * a control by the permission an endpoint's guard asks needs one guard's
13
- * parameter, and a browser that imports the whole table as a value carries
14
- * every API name, every mode and every guard's parameter, a reviewer's
15
- * reason beside an open endpoint included. This is the least that screen
16
- * needs: the names of the APIs behind the guard (which the client calls, so
17
- * its own code names them already) and what each asks.
13
+ * parameter, for the endpoints it gates on. A table of them would still hand
14
+ * every page that imports it the name of every API behind the guard and what
15
+ * each asks, whichever of them its screens use. One export per API lets the
16
+ * bundler keep exactly the ones the client imports, and keep no name at all:
17
+ * the API's name sits in the export's type and doc comment, which a build
18
+ * strips, and in its identifier, which a minifier renames.
19
+ *
20
+ * Each export is tagged with its API's name and the guard's
21
+ * (LambderApiGuardParam), in types alone. A client function typed to the
22
+ * tagged parameters takes only generated ones, so a hand-written literal,
23
+ * which would restate the server's declaration and could drift from it, does
24
+ * not compile.
18
25
  */
19
- const TABLE_NAME = "guardParams";
26
+ /** The tag type every export is asserted to, imported from the client entry, which is safe in a browser. */
27
+ const TAG_TYPE = "LambderApiGuardParam";
20
28
  const defaultHeader = (guard) => [
21
29
  "Generated by writeApiGuardParams() from lambder/build. Do not edit.",
22
30
  "",
23
31
  `The parameter each API the server guards with "${guard}" gives that guard,`,
24
- "and nothing else about any API: what a client decides with it, without",
25
- "carrying the server's other declarations.",
32
+ "one export per API and nothing else about any API: a client imports the",
33
+ "ones its screens gate on, and its bundle carries those alone.",
26
34
  ].join("\n");
27
35
  /**
28
- * Writes one guard's parameters as a module of plain data, or checks the one
29
- * on disk, and says which APIs moved.
36
+ * A plain-data value as the type `as const` gives it: its literals, readonly
37
+ * all the way down. What each export is asserted to beside its tag, so the
38
+ * tagged type keeps the literal the API declared.
39
+ */
40
+ const constTypeOf = (value) => {
41
+ if (Array.isArray(value))
42
+ return `readonly [${value.map(constTypeOf).join(", ")}]`;
43
+ if (value !== null && typeof value === "object") {
44
+ const members = Object.entries(value).map(([key, member]) => `readonly ${JSON.stringify(key)}: ${constTypeOf(member)}`);
45
+ return members.length ? `{ ${members.join("; ")} }` : "{}";
46
+ }
47
+ return JSON.stringify(value);
48
+ };
49
+ /**
50
+ * One API's export as written, less the statement's semicolon: `export const
51
+ * <name> = <JSON> as const as LambderApiGuardParam<"<api>", "<guard>", <type>>`.
52
+ * A check renders each export it reads back again and compares the two, so
53
+ * the identifier a client imports, the guard and the literal type are held
54
+ * to what this writer would write, not only the value.
55
+ */
56
+ const exportLineOf = (name, param, guard) => `export const ${apiGuardParamExportName(name)} = ${JSON.stringify(param)} as const as ${TAG_TYPE}<${JSON.stringify(name)}, ${JSON.stringify(guard)}, ${constTypeOf(param)}>`;
57
+ /** The start of one export, far enough to read back its parameter's JSON and its API's name. */
58
+ const EXPORT_LINE = new RegExp(`^export const [A-Za-z_$][\\w$]* = (.+) as const as ${TAG_TYPE}<("(?:[^"\\\\]|\\\\.)*"), `);
59
+ /** The tag's import, in whatever spacing and quotes a formatter gives it, since no `// prettier-ignore` sits above it. */
60
+ const IMPORT_LINE = new RegExp(`^import type \\{\\s*${TAG_TYPE}\\s*\\} from (["'])lambder/client\\1;?$`);
61
+ /** The empty module of a guard no API declares, however a formatter spaces it. */
62
+ const EMPTY_EXPORT_LINE = /^export \{\s*\};?$/;
63
+ /**
64
+ * The parameters a file already holds, by API name, or null for a file that
65
+ * does not hold them as written. As written means the tag's import and one
66
+ * export per API, each the line exportLineOf renders for its API and value,
67
+ * or the empty module alone for a guard no API declares. Anything else makes
68
+ * it null: another statement, an export renamed by hand or written for
69
+ * another guard, an API exported twice, a missing import, a file with no
70
+ * statement at all. Comments, blank lines, re-indentation, line endings and
71
+ * the semicolons change nothing.
72
+ */
73
+ const readGuardParams = (contents, guard) => {
74
+ const params = {};
75
+ let imported = false;
76
+ let empty = false;
77
+ for (const line of contents.split(/\r?\n/).map((text) => text.trim())) {
78
+ if (line === "" || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*"))
79
+ continue;
80
+ if (IMPORT_LINE.test(line)) {
81
+ imported = true;
82
+ continue;
83
+ }
84
+ if (EMPTY_EXPORT_LINE.test(line)) {
85
+ empty = true;
86
+ continue;
87
+ }
88
+ const match = EXPORT_LINE.exec(line);
89
+ if (!match)
90
+ return null;
91
+ try {
92
+ const param = JSON.parse(match[1]);
93
+ const name = JSON.parse(match[2]);
94
+ if (typeof name !== "string" || Object.prototype.hasOwnProperty.call(params, name))
95
+ return null;
96
+ if (line.replace(/;$/, "") !== exportLineOf(name, param, guard))
97
+ return null;
98
+ params[name] = param;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ const held = Object.keys(params).length > 0 ? imported && !empty : empty && !imported;
105
+ return held ? params : null;
106
+ };
107
+ /** The file's statements after its header: the tag's import and one export per API, or an empty export for a guard no API declares. */
108
+ const statementLinesOf = (params, guard, semicolon) => {
109
+ const names = Object.keys(params);
110
+ if (names.length === 0)
111
+ return [`export {}${semicolon}`];
112
+ return [
113
+ `import type { ${TAG_TYPE} } from "lambder/client"${semicolon}`,
114
+ ...names.flatMap((name) => [
115
+ "",
116
+ `/** What ${name} gives the "${guard}" guard. */`,
117
+ // Keeps a formatter off the JSON a check reads back.
118
+ "// prettier-ignore",
119
+ `${exportLineOf(name, params[name], guard)}${semicolon}`,
120
+ ]),
121
+ ];
122
+ };
123
+ /**
124
+ * Writes one guard's parameters as a module of one export per API, or checks
125
+ * the one on disk, and says which APIs moved.
30
126
  *
31
127
  * ```ts
32
128
  * import { writeApiGuardParams } from "lambder/build";
@@ -40,11 +136,12 @@ const defaultHeader = (guard) => [
40
136
  * });
41
137
  * ```
42
138
  *
43
- * The module exports `guardParams`, `as const`, so a client reads its types
44
- * straight off it: `keyof typeof guardParams` is the APIs behind the guard,
45
- * and `(typeof guardParams)[K]` the literal API K declared, `true` for a
46
- * guard named without a parameter (the string and list forms). A parameter
47
- * that is not plain data fails the write, as it does for writeApiOptions.
139
+ * API `orders.list` exports `ordersListGuardParam`: the literal it declared,
140
+ * `true` for a guard named without a parameter (the string and list forms),
141
+ * typed LambderApiGuardParam<"orders.list", "store", literal>. Two APIs whose
142
+ * names export under one identifier fail the write, and so does a parameter
143
+ * that is not plain data, as it does for writeApiOptions, or that is null,
144
+ * which no tag can carry.
48
145
  */
49
146
  export const writeApiGuardParams = async (options) => {
50
147
  const file = resolve(options.file);
@@ -54,30 +151,37 @@ export const writeApiGuardParams = async (options) => {
54
151
  throw new Error(`writeApiGuardParams: the server declares no guard "${options.guard}" (it declares ${declared.length ? declared.map((name) => `"${name}"`).join(", ") : "none"}).`);
55
152
  }
56
153
  const params = {};
154
+ const exportedBy = new Map();
57
155
  for (const name of Object.keys(entries.apis).sort()) {
58
156
  const param = apiGuardParam(entries.apis, name, options.guard);
59
- if (param !== undefined)
60
- params[name] = param;
157
+ if (param === undefined)
158
+ continue;
159
+ if (param === null) {
160
+ throw new Error(`writeApiGuardParams: "${name}" gives the "${options.guard}" guard null, which an export cannot carry with its tag; give the guard another value.`);
161
+ }
162
+ const exportName = apiGuardParamExportName(name);
163
+ const taken = exportedBy.get(exportName);
164
+ if (taken !== undefined) {
165
+ throw new Error(`writeApiGuardParams: "${taken}" and "${name}" would both export ${exportName}; rename one of them.`);
166
+ }
167
+ exportedBy.set(exportName, name);
168
+ params[name] = param;
61
169
  }
62
170
  const previousText = existsSync(file) ? readFileSync(file, "utf8") : null;
63
- const previous = previousText === null ? null : readGeneratedTable(previousText, TABLE_NAME);
171
+ const previous = previousText === null ? null : readGuardParams(previousText, options.guard);
64
172
  const changes = nameChangesOf(params, previous ?? {});
65
173
  const count = Object.keys(params).length;
174
+ const semicolon = options.semicolons === false ? "" : ";";
66
175
  const settled = settleGeneratedFile({
67
176
  name: options.file, file, check: options.check, previousText, readBack: previous !== null,
68
- tables: [{ name: TABLE_NAME, count, changes }],
177
+ tables: [{ name: `${options.guard} params`, count, changes }],
69
178
  held: `${count} APIs guarded by "${options.guard}"`,
70
- tablesNoun: "the table",
179
+ tablesNoun: "the parameters",
71
180
  unmovedLine: "no parameters changed",
72
181
  render: () => [
73
182
  ...headerLinesOf(options.header ?? defaultHeader(options.guard)),
74
183
  "",
75
- ...tableStatementLines({
76
- name: TABLE_NAME,
77
- doc: `The parameter each API guarded by "${options.guard}" gives it: the value as declared, or true for the guard named without one.`,
78
- value: params,
79
- semicolon: options.semicolons === false ? "" : ";",
80
- }),
184
+ ...statementLinesOf(params, options.guard, semicolon),
81
185
  "",
82
186
  ].join("\n"),
83
187
  });
package/dist/client.d.ts CHANGED
@@ -29,8 +29,8 @@ export { LambderApiRefusal, isLambderApiRefusal, isLambderRefusalCode, refuse, r
29
29
  export type { LambderApiRefusalOptions, LambderRefusalMessage, LambderPlainRefusalMessage, LambderUncheckedRefusalMessage, LambderRefusalCode, LambderRefuseOptions, LambderDeclaredRefuseOptions, LambderDeclaredRefuse, } from "./shared/wire/LambderApiRefusal.js";
30
30
  export { isObjectPayload } from "./shared/wire/LambderObjectPayload.js";
31
31
  export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiSuccessEnvelope, LambderApiRefusalEnvelope, LambderRefusalEnvelopeFields, LambderApiRefusalConfig, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderContractRefusalsOf, LambderContractRefusalMessage, LambderContractRefusals, LambderContractAnyRefusalMessage, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
32
- export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
33
- export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, } from "./shared/wire/LambderApiOptionEntries.js";
32
+ export { apiGuardParam, apiGuardParamExportName } from "./shared/wire/LambderApiOptionEntries.js";
33
+ export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, LambderApiGuardParam, } from "./shared/wire/LambderApiOptionEntries.js";
34
34
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
35
35
  export type { LambderCrashDetail, LambderCrashCause } from "./shared/wire/LambderCrashDetail.js";
36
36
  export { compressPayloadGzip, isRequestCompressionAvailable, COMPRESSED_PAYLOAD_GZ_FIELD, COMPRESSED_PAYLOAD_BR_FIELD, COMPRESSED_PAYLOAD_BYTES_FIELD, DEFAULT_REQUEST_COMPRESSION_SETTINGS, } from "./shared/wire/LambderRequestPayload.js";
package/dist/client.js CHANGED
@@ -28,7 +28,7 @@ export { LambderApiRefusal, isLambderApiRefusal, isLambderRefusalCode, refuse, r
28
28
  export { isObjectPayload } from "./shared/wire/LambderObjectPayload.js";
29
29
  // The server's declared options as plain data (the generated options module's
30
30
  // entry types) and the readers a client derives its own facts from.
31
- export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
31
+ export { apiGuardParam, apiGuardParamExportName } from "./shared/wire/LambderApiOptionEntries.js";
32
32
  // A crash described for a caller allowed to see it (the envelope's `crash` field; pure, no Node built-ins).
33
33
  export { describeCrash, errorFromCrashDetail } from "./shared/wire/LambderCrashDetail.js";
34
34
  // Request payload compression (browser-safe: gzip via CompressionStream, no Node built-ins).
package/dist/index.d.ts CHANGED
@@ -140,8 +140,8 @@ export type { LambderRateLimitKeyBuilder } from "./api/LambderApiRateLimits.js";
140
140
  export type { LambderContextRateLimit, LambderContextRateLimitCheck, LambderRateLimitCheckResult, LambderChargeablePolicyNames, LambderChargeKeyArgs, LambderRateLimitKeyFn, LambderRateLimitPer, LambderRateLimitBudget, LambderRateLimitChargeAt, LambderApiRateLimitPolicyConfig, LambderApiRateLimitsConfig, LambderAllowedPolicyNames, LambderPolicyNamesInputLacks, LambderRateLimitOption, } from "./api/LambderApiRateLimits.js";
141
141
  export type { LambderApiIdempotencyConfig } from "./api/LambderApiIdempotency.js";
142
142
  export type { LambderGuardsOptionValue, LambderRateLimitMessage, LambderRateLimitOverride, LambderRateLimitOptionValue, LambderRefusalsOptionValue, LambderApiIdempotencyOption, } from "./shared/wire/LambderApiOptionValues.js";
143
- export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
144
- export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, } from "./shared/wire/LambderApiOptionEntries.js";
143
+ export { apiGuardParam, apiGuardParamExportName } from "./shared/wire/LambderApiOptionEntries.js";
144
+ export type { LambderApiOptionEntries, LambderApiOptionEntry, LambderRateLimitPolicyEntry, LambderGuardDeclarationEntry, LambderApisWithGuard, LambderApisGuardedBy, LambderApisWithMode, LambderGuardParamOf, LambderApiGuardParam, } from "./shared/wire/LambderApiOptionEntries.js";
145
145
  export { createLambderI18n } from "./shared/LambderI18n.js";
146
146
  export type { LambderLanguageMeta, LambderI18nConfig, LambderI18nInstance, LambderI18nTranslator, LambderI18nExtractParams, LambderI18nDictionaryLoader, LambderI18nCodes, LambderI18nKeys, LambderI18nTranslatorFor, } from "./shared/LambderI18n.js";
147
147
  export type { LambderApiContractShape, LambderApiMode, LambderApiEnvelopeBody, LambderApiSuccessEnvelope, LambderApiRefusalEnvelope, LambderRefusalEnvelopeFields, LambderApiRefusalConfig, LambderContractEntry, LambderGuardNamesIn, LambderContractMode, LambderContractKeysWithMode, LambderContractKeysWithGuard, LambderJsonOf, LambderContractRefusalsOf, LambderContractRefusalMessage, LambderContractRefusals, LambderContractAnyRefusalMessage, LambderContractGuardsOf, LambderContractGuardNames, LambderContractGuardInputsOf, LambderContractGuardInput, LambderContractGuardInputNames, LambderContractRateLimitOf, LambderContractRateLimitNames, LambderContractIdempotencyOf, } from "./shared/wire/LambderApiContract.js";
package/dist/index.js CHANGED
@@ -97,7 +97,7 @@ export { LambderApiRefusalValidationError } from "./api/LambderApiRefusals.js";
97
97
  export { lambderRateLimitKeyBuilder, rateLimitRefusal, DEFAULT_RATE_LIMIT_REFUSAL } from "./api/LambderApiRateLimits.js";
98
98
  // The declared options as plain data: what apiOptionEntries() reports and
99
99
  // writeApiOptions writes, and the readers over the generated tables.
100
- export { apiGuardParam } from "./shared/wire/LambderApiOptionEntries.js";
100
+ export { apiGuardParam, apiGuardParamExportName } from "./shared/wire/LambderApiOptionEntries.js";
101
101
  // Typed translations (standalone, isomorphic)
102
102
  export { createLambderI18n } from "./shared/LambderI18n.js";
103
103
  export { resolveApiOutcome } from "./shared/wire/LambderApiOutcome.js";
@@ -153,3 +153,27 @@ export type LambderGuardParamOf<TEntry, N extends string> = TEntry extends {
153
153
  * reads instead of the source.
154
154
  */
155
155
  export declare const apiGuardParam: <TOptions extends Record<string, LambderApiOptionEntry>, K extends keyof TOptions & string, N extends string>(options: TOptions, name: K, guard: N) => LambderGuardParamOf<TOptions[K], N>;
156
+ /**
157
+ * The identifier writeApiGuardParams exports an API's parameter under: its
158
+ * group, its action capitalized, then `GuardParam` (`orders.list` exports
159
+ * `ordersListGuardParam`). What code that reads a generated module by API
160
+ * name (a test comparing it with the server) looks each export up by.
161
+ */
162
+ export declare const apiGuardParamExportName: (apiName: string) => string;
163
+ /** The key a generated guard parameter's tag sits under: declared and never created, so it exists in types alone. */
164
+ declare const API_GUARD_PARAM_TAG: unique symbol;
165
+ /**
166
+ * One API's parameter for one guard, as writeApiGuardParams exports it: the
167
+ * value TParam as declared (`true` for a guard named without one), tagged
168
+ * with the API's name and the guard's. The tag exists in types alone, so the
169
+ * names reach no bundle. A function typed to a guard's tagged parameters
170
+ * takes only generated ones: a hand-written literal, which would restate the
171
+ * server's declaration and could drift from it, does not compile.
172
+ */
173
+ export type LambderApiGuardParam<TApi extends string, TGuard extends string, TParam> = TParam & {
174
+ readonly [API_GUARD_PARAM_TAG]: {
175
+ readonly api: TApi;
176
+ readonly guard: TGuard;
177
+ };
178
+ };
179
+ export {};
@@ -15,6 +15,7 @@
15
15
  * (when a guard runs, what a budget spans, when a custom key is charged) are
16
16
  * declared here too and re-exported by the engines that read them.
17
17
  */
18
+ import { splitApiName } from "./LambderApiNames.js";
18
19
  /**
19
20
  * The parameter an API's guards option gives guard N, read off a generated
20
21
  * `apiOptions` table with the type the table pins: the literal a permission
@@ -33,3 +34,15 @@ export const apiGuardParam = (options, name, guard) => {
33
34
  param = guards[guard];
34
35
  return param;
35
36
  };
37
+ /**
38
+ * The identifier writeApiGuardParams exports an API's parameter under: its
39
+ * group, its action capitalized, then `GuardParam` (`orders.list` exports
40
+ * `ordersListGuardParam`). What code that reads a generated module by API
41
+ * name (a test comparing it with the server) looks each export up by.
42
+ */
43
+ export const apiGuardParamExportName = (apiName) => {
44
+ const parts = splitApiName(apiName);
45
+ if (!parts)
46
+ throw new Error(`Lambder: "${apiName}" is not an endpoint name, so its guard parameter has no export name.`);
47
+ return `${parts.group}${parts.action[0].toUpperCase()}${parts.action.slice(1)}GuardParam`;
48
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "13.0.1",
3
+ "version": "14.0.1",
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": [