@willyim/idp 0.1.1 → 0.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/README.md +95 -12
- package/dist/src/api.d.ts +31 -55
- package/dist/src/api.d.ts.map +1 -1
- package/dist/src/api.js +16 -11
- package/dist/src/claims.d.ts +9 -34
- package/dist/src/claims.d.ts.map +1 -1
- package/dist/src/claims.js +12 -40
- package/dist/src/client.d.ts +30 -34
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +16 -22
- package/dist/src/drizzle/index.d.ts +78 -13
- package/dist/src/drizzle/index.d.ts.map +1 -1
- package/dist/src/errors.d.ts +8 -0
- package/dist/src/errors.d.ts.map +1 -0
- package/dist/src/errors.js +12 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/schemas/index.d.ts +323 -0
- package/dist/src/schemas/index.d.ts.map +1 -0
- package/dist/src/schemas/index.js +207 -0
- package/dist/src/schemas/openapi.d.ts +31 -0
- package/dist/src/schemas/openapi.d.ts.map +1 -0
- package/dist/src/schemas/openapi.js +111 -0
- package/dist/src/schemas/operations.d.ts +502 -0
- package/dist/src/schemas/operations.d.ts.map +1 -0
- package/dist/src/schemas/operations.js +213 -0
- package/dist/src/session.d.ts +17 -3
- package/dist/src/session.d.ts.map +1 -1
- package/dist/src/session.js +12 -1
- package/dist/src/user-keys.d.ts +124 -0
- package/dist/src/user-keys.d.ts.map +1 -0
- package/dist/src/user-keys.js +174 -0
- package/dist/src/validate.d.ts +13 -0
- package/dist/src/validate.d.ts.map +1 -0
- package/dist/src/validate.js +28 -0
- package/dist/src/wire.d.ts +164 -0
- package/dist/src/wire.d.ts.map +1 -0
- package/dist/src/wire.js +100 -0
- package/openapi/idp-api.json +1110 -17
- package/package.json +16 -7
- package/dist/src/generated/idp-api.d.ts +0 -1022
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire shapes of the IdP management API (`/api/v1/*`), as zod schemas.
|
|
3
|
+
*
|
|
4
|
+
* One definition, three consumers: `apps/idp` validates incoming requests with
|
|
5
|
+
* them, `scripts/generate-openapi.mjs` turns them into the OpenAPI document,
|
|
6
|
+
* and the SDK's `api.ts` parses responses against them. Types are `z.infer`,
|
|
7
|
+
* so there is no generated `.d.ts` to drift out of sync.
|
|
8
|
+
*
|
|
9
|
+
* The OIDC endpoints are NOT here — those are standards-defined and live in
|
|
10
|
+
* `../wire.ts`, next to the code that talks to them.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
export const ApplicationSchema = z.object({
|
|
14
|
+
clientId: z.string(),
|
|
15
|
+
name: z.string().nullable(),
|
|
16
|
+
app: z.string().nullable().describe("Application key; consumer workspace claims are filtered by this"),
|
|
17
|
+
allowSignup: z.boolean().describe("Whether unknown users may sign themselves up"),
|
|
18
|
+
permissions: z.array(z.string()).describe("The app's declared product-permission catalog"),
|
|
19
|
+
redirectUris: z.array(z.string()),
|
|
20
|
+
disabled: z.boolean(),
|
|
21
|
+
createdAt: z.string().describe("ISO 8601 timestamp"),
|
|
22
|
+
});
|
|
23
|
+
export const UserSchema = z.object({
|
|
24
|
+
id: z.string(),
|
|
25
|
+
email: z.string(),
|
|
26
|
+
name: z.string().nullable(),
|
|
27
|
+
emailVerified: z.boolean(),
|
|
28
|
+
createdAt: z.string(),
|
|
29
|
+
});
|
|
30
|
+
export const WorkspaceSchema = z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
name: z.string(),
|
|
33
|
+
slug: z.string(),
|
|
34
|
+
applicationId: z.string().nullable(),
|
|
35
|
+
createdAt: z.string(),
|
|
36
|
+
});
|
|
37
|
+
export const ApplicationListSchema = z.object({ applications: z.array(ApplicationSchema) });
|
|
38
|
+
/**
|
|
39
|
+
* Register an application. Superadmin only — there is no app to scope it to
|
|
40
|
+
* yet, so no per-app permission could authorize it.
|
|
41
|
+
*/
|
|
42
|
+
export const CreateApplicationInput = z.object({
|
|
43
|
+
name: z.string().min(1),
|
|
44
|
+
app: z
|
|
45
|
+
.string()
|
|
46
|
+
.min(1)
|
|
47
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, numbers and dashes only")
|
|
48
|
+
.describe("Stable application key — the join key for members, workspaces and keys"),
|
|
49
|
+
redirectUris: z.array(z.string().min(1)).min(1),
|
|
50
|
+
firstAdminUserId: z
|
|
51
|
+
.string()
|
|
52
|
+
.nullable()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe("Who becomes the app's first IdP admin. Defaults to the calling user; null leaves the app with no members (superadmin-managed)."),
|
|
55
|
+
});
|
|
56
|
+
export const ApplicationCreatedSchema = z.object({
|
|
57
|
+
clientId: z.string(),
|
|
58
|
+
clientSecret: z.string().describe("Plaintext client secret — shown exactly once, never stored"),
|
|
59
|
+
app: z.string(),
|
|
60
|
+
});
|
|
61
|
+
/** Partial update of an application. Omitted fields are left alone. */
|
|
62
|
+
export const UpdateApplicationInput = z.object({
|
|
63
|
+
name: z.string().min(1).optional(),
|
|
64
|
+
redirectUris: z.array(z.string().min(1)).min(1).optional(),
|
|
65
|
+
allowSignup: z.boolean().optional(),
|
|
66
|
+
});
|
|
67
|
+
export const ClientSecretSchema = z.object({
|
|
68
|
+
clientSecret: z.string().describe("Plaintext client secret — shown exactly once, never stored"),
|
|
69
|
+
});
|
|
70
|
+
/** Replaces the app's product-permission catalog wholesale. */
|
|
71
|
+
export const SetAppPermissionsInput = z.object({
|
|
72
|
+
permissions: z.array(z.string().min(1)),
|
|
73
|
+
});
|
|
74
|
+
export const AppPermissionsSchema = z.object({ permissions: z.array(z.string()) });
|
|
75
|
+
export const UserListSchema = z.object({ users: z.array(UserSchema) });
|
|
76
|
+
export const WorkspaceListSchema = z.object({ workspaces: z.array(WorkspaceSchema) });
|
|
77
|
+
// --- Write management API (scoped-key authenticated, per-app) ---
|
|
78
|
+
export const RoleSchema = z.enum(["admin", "member"]);
|
|
79
|
+
export const MemberSchema = z.object({
|
|
80
|
+
userId: z.string(),
|
|
81
|
+
email: z.string(),
|
|
82
|
+
name: z.string().nullable(),
|
|
83
|
+
role: RoleSchema,
|
|
84
|
+
permissions: z.array(z.string()),
|
|
85
|
+
});
|
|
86
|
+
export const MemberListSchema = z.object({ members: z.array(MemberSchema) });
|
|
87
|
+
/** Add (existing user) or invite (new email) an app member. */
|
|
88
|
+
export const InviteMemberInput = z.object({
|
|
89
|
+
email: z.string().email(),
|
|
90
|
+
role: RoleSchema.default("member"),
|
|
91
|
+
permissions: z.array(z.string()).default([]),
|
|
92
|
+
});
|
|
93
|
+
export const InviteMemberResult = z.object({
|
|
94
|
+
// "added" = existing user joined now; "invited" = pending invite emailed.
|
|
95
|
+
result: z.enum(["added", "invited"]),
|
|
96
|
+
});
|
|
97
|
+
export const UpdateMemberInput = z.object({
|
|
98
|
+
role: RoleSchema,
|
|
99
|
+
permissions: z.array(z.string()).default([]),
|
|
100
|
+
});
|
|
101
|
+
export const CreateWorkspaceInput = z.object({
|
|
102
|
+
name: z.string().min(1),
|
|
103
|
+
slug: z
|
|
104
|
+
.string()
|
|
105
|
+
.min(1)
|
|
106
|
+
.regex(/^[a-z0-9-]+$/, "lowercase letters, numbers and dashes only"),
|
|
107
|
+
});
|
|
108
|
+
export const WorkspaceCreatedSchema = z.object({
|
|
109
|
+
id: z.string(),
|
|
110
|
+
name: z.string(),
|
|
111
|
+
slug: z.string(),
|
|
112
|
+
});
|
|
113
|
+
export const OkSchema = z.object({ ok: z.literal(true) });
|
|
114
|
+
// --- End-user API keys (the app's own API credentials, stored in the IdP) ---
|
|
115
|
+
export const UserApiKeySchema = z.object({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
userId: z.string(),
|
|
118
|
+
workspaceId: z.string().nullable(),
|
|
119
|
+
name: z.string(),
|
|
120
|
+
prefix: z.string().describe("Non-secret token prefix (wak_…) for display"),
|
|
121
|
+
scopes: z.array(z.string()),
|
|
122
|
+
status: z.enum(["active", "expired", "revoked"]),
|
|
123
|
+
createdAt: z.string(),
|
|
124
|
+
lastUsedAt: z.string().nullable(),
|
|
125
|
+
expiresAt: z.string().nullable(),
|
|
126
|
+
});
|
|
127
|
+
export const UserApiKeyListSchema = z.object({ keys: z.array(UserApiKeySchema) });
|
|
128
|
+
export const CreateUserApiKeyInput = z.object({
|
|
129
|
+
userId: z.string().min(1),
|
|
130
|
+
name: z.string().min(1),
|
|
131
|
+
scopes: z.array(z.string()).default([]).describe("Subset of the app's product permission catalog"),
|
|
132
|
+
workspaceId: z.string().optional(),
|
|
133
|
+
expiresAt: z.iso.datetime().optional().describe("ISO 8601; omit for non-expiring"),
|
|
134
|
+
});
|
|
135
|
+
export const UserApiKeyCreatedSchema = z.object({
|
|
136
|
+
id: z.string(),
|
|
137
|
+
token: z.string().describe("Plaintext key — shown exactly once, never stored"),
|
|
138
|
+
prefix: z.string(),
|
|
139
|
+
});
|
|
140
|
+
export const ValidateUserApiKeyInput = z.object({ token: z.string().min(1) });
|
|
141
|
+
export const UserApiKeyValidationSchema = z.union([
|
|
142
|
+
z.object({
|
|
143
|
+
valid: z.literal(true),
|
|
144
|
+
keyId: z.string(),
|
|
145
|
+
userId: z.string(),
|
|
146
|
+
workspaceId: z.string().nullable(),
|
|
147
|
+
scopes: z.array(z.string()),
|
|
148
|
+
name: z.string(),
|
|
149
|
+
}),
|
|
150
|
+
z.object({ valid: z.literal(false), reason: z.enum(["not_found", "revoked", "expired"]) }),
|
|
151
|
+
]);
|
|
152
|
+
export const AuditEntrySchema = z.object({
|
|
153
|
+
id: z.number(),
|
|
154
|
+
tableName: z.string(),
|
|
155
|
+
operation: z.string(),
|
|
156
|
+
rowId: z.string().nullable(),
|
|
157
|
+
userId: z.string().nullable(),
|
|
158
|
+
actor: z.string().nullable(),
|
|
159
|
+
createdAt: z.string(),
|
|
160
|
+
});
|
|
161
|
+
export const AuditListSchema = z.object({ entries: z.array(AuditEntrySchema) });
|
|
162
|
+
// --- Scoped management API keys (drive this API for one app) ---
|
|
163
|
+
export const ApiKeySchema = z.object({
|
|
164
|
+
id: z.string(),
|
|
165
|
+
name: z.string(),
|
|
166
|
+
prefix: z.string().describe("Non-secret token prefix (wim_…) for display"),
|
|
167
|
+
permissions: z.array(z.string()).describe("IdP management permissions this key holds"),
|
|
168
|
+
status: z.enum(["active", "expired", "revoked"]),
|
|
169
|
+
createdAt: z.string(),
|
|
170
|
+
lastUsedAt: z.string().nullable(),
|
|
171
|
+
expiresAt: z.string().nullable(),
|
|
172
|
+
revokedAt: z.string().nullable(),
|
|
173
|
+
});
|
|
174
|
+
export const ApiKeyListSchema = z.object({ keys: z.array(ApiKeySchema) });
|
|
175
|
+
export const CreateApiKeyInput = z.object({
|
|
176
|
+
name: z.string().min(1),
|
|
177
|
+
permissions: z
|
|
178
|
+
.array(z.string().min(1))
|
|
179
|
+
.describe("IdP management permissions; must be a subset of the caller's own on this app"),
|
|
180
|
+
expiresAt: z.iso.datetime().optional().describe("ISO 8601; omit for non-expiring"),
|
|
181
|
+
});
|
|
182
|
+
export const ApiKeyCreatedSchema = z.object({
|
|
183
|
+
id: z.string(),
|
|
184
|
+
token: z.string().describe("Plaintext key — shown exactly once, never stored"),
|
|
185
|
+
prefix: z.string(),
|
|
186
|
+
});
|
|
187
|
+
// --- IdP-level admin keys (superadmin over every app) ---
|
|
188
|
+
export const AdminKeySchema = z.object({
|
|
189
|
+
id: z.string(),
|
|
190
|
+
name: z.string(),
|
|
191
|
+
prefix: z.string().describe("Non-secret token prefix (wim_…) for display"),
|
|
192
|
+
status: z.enum(["active", "expired", "revoked"]),
|
|
193
|
+
createdAt: z.string(),
|
|
194
|
+
lastUsedAt: z.string().nullable(),
|
|
195
|
+
expiresAt: z.string().nullable(),
|
|
196
|
+
revokedAt: z.string().nullable(),
|
|
197
|
+
});
|
|
198
|
+
export const AdminKeyListSchema = z.object({ keys: z.array(AdminKeySchema) });
|
|
199
|
+
export const CreateAdminKeyInput = z.object({
|
|
200
|
+
name: z.string().min(1).describe("Who holds it — one key per agent, so the audit log reads"),
|
|
201
|
+
expiresAt: z.iso.datetime().optional().describe("ISO 8601; omit for non-expiring"),
|
|
202
|
+
});
|
|
203
|
+
export const AdminKeyCreatedSchema = z.object({
|
|
204
|
+
id: z.string(),
|
|
205
|
+
token: z.string().describe("Plaintext key — shown exactly once, never stored"),
|
|
206
|
+
prefix: z.string(),
|
|
207
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The published OpenAPI document, built from `./operations.ts`.
|
|
3
|
+
*
|
|
4
|
+
* `apps/idp` serves this at `/api/v1/openapi.json`; `npm run openapi` writes
|
|
5
|
+
* the same object to `openapi/idp-api.json` so the snapshot in the repo is
|
|
6
|
+
* always what the server would answer.
|
|
7
|
+
*/
|
|
8
|
+
export declare function buildOpenApiDocument(input: {
|
|
9
|
+
baseUrl: string;
|
|
10
|
+
}): {
|
|
11
|
+
openapi: string;
|
|
12
|
+
info: {
|
|
13
|
+
title: string;
|
|
14
|
+
version: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
servers: {
|
|
18
|
+
url: string;
|
|
19
|
+
}[];
|
|
20
|
+
components: {
|
|
21
|
+
securitySchemes: {
|
|
22
|
+
bearerAuth: {
|
|
23
|
+
type: string;
|
|
24
|
+
scheme: string;
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
paths: Record<string, Record<string, unknown>>;
|
|
30
|
+
};
|
|
31
|
+
//# sourceMappingURL=openapi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../../src/schemas/openapi.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqFH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;;;;EA6B9D"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The published OpenAPI document, built from `./operations.ts`.
|
|
3
|
+
*
|
|
4
|
+
* `apps/idp` serves this at `/api/v1/openapi.json`; `npm run openapi` writes
|
|
5
|
+
* the same object to `openapi/idp-api.json` so the snapshot in the repo is
|
|
6
|
+
* always what the server would answer.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { lookup, operations } from "./operations.js";
|
|
10
|
+
/**
|
|
11
|
+
* Responses describe what the API *returns*, so they use zod's output types.
|
|
12
|
+
* Request bodies describe what a caller may *send*, which is a different shape:
|
|
13
|
+
* a field with a `.default()` is optional on the way in and guaranteed on the
|
|
14
|
+
* way out. Emitting output types for both would mark every defaulted field
|
|
15
|
+
* `required` and force callers to supply values the API would have defaulted.
|
|
16
|
+
*/
|
|
17
|
+
const json = (schema) => z.toJSONSchema(schema);
|
|
18
|
+
const jsonInput = (schema) => z.toJSONSchema(schema, { io: "input" });
|
|
19
|
+
const DESCRIPTION = "Management API for the willy.im identity provider. Authenticate with `Authorization: Bearer <token>`. Two kinds of token: the superadmin `ADMIN_API_TOKEN` (every app) and per-app **scoped API keys** minted in the admin console (one app, a fixed permission set). The cross-app endpoints below require the superadmin token.";
|
|
20
|
+
function pathParamNames(path) {
|
|
21
|
+
return [...path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
|
|
22
|
+
}
|
|
23
|
+
function parametersFor(path, def) {
|
|
24
|
+
const pathParams = pathParamNames(path).map((name) => ({
|
|
25
|
+
name,
|
|
26
|
+
in: "path",
|
|
27
|
+
required: true,
|
|
28
|
+
...(def.params?.[name] ? { description: def.params[name] } : {}),
|
|
29
|
+
schema: { type: "string" },
|
|
30
|
+
}));
|
|
31
|
+
const queryParams = (def.query ?? []).map((param) => ({
|
|
32
|
+
name: param.name,
|
|
33
|
+
in: "query",
|
|
34
|
+
required: param.required ?? false,
|
|
35
|
+
...(param.description ? { description: param.description } : {}),
|
|
36
|
+
schema: { type: "string" },
|
|
37
|
+
}));
|
|
38
|
+
return [...pathParams, ...queryParams];
|
|
39
|
+
}
|
|
40
|
+
function operationObject(method, path, def) {
|
|
41
|
+
const scoped = def.permission !== undefined;
|
|
42
|
+
const write = method !== "get";
|
|
43
|
+
const parameters = parametersFor(path, def);
|
|
44
|
+
return {
|
|
45
|
+
summary: def.summary,
|
|
46
|
+
description: def.description ??
|
|
47
|
+
(scoped
|
|
48
|
+
? `Requires \`${def.permission}\` on the path app (or the superadmin token).`
|
|
49
|
+
: "Requires the superadmin token."),
|
|
50
|
+
security: [{ bearerAuth: [] }],
|
|
51
|
+
...(parameters.length ? { parameters } : {}),
|
|
52
|
+
...(def.input
|
|
53
|
+
? {
|
|
54
|
+
requestBody: {
|
|
55
|
+
required: true,
|
|
56
|
+
content: { "application/json": { schema: jsonInput(def.input) } },
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
responses: {
|
|
61
|
+
[def.successCode]: {
|
|
62
|
+
description: "OK",
|
|
63
|
+
content: { "application/json": { schema: json(def.success) } },
|
|
64
|
+
},
|
|
65
|
+
"401": { description: "Missing or invalid bearer token" },
|
|
66
|
+
...(scoped
|
|
67
|
+
? { "403": { description: "Key lacks the permission / is bound to another app" } }
|
|
68
|
+
: {}),
|
|
69
|
+
...(write
|
|
70
|
+
? {
|
|
71
|
+
"405": {
|
|
72
|
+
description: "Method not allowed on this resource (see the `Allow` header)",
|
|
73
|
+
},
|
|
74
|
+
"409": { description: "Conflict (already a member, last admin, slug taken, …)" },
|
|
75
|
+
}
|
|
76
|
+
: {}),
|
|
77
|
+
...(def.notFound ? { "404": { description: def.notFound } } : {}),
|
|
78
|
+
...(def.input ? { "422": { description: "Body failed validation" } } : {}),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function buildOpenApiDocument(input) {
|
|
83
|
+
const paths = {};
|
|
84
|
+
for (const key of Object.keys(operations)) {
|
|
85
|
+
const [method, path] = key.split(" ");
|
|
86
|
+
const def = lookup(method, path);
|
|
87
|
+
if (!def)
|
|
88
|
+
continue;
|
|
89
|
+
paths[path] ??= {};
|
|
90
|
+
paths[path][method] = operationObject(method, path, def);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
openapi: "3.1.0",
|
|
94
|
+
info: {
|
|
95
|
+
title: "willy.im IdP — Management API",
|
|
96
|
+
version: "1.0.0",
|
|
97
|
+
description: DESCRIPTION,
|
|
98
|
+
},
|
|
99
|
+
servers: [{ url: input.baseUrl }],
|
|
100
|
+
components: {
|
|
101
|
+
securitySchemes: {
|
|
102
|
+
bearerAuth: {
|
|
103
|
+
type: "http",
|
|
104
|
+
scheme: "bearer",
|
|
105
|
+
description: "Superadmin ADMIN_API_TOKEN or a scoped API key (wim_…).",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
paths,
|
|
110
|
+
};
|
|
111
|
+
}
|