lambder 13.0.1 → 14.1.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 +82 -0
- package/README.md +1 -1
- package/dist/api/LambderApiPipeline.d.ts +22 -2
- package/dist/api/LambderApiPipeline.js +28 -3
- package/dist/api/LambderApiRefusals.d.ts +16 -6
- package/dist/api/LambderApiRefusals.js +17 -10
- package/dist/api/LambderApiSignature.d.ts +5 -5
- package/dist/api/LambderApiSignature.js +7 -5
- package/dist/build/checkApiRefusals.d.ts +54 -0
- package/dist/build/checkApiRefusals.js +415 -0
- package/dist/build/generatedTables.d.ts +1 -1
- package/dist/build/loadTypeScriptCompiler.d.ts +10 -0
- package/dist/build/loadTypeScriptCompiler.js +20 -0
- package/dist/build/writeApiContract.js +21 -15
- package/dist/build/writeApiGuardParams.d.ts +11 -10
- package/dist/build/writeApiGuardParams.js +135 -31
- package/dist/build.d.ts +5 -2
- package/dist/build.js +4 -2
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/core/Lambder.d.ts +6 -5
- package/dist/core/Lambder.js +10 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/shared/wire/LambderApiOptionEntries.d.ts +24 -0
- package/dist/shared/wire/LambderApiOptionEntries.js +13 -0
- package/dist/shared/wire/LambderApiRefusal.d.ts +2 -2
- package/package.json +1 -1
|
@@ -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,
|
|
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:
|
|
7
|
-
* API that declares the guard, the parameter it gives it,
|
|
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,
|
|
14
|
-
* every
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
|
|
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:
|
|
25
|
-
"
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* that is not plain data
|
|
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
|
|
60
|
-
|
|
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 :
|
|
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:
|
|
177
|
+
tables: [{ name: `${options.guard} params`, count, changes }],
|
|
69
178
|
held: `${count} APIs guarded by "${options.guard}"`,
|
|
70
|
-
tablesNoun: "the
|
|
179
|
+
tablesNoun: "the parameters",
|
|
71
180
|
unmovedLine: "no parameters changed",
|
|
72
181
|
render: () => [
|
|
73
182
|
...headerLinesOf(options.header ?? defaultHeader(options.guard)),
|
|
74
183
|
"",
|
|
75
|
-
...
|
|
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/build.d.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* ships: the signature file both sides read, the declared options as plain
|
|
6
6
|
* data, and one guard's parameters as the least a browser needs of them, all
|
|
7
7
|
* from the app's own instance; and the contract a client compiles against,
|
|
8
|
-
* from the server's sources;
|
|
9
|
-
* every app a script names, in one call
|
|
8
|
+
* from the server's sources; generateApiFiles, every one of them for
|
|
9
|
+
* every app a script names, in one call; and checkApiRefusals, every
|
|
10
|
+
* refusal a handler can reach held to the codes it may send. Node-only and imported by nothing else
|
|
10
11
|
* in the package, so no deployment or bundle carries it.
|
|
11
12
|
*/
|
|
12
13
|
export { writeApiSignatures } from "./build/writeApiSignatures.js";
|
|
@@ -18,6 +19,8 @@ export type { LambderApiOptionsFileOptions, LambderApiOptionsFileResult, } from
|
|
|
18
19
|
export { writeApiGuardParams } from "./build/writeApiGuardParams.js";
|
|
19
20
|
export type { LambderApiGuardParamsFileOptions, LambderApiGuardParamsFileResult, } from "./build/writeApiGuardParams.js";
|
|
20
21
|
export type { LambderApiOptionsSource, LambderNameChanges } from "./build/generatedTables.js";
|
|
22
|
+
export { checkApiRefusals } from "./build/checkApiRefusals.js";
|
|
23
|
+
export type { LambderApiRefusalCheckOptions, LambderApiRefusalCheckResult, LambderRefusalCheckFinding, } from "./build/checkApiRefusals.js";
|
|
21
24
|
export { generateApiFiles } from "./build/generateApiFiles.js";
|
|
22
25
|
export type { LambderApiFilesConfig, LambderApiFilesApp, LambderApiFilesResult } from "./build/generateApiFiles.js";
|
|
23
26
|
export type { LambderModuleLocation } from "./build/moduleLocation.js";
|
package/dist/build.js
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* ships: the signature file both sides read, the declared options as plain
|
|
6
6
|
* data, and one guard's parameters as the least a browser needs of them, all
|
|
7
7
|
* from the app's own instance; and the contract a client compiles against,
|
|
8
|
-
* from the server's sources;
|
|
9
|
-
* every app a script names, in one call
|
|
8
|
+
* from the server's sources; generateApiFiles, every one of them for
|
|
9
|
+
* every app a script names, in one call; and checkApiRefusals, every
|
|
10
|
+
* refusal a handler can reach held to the codes it may send. Node-only and imported by nothing else
|
|
10
11
|
* in the package, so no deployment or bundle carries it.
|
|
11
12
|
*/
|
|
12
13
|
export { writeApiSignatures } from "./build/writeApiSignatures.js";
|
|
13
14
|
export { writeApiContract } from "./build/writeApiContract.js";
|
|
14
15
|
export { writeApiOptions } from "./build/writeApiOptions.js";
|
|
15
16
|
export { writeApiGuardParams } from "./build/writeApiGuardParams.js";
|
|
17
|
+
export { checkApiRefusals } from "./build/checkApiRefusals.js";
|
|
16
18
|
export { generateApiFiles } from "./build/generateApiFiles.js";
|
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/core/Lambder.d.ts
CHANGED
|
@@ -560,11 +560,12 @@ export default class Lambder<TApp extends LambderAppTypes = LambderPlainAppTypes
|
|
|
560
560
|
private runApi;
|
|
561
561
|
/**
|
|
562
562
|
* A thrown LambderApiRefusal (from a hook, say) as the structured API
|
|
563
|
-
* envelope: the
|
|
564
|
-
*
|
|
565
|
-
* the refusal came before the call reached it
|
|
566
|
-
*
|
|
567
|
-
* refusal goes out
|
|
563
|
+
* envelope: the pipeline's own answer to a refusal (answerRefusal), so
|
|
564
|
+
* it is checked against the endpoint the call names, its lazy group
|
|
565
|
+
* loaded if the refusal came before the call reached it, and one saying
|
|
566
|
+
* the session is over ends it. A name no API is registered under
|
|
567
|
+
* declares no code, so only an uncoded or a framework refusal goes out
|
|
568
|
+
* for it.
|
|
568
569
|
*/
|
|
569
570
|
private apiErrorResponse;
|
|
570
571
|
}
|
package/dist/core/Lambder.js
CHANGED
|
@@ -13,12 +13,12 @@ import { LambderFiles } from "./LambderFiles.js";
|
|
|
13
13
|
import { isLambderApiRefusal } from "../shared/wire/LambderApiRefusal.js";
|
|
14
14
|
import { LambderApiPipeline } from "../api/LambderApiPipeline.js";
|
|
15
15
|
import { LAMBDER_BACKEND_SWAP, LAMBDER_CRASH_WATCH } from "../shared/util/LambderTestingDoors.js";
|
|
16
|
-
import {
|
|
16
|
+
import { declaredRefusalVocabulary, readRefusalVocabulary, resolveAllowedRefusals, toRefusalCodes, } from "../api/LambderApiRefusals.js";
|
|
17
17
|
import { refuse } from "../shared/wire/LambderApiRefusal.js";
|
|
18
18
|
import { apiSignatureOf } from "../api/LambderApiSignature.js";
|
|
19
19
|
import { apiNameKeyOf } from "../shared/wire/LambderApiSignatureMap.js";
|
|
20
20
|
import { assertPlainData } from "../shared/util/assertPlainData.js";
|
|
21
|
-
import { apiNotFoundAnswer, envelopeAnswer,
|
|
21
|
+
import { apiNotFoundAnswer, envelopeAnswer, sessionExpiredAnswer, successEnvelope, versionExpiredAnswer, } from "../api/LambderApiEnvelope.js";
|
|
22
22
|
import { describePayloadKind, isObjectPayload } from "../shared/wire/LambderObjectPayload.js";
|
|
23
23
|
import { LambderApiOutputValidationError } from "../api/LambderApiOutputValidationError.js";
|
|
24
24
|
import { toGuardEntries } from "../api/LambderApiGuards.js";
|
|
@@ -1275,16 +1275,17 @@ export default class Lambder {
|
|
|
1275
1275
|
}
|
|
1276
1276
|
/**
|
|
1277
1277
|
* A thrown LambderApiRefusal (from a hook, say) as the structured API
|
|
1278
|
-
* envelope: the
|
|
1279
|
-
*
|
|
1280
|
-
* the refusal came before the call reached it
|
|
1281
|
-
*
|
|
1282
|
-
* refusal goes out
|
|
1278
|
+
* envelope: the pipeline's own answer to a refusal (answerRefusal), so
|
|
1279
|
+
* it is checked against the endpoint the call names, its lazy group
|
|
1280
|
+
* loaded if the refusal came before the call reached it, and one saying
|
|
1281
|
+
* the session is over ends it. A name no API is registered under
|
|
1282
|
+
* declares no code, so only an uncoded or a framework refusal goes out
|
|
1283
|
+
* for it.
|
|
1283
1284
|
*/
|
|
1284
1285
|
async apiErrorResponse(err, ctx) {
|
|
1285
1286
|
const apiName = ctx.apiName ?? "";
|
|
1286
|
-
const
|
|
1287
|
-
return responseFromAnswer(
|
|
1287
|
+
const refusals = (await this.registeredDefinitionOf(apiName))?.refusals ?? NO_DECLARED_REFUSALS;
|
|
1288
|
+
return responseFromAnswer(await this.pipeline.answerRefusal(apiName, refusals, err, ctx));
|
|
1288
1289
|
}
|
|
1289
1290
|
}
|
|
1290
1291
|
/** The init bound to one vocabulary (or none): the policy builders, refuse and create() that share it. */
|
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
|
+
};
|
|
@@ -10,7 +10,7 @@ export type LambderApiRefusalOptions = {
|
|
|
10
10
|
refusal?: LambderUncheckedRefusalMessage | string;
|
|
11
11
|
/** Sets the envelope's `notAuthorized` flag (routed to the caller's notAuthorizedHandler). */
|
|
12
12
|
notAuthorized?: boolean;
|
|
13
|
-
/** Sets the envelope's `sessionExpired` flag (the caller clears session cookies and calls sessionExpiredHandler). */
|
|
13
|
+
/** Sets the envelope's `sessionExpired` flag (the caller clears session cookies and calls sessionExpiredHandler); on an API call, the server also ends the session the call held. */
|
|
14
14
|
sessionExpired?: boolean;
|
|
15
15
|
/**
|
|
16
16
|
* HTTP status of the refusal response. Default 200: the envelope is the
|
|
@@ -178,7 +178,7 @@ export type LambderRefuseOptions = {
|
|
|
178
178
|
title?: string;
|
|
179
179
|
/** Sets the envelope's notAuthorized flag (routed to the caller's notAuthorizedHandler). A declared code's flag is its declaration's, and a refusal with such a code that sets it here is a crash. */
|
|
180
180
|
notAuthorized?: boolean;
|
|
181
|
-
/** Sets the envelope's sessionExpired flag. Not for a refusal with a declared code, whose flags are its declaration's. */
|
|
181
|
+
/** Sets the envelope's sessionExpired flag, and the server ends the session the call held. Not for a refusal with a declared code, whose flags are its declaration's. */
|
|
182
182
|
sessionExpired?: boolean;
|
|
183
183
|
/** HTTP status of the refusal. Default 200; avoid 5xx (caller treats as crash) and 422 (reserved for validation). A declared code's status is its declaration's, and a refusal with such a code that sets one here is a crash. */
|
|
184
184
|
statusCode?: LambderHttpStatusCode;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.1.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": [
|