@faable/auth-sdk 2.2.1 → 2.3.0
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/dist/FaableAuthApi.d.ts +65 -52
- package/dist/FaableAuthApi.js +67 -52
- package/dist/api/api-types.d.ts +1 -1
- package/dist/api/generated-client.d.ts +2879 -0
- package/dist/api/generated-client.js +720 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
- package/scripts/gen-client.mjs +205 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faable/auth-sdk",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"author": "Marc Pomar <marc@faable.com>",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"typescript": "^5.3.3"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"gentypes": "openapi-typescript ${AUTH_OPENAPI:-spec/openapi.json} -o src/api/types.ts",
|
|
28
|
+
"gentypes": "openapi-typescript ${AUTH_OPENAPI:-spec/openapi.json} -o src/api/types.ts && node scripts/gen-client.mjs",
|
|
29
29
|
"build": "rimraf dist && tsc",
|
|
30
30
|
"prebuild": "npm run gentypes",
|
|
31
31
|
"prepublishOnly": "npm run build",
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Generates `src/api/generated-client.ts` from the OpenAPI spec.
|
|
2
|
+
//
|
|
3
|
+
// Why: the SDK's typed methods (userGet, teamList, …) used to be hand-written
|
|
4
|
+
// and always lagged the server. The TYPES are already auto-generated by
|
|
5
|
+
// openapi-typescript; this script does the same for the METHODS, so the SDK
|
|
6
|
+
// covers the whole management API for free and stays in sync on every build.
|
|
7
|
+
//
|
|
8
|
+
// Scope: only operations whose `security` includes `bearerAuth` (the management
|
|
9
|
+
// API). OAuth/OIDC/session/login browser flows carry no bearerAuth and are
|
|
10
|
+
// skipped — they are not SDK convenience calls. All included operationIds have
|
|
11
|
+
// the stable `resource/action` shape, so the camelCase names come out clean.
|
|
12
|
+
//
|
|
13
|
+
// Run via `npm run gentypes` (after openapi-typescript). Output is committed and
|
|
14
|
+
// reviewed in diff like `types.ts`. Do not edit the output by hand.
|
|
15
|
+
|
|
16
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { dirname, resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const ROOT = resolve(__dirname, "..");
|
|
22
|
+
|
|
23
|
+
const SPEC = process.env.AUTH_OPENAPI || resolve(ROOT, "spec/openapi.json");
|
|
24
|
+
const OUT = resolve(ROOT, "src/api/generated-client.ts");
|
|
25
|
+
|
|
26
|
+
const spec = JSON.parse(readFileSync(SPEC, "utf8"));
|
|
27
|
+
|
|
28
|
+
// operationId (`resource/action`, possibly snake/kebab in the action) → camelCase
|
|
29
|
+
// method name. `user/list`→userList, `client/rotateSecret`→clientRotateSecret,
|
|
30
|
+
// `role/assign_users`→roleAssignUsers.
|
|
31
|
+
const toMethodName = (operationId) =>
|
|
32
|
+
operationId
|
|
33
|
+
.split(/[/_-]/)
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map((seg, i) =>
|
|
36
|
+
i === 0
|
|
37
|
+
? seg[0].toLowerCase() + seg.slice(1)
|
|
38
|
+
: seg[0].toUpperCase() + seg.slice(1),
|
|
39
|
+
)
|
|
40
|
+
.join("");
|
|
41
|
+
|
|
42
|
+
// `/team/{team_id}/member/check/{user_id}` → ["team_id", "user_id"], in order.
|
|
43
|
+
const pathParamNames = (path) =>
|
|
44
|
+
[...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
|
|
45
|
+
|
|
46
|
+
// `/user/{user_id}` → "/user/${user_id}" (template-literal body).
|
|
47
|
+
const toUrlTemplate = (path) =>
|
|
48
|
+
path.replace(/\{([^}]+)\}/g, (_, name) => "${" + name + "}");
|
|
49
|
+
|
|
50
|
+
const hasBearerAuth = (op) =>
|
|
51
|
+
Array.isArray(op.security) &&
|
|
52
|
+
op.security.some((req) => req && Object.keys(req).includes("bearerAuth"));
|
|
53
|
+
|
|
54
|
+
const successSchema = (op) => {
|
|
55
|
+
const responses = op.responses || {};
|
|
56
|
+
const res = responses["200"] || responses["201"];
|
|
57
|
+
return res?.content?.["application/json"]?.schema;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// A paginated list response is `{ next, results }` (see buildPaginator in
|
|
61
|
+
// sdk-base). Detected structurally so it tracks the server, not the op name.
|
|
62
|
+
const isPaginated = (op) => {
|
|
63
|
+
const schema = successSchema(op);
|
|
64
|
+
const req = schema?.required;
|
|
65
|
+
return Array.isArray(req) && req.includes("next") && req.includes("results");
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const escape = (s) => (s || "").replace(/\*\//g, "*\\/").replace(/\r?\n/g, " ");
|
|
69
|
+
|
|
70
|
+
// Collect + sort operations by operationId for a stable, reviewable diff.
|
|
71
|
+
const operations = [];
|
|
72
|
+
for (const [path, methods] of Object.entries(spec.paths || {})) {
|
|
73
|
+
for (const [httpMethod, op] of Object.entries(methods)) {
|
|
74
|
+
if (!op || typeof op !== "object" || !op.operationId) continue;
|
|
75
|
+
if (!["get", "post", "delete"].includes(httpMethod)) continue;
|
|
76
|
+
if (!hasBearerAuth(op)) continue;
|
|
77
|
+
operations.push({ path, httpMethod, op });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
operations.sort((a, b) => a.op.operationId.localeCompare(b.op.operationId));
|
|
81
|
+
|
|
82
|
+
const emitMethod = ({ path, httpMethod, op }) => {
|
|
83
|
+
const id = op.operationId;
|
|
84
|
+
const name = toMethodName(id);
|
|
85
|
+
const pathParams = pathParamNames(path);
|
|
86
|
+
const queryParams = (op.parameters || []).filter((p) => p.in === "query");
|
|
87
|
+
const hasBody = !!op.requestBody;
|
|
88
|
+
const paginated = isPaginated(op);
|
|
89
|
+
const url = "`" + toUrlTemplate(path) + "`";
|
|
90
|
+
|
|
91
|
+
const hasQuery = queryParams.length > 0;
|
|
92
|
+
const args = pathParams.map((p) => `${p}: string`);
|
|
93
|
+
if (hasBody) args.push(`data: OpBody<"${id}">`);
|
|
94
|
+
|
|
95
|
+
// The only POST without a query in the spec; if that ever changes the
|
|
96
|
+
// generator must learn to pass query on a POST (fetcher.post takes a strict
|
|
97
|
+
// FetcherConfig). Fail loud rather than silently drop the query params.
|
|
98
|
+
if (httpMethod === "post" && hasQuery) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`gen-client: POST with query params not supported yet (${id}). ` +
|
|
101
|
+
"Extend emitMethod to thread query params through fetcher.post's config.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let body;
|
|
106
|
+
if (paginated) {
|
|
107
|
+
args.push(`params?: Omit<OpQuery<"${id}">, "cursor" | "next">`);
|
|
108
|
+
// The paginator's request params are untyped (`any`), so array/number/enum
|
|
109
|
+
// query values pass straight through.
|
|
110
|
+
body = `return this.paginator<OpItem<"${id}">>({ url: ${url}, params });`;
|
|
111
|
+
} else if (httpMethod === "post") {
|
|
112
|
+
// fetcher.post rejects a falsy body ("empty body"), so bodyless POSTs (e.g.
|
|
113
|
+
// client/rotateSecret) send an empty object.
|
|
114
|
+
const data = hasBody ? "data" : "{}";
|
|
115
|
+
body = `return this.fetcher.post<OpResult<"${id}">>(${url}, ${data});`;
|
|
116
|
+
} else if (hasQuery) {
|
|
117
|
+
// GET/DELETE with query params route through fetcher.request: its `params`
|
|
118
|
+
// is untyped, sidestepping FetcherConfig's string-only constraint, and the
|
|
119
|
+
// GET path still goes through the ETag cache.
|
|
120
|
+
args.push(`params?: OpQuery<"${id}">`);
|
|
121
|
+
const method = httpMethod.toUpperCase();
|
|
122
|
+
body = `return this.fetcher.request<OpResult<"${id}">>({ method: "${method}", url: ${url}, params });`;
|
|
123
|
+
} else if (httpMethod === "delete") {
|
|
124
|
+
body = `return this.fetcher.delete<OpResult<"${id}">>(${url});`;
|
|
125
|
+
} else {
|
|
126
|
+
body = `return this.fetcher.get<OpResult<"${id}">>(${url});`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const guards = pathParams.map((p) => ` requireId("${p}", ${p});`);
|
|
130
|
+
const doc = op.summary || op.description;
|
|
131
|
+
const jsdoc = [
|
|
132
|
+
" /**",
|
|
133
|
+
` * \`${httpMethod.toUpperCase()} ${path}\` — operationId: \`${id}\``,
|
|
134
|
+
...(doc ? [` *`, ` * ${escape(doc)}`] : []),
|
|
135
|
+
" */",
|
|
136
|
+
].join("\n");
|
|
137
|
+
|
|
138
|
+
return [
|
|
139
|
+
jsdoc,
|
|
140
|
+
` ${name}(${args.join(", ")}) {`,
|
|
141
|
+
...guards,
|
|
142
|
+
` ${body}`,
|
|
143
|
+
" }",
|
|
144
|
+
].join("\n");
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const header = `// AUTO-GENERATED by scripts/gen-client.mjs — do not edit by hand.
|
|
148
|
+
// Regenerated from the OpenAPI spec on every build (npm run gentypes).
|
|
149
|
+
// Source of truth: the auth server's operationIds + bearerAuth security.
|
|
150
|
+
|
|
151
|
+
import type { operations } from "./types.js";
|
|
152
|
+
import { FaableApi } from "@faable/sdk-base";
|
|
153
|
+
import { requireId } from "../helpers.js";
|
|
154
|
+
|
|
155
|
+
// JSON body of an operation's request (typed from the generated \`operations\`).
|
|
156
|
+
type OpBody<K extends keyof operations> = operations[K] extends {
|
|
157
|
+
requestBody: { content: { "application/json": infer B } };
|
|
158
|
+
}
|
|
159
|
+
? B
|
|
160
|
+
: never;
|
|
161
|
+
|
|
162
|
+
// JSON body of an operation's 2xx response.
|
|
163
|
+
type Content2xx<R> = R extends {
|
|
164
|
+
200: { content: { "application/json": infer T } };
|
|
165
|
+
}
|
|
166
|
+
? T
|
|
167
|
+
: R extends { 201: { content: { "application/json": infer T } } }
|
|
168
|
+
? T
|
|
169
|
+
: void;
|
|
170
|
+
|
|
171
|
+
type OpResult<K extends keyof operations> = operations[K] extends {
|
|
172
|
+
responses: infer R;
|
|
173
|
+
}
|
|
174
|
+
? Content2xx<R>
|
|
175
|
+
: void;
|
|
176
|
+
|
|
177
|
+
// Item type of a paginated (\`{ next, results }\`) list response.
|
|
178
|
+
type OpItem<K extends keyof operations> =
|
|
179
|
+
OpResult<K> extends { results: (infer I)[] } ? I : never;
|
|
180
|
+
|
|
181
|
+
// Query parameters of an operation.
|
|
182
|
+
type OpQuery<K extends keyof operations> = operations[K] extends {
|
|
183
|
+
parameters: { query?: infer Q };
|
|
184
|
+
}
|
|
185
|
+
? NonNullable<Q>
|
|
186
|
+
: Record<string, never>;
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
const classBody = operations.map(emitMethod).join("\n\n");
|
|
190
|
+
|
|
191
|
+
const out = `${header}
|
|
192
|
+
/**
|
|
193
|
+
* Auto-generated management-API methods, one per \`bearerAuth\` operation in the
|
|
194
|
+
* OpenAPI spec. \`FaableAuthApi\` extends this and adds the constructor,
|
|
195
|
+
* custom-logic helpers, and deprecated aliases.
|
|
196
|
+
*/
|
|
197
|
+
export abstract class GeneratedFaableAuthApi extends FaableApi {
|
|
198
|
+
${classBody}
|
|
199
|
+
}
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
writeFileSync(OUT, out);
|
|
203
|
+
console.warn(
|
|
204
|
+
`gen-client: wrote ${operations.length} methods → ${OUT.replace(ROOT + "/", "")}`,
|
|
205
|
+
);
|