apiblaze 0.17.21 → 0.17.23

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.
@@ -1,11 +1,143 @@
1
1
  /**
2
- * apiblaze/server — the server-side half of the API-key widget.
2
+ * apiblaze/server — SHARED widget-server substrate.
3
+ *
4
+ * One CP credential, one getUser contract, one ensure-tenant/ensure-user path —
5
+ * consumed by BOTH widget faces (`createApiblazeKeys` and `createApiblazeGroups`).
6
+ * Factored per iam_toolkit_spec ("share, don't fork" — copy-forking this
7
+ * bootstrap is a spec failure).
8
+ *
9
+ * Trust model: the CP key stays server-side and vouches for the CHANNEL; the
10
+ * acting end-user rides X-End-User-Id / X-End-User-Email. On the apikeys plane
11
+ * the end-user header only narrows (self-scoped key ops); on the iam plane it
12
+ * IS the principal (iam-worker independently checks the resolved user's
13
+ * apiblaze_admins membership before any group op).
14
+ */
15
+ interface AppUser {
16
+ /** The producer's stable id for the consumer COMPANY — becomes the tenant (isolation). */
17
+ tenant: string;
18
+ /** The producer's stable id for the PERSON — the acting end-user. */
19
+ userId: string;
20
+ /** Optional; REQUIRED in practice for the groups widget — tenant-admin status
21
+ * is granted by email (the tenant's admin-emails allowlist). */
22
+ email?: string;
23
+ /** Optional display name (cosmetic). */
24
+ label?: string;
25
+ /** Key-widget eligibility (see createApiblazeKeys). Ignored by the groups widget. */
26
+ keyTypes?: string[] | false;
27
+ /** Any extra fields your eligibility logic wants (e.g. isEngineer). */
28
+ [k: string]: unknown;
29
+ }
30
+ declare class PlaneError extends Error {
31
+ status: number;
32
+ detail: string;
33
+ constructor(status: number, detail: string);
34
+ }
35
+
36
+ /**
37
+ * createApiblazeGroups — the server-side half of the Users & Groups widget
38
+ * (<UsersGroupsWidget/>), riding the SAME shared substrate (core.ts) and the
39
+ * SAME one CP widget credential as the API-key widget.
40
+ *
41
+ * Usage (Next.js App Router — app/api/apiblaze/groups/route.ts):
42
+ *
43
+ * import { createApiblazeGroups } from 'apiblaze/server';
44
+ * const groups = createApiblazeGroups({
45
+ * cpKey: process.env.APIBLAZE_CP_KEY!, // the SAME key the key widget uses
46
+ * getUser: async () => { ...same contract... return { tenant, userId, email } },
47
+ * });
48
+ * export const GET = groups.handler;
49
+ * export const POST = groups.handler;
50
+ *
51
+ * AUTHORIZATION MODEL (iam_toolkit_spec): unlike key ops (self-scoped), group
52
+ * admin ops are tenant-wide — so the acting END-USER IS THE PRINCIPAL. This
53
+ * server forwards the session user via X-End-User-Id/-Email; apiblaze's iam
54
+ * plane independently verifies that user is a TENANT ADMIN (apiblaze_admins
55
+ * member, granted via the tenant's admin-emails allowlist) before any op. A
56
+ * non-admin gets `access: 'pending'` — nothing here can elevate them, and no
57
+ * producer-side flag can either. The CP key is only the channel.
58
+ *
59
+ * Strictly users + groups — NO authorization-rule authoring (that stays in
60
+ * `apiblaze authz` / the dashboard).
61
+ */
62
+
63
+ interface ApiblazeGroupsConfig {
64
+ /** The producer CP key — the SAME widget key the API-key widget uses. Server-only. */
65
+ cpKey: string;
66
+ /** Return the logged-in user from the request/session, or null if unauthenticated.
67
+ * `email` is how tenant-admin status is granted (admin-emails allowlist) — pass it. */
68
+ getUser: (req: Request) => Promise<AppUser | null> | AppUser | null;
69
+ /** Override the apikeys plane base (tenant/user provisioning). Default https://apikeys.apiblaze.com */
70
+ base?: string;
71
+ /** Override the iam plane base. Default https://iam.apiblaze.com */
72
+ iamBase?: string;
73
+ }
74
+ type GroupsAction = {
75
+ action: 'snapshot';
76
+ } | {
77
+ action: 'create-group';
78
+ name: string;
79
+ description?: string;
80
+ } | {
81
+ action: 'rename-group';
82
+ groupId: string;
83
+ name?: string;
84
+ description?: string;
85
+ } | {
86
+ action: 'delete-group';
87
+ groupId: string;
88
+ } | {
89
+ action: 'add-member';
90
+ groupId: string;
91
+ abzSub: string;
92
+ role?: 'member' | 'admin';
93
+ } | {
94
+ action: 'change-role';
95
+ groupId: string;
96
+ abzSub: string;
97
+ role: 'member' | 'admin';
98
+ } | {
99
+ action: 'remove-member';
100
+ groupId: string;
101
+ abzSub: string;
102
+ } | {
103
+ action: 'add-subgroup';
104
+ groupId: string;
105
+ childGroupId: string;
106
+ } | {
107
+ action: 'remove-subgroup';
108
+ groupId: string;
109
+ childGroupId: string;
110
+ } | {
111
+ action: 'group-detail';
112
+ groupId: string;
113
+ } | {
114
+ action: 'provision-observed';
115
+ consumerUserId: string;
116
+ email?: string;
117
+ displayName?: string;
118
+ groups?: string[];
119
+ };
120
+ declare function createApiblazeGroups(cfg: ApiblazeGroupsConfig): {
121
+ handler: (req: Request) => Promise<Response>;
122
+ run: (user: AppUser, op: GroupsAction) => Promise<{
123
+ status: number;
124
+ data: unknown;
125
+ }>;
126
+ };
127
+
128
+ /**
129
+ * apiblaze/server — the server-side half of the apiblaze widgets.
3
130
  *
4
131
  * Holds the producer's CP key (server-only, never shipped to the browser) and turns
5
- * a logged-in user into API-key operations against apiblaze.com. The browser widget
6
- * (`apiblaze/react`) talks ONLY to your own backend; your backend talks to apiblaze —
7
- * so there are no cross-domain cookies, no secrets in the client, and no auth wiring
8
- * beyond "read my session."
132
+ * a logged-in user into apiblaze operations. The browser widgets (`apiblaze/react`)
133
+ * talk ONLY to your own backend; your backend talks to apiblaze — so there are no
134
+ * cross-domain cookies, no secrets in the client, and no auth wiring beyond
135
+ * "read my session."
136
+ *
137
+ * TWO faces over ONE shared substrate (core.ts — one CP credential, one
138
+ * ensure-tenant/ensure-user path):
139
+ * - createApiblazeKeys → API-key management (<ApiKeyWidget/>)
140
+ * - createApiblazeGroups → users & groups IAM (<UsersGroupsWidget/>)
9
141
  *
10
142
  * Usage (Next.js App Router — app/api/apiblaze/keys/route.ts):
11
143
  *
@@ -17,34 +149,41 @@
17
149
  * getUser: async () => {
18
150
  * const s = await auth();
19
151
  * if (!s?.user) return null;
20
- * return { tenant: s.user.orgId ?? s.user.id, userId: s.user.id, email: s.user.email ?? undefined, label: s.user.name ?? undefined };
152
+ * return {
153
+ * tenant: s.user.orgId ?? s.user.id,
154
+ * userId: s.user.id,
155
+ * email: s.user.email ?? undefined,
156
+ * // Which key types this person may create. Omit → ['call-only'].
157
+ * // false → no API access. A list of 2+ → the widget shows a picker.
158
+ * keyTypes: s.user.isEngineer ? ['manager', 'call-only'] : undefined,
159
+ * };
21
160
  * },
22
- * resolveScopes: (u) => u.isEngineer ? ['call', 'use-premium'] : ['call'], // optional
23
161
  * });
24
162
  * export const GET = keys.handler;
25
163
  * export const POST = keys.handler;
164
+ *
165
+ * SECURITY: eligibility is decided HERE, on your server, from your session —
166
+ * never from the browser. When a user is eligible for more than one type the
167
+ * browser sends its pick, but this handler only honors it if it's in the user's
168
+ * allowed set; otherwise it's rejected. `keyTypes: false` is enforced server-side
169
+ * (403) before anything is provisioned, not merely hidden in the UI.
26
170
  */
27
- interface AppUser {
28
- /** The producer's stable id for the consumer COMPANY — becomes the tenant (isolation). */
29
- tenant: string;
30
- /** The producer's stable id for the PERSON — the key creator/owner (list scope). */
31
- userId: string;
32
- /** Optional, stored now for possible future portal parity. */
33
- email?: string;
34
- /** Optional display name (cosmetic). */
35
- label?: string;
36
- /** Any extra fields your resolveScopes wants (e.g. isEngineer). */
37
- [k: string]: unknown;
38
- }
171
+
39
172
  interface ApiblazeKeysConfig {
40
- /** The producer CP key from the apiblaze dashboard Developers section. Server-only. */
173
+ /** The producer CP key from the apiblaze dashboard Developers section. Server-only.
174
+ * Use a purpose-built "widget" key (not a full admin key) so the platform's own
175
+ * subset rule is a real backstop under your eligibility logic. */
41
176
  cpKey: string;
42
177
  /** Return the logged-in user from the request/session, or null if unauthenticated. */
43
178
  getUser: (req: Request) => Promise<AppUser | null> | AppUser | null;
44
- /** Permission set to mint for a user. Default ['call']. Server-side only, so a browser
45
- * can never escalate return more for your own engineers here. */
46
- resolveScopes?: (user: AppUser) => string[] | Promise<string[]>;
47
- /** Default expiry for minted keys (seconds); enables re-reveal. Default 1 year. */
179
+ /** Optional override for eligibility, if you'd rather compute it here than inline
180
+ * in getUser. Same contract as AppUser.keyTypes (undefined default, false/[] → deny).
181
+ * Takes precedence over user.keyTypes when provided. */
182
+ resolveKeyTypes?: (user: AppUser) => (string[] | false) | Promise<string[] | false>;
183
+ /** Key lifetime in seconds. Omit for a DURABLE key (no expiry) — the right
184
+ * default for a production API key a developer embeds, so it never silently
185
+ * stops working. A durable key is shown ONCE at creation (not re-revealable);
186
+ * set an expiry if you want keys that stay revealable until they expire. */
48
187
  keyExpiresInSeconds?: number;
49
188
  /** Override the apiblaze base (tests/self-host). Default https://apikeys.apiblaze.com */
50
189
  base?: string;
@@ -53,6 +192,7 @@ type Action = {
53
192
  action: 'list';
54
193
  } | {
55
194
  action: 'create';
195
+ keyType?: string;
56
196
  } | {
57
197
  action: 'reveal';
58
198
  keyId: string;
@@ -71,4 +211,4 @@ declare function createApiblazeKeys(cfg: ApiblazeKeysConfig): {
71
211
  }>;
72
212
  };
73
213
 
74
- export { type ApiblazeKeysConfig, type AppUser, createApiblazeKeys };
214
+ export { type ApiblazeGroupsConfig, type ApiblazeKeysConfig, type AppUser, type GroupsAction, PlaneError, createApiblazeGroups, createApiblazeKeys };
@@ -1,11 +1,143 @@
1
1
  /**
2
- * apiblaze/server — the server-side half of the API-key widget.
2
+ * apiblaze/server — SHARED widget-server substrate.
3
+ *
4
+ * One CP credential, one getUser contract, one ensure-tenant/ensure-user path —
5
+ * consumed by BOTH widget faces (`createApiblazeKeys` and `createApiblazeGroups`).
6
+ * Factored per iam_toolkit_spec ("share, don't fork" — copy-forking this
7
+ * bootstrap is a spec failure).
8
+ *
9
+ * Trust model: the CP key stays server-side and vouches for the CHANNEL; the
10
+ * acting end-user rides X-End-User-Id / X-End-User-Email. On the apikeys plane
11
+ * the end-user header only narrows (self-scoped key ops); on the iam plane it
12
+ * IS the principal (iam-worker independently checks the resolved user's
13
+ * apiblaze_admins membership before any group op).
14
+ */
15
+ interface AppUser {
16
+ /** The producer's stable id for the consumer COMPANY — becomes the tenant (isolation). */
17
+ tenant: string;
18
+ /** The producer's stable id for the PERSON — the acting end-user. */
19
+ userId: string;
20
+ /** Optional; REQUIRED in practice for the groups widget — tenant-admin status
21
+ * is granted by email (the tenant's admin-emails allowlist). */
22
+ email?: string;
23
+ /** Optional display name (cosmetic). */
24
+ label?: string;
25
+ /** Key-widget eligibility (see createApiblazeKeys). Ignored by the groups widget. */
26
+ keyTypes?: string[] | false;
27
+ /** Any extra fields your eligibility logic wants (e.g. isEngineer). */
28
+ [k: string]: unknown;
29
+ }
30
+ declare class PlaneError extends Error {
31
+ status: number;
32
+ detail: string;
33
+ constructor(status: number, detail: string);
34
+ }
35
+
36
+ /**
37
+ * createApiblazeGroups — the server-side half of the Users & Groups widget
38
+ * (<UsersGroupsWidget/>), riding the SAME shared substrate (core.ts) and the
39
+ * SAME one CP widget credential as the API-key widget.
40
+ *
41
+ * Usage (Next.js App Router — app/api/apiblaze/groups/route.ts):
42
+ *
43
+ * import { createApiblazeGroups } from 'apiblaze/server';
44
+ * const groups = createApiblazeGroups({
45
+ * cpKey: process.env.APIBLAZE_CP_KEY!, // the SAME key the key widget uses
46
+ * getUser: async () => { ...same contract... return { tenant, userId, email } },
47
+ * });
48
+ * export const GET = groups.handler;
49
+ * export const POST = groups.handler;
50
+ *
51
+ * AUTHORIZATION MODEL (iam_toolkit_spec): unlike key ops (self-scoped), group
52
+ * admin ops are tenant-wide — so the acting END-USER IS THE PRINCIPAL. This
53
+ * server forwards the session user via X-End-User-Id/-Email; apiblaze's iam
54
+ * plane independently verifies that user is a TENANT ADMIN (apiblaze_admins
55
+ * member, granted via the tenant's admin-emails allowlist) before any op. A
56
+ * non-admin gets `access: 'pending'` — nothing here can elevate them, and no
57
+ * producer-side flag can either. The CP key is only the channel.
58
+ *
59
+ * Strictly users + groups — NO authorization-rule authoring (that stays in
60
+ * `apiblaze authz` / the dashboard).
61
+ */
62
+
63
+ interface ApiblazeGroupsConfig {
64
+ /** The producer CP key — the SAME widget key the API-key widget uses. Server-only. */
65
+ cpKey: string;
66
+ /** Return the logged-in user from the request/session, or null if unauthenticated.
67
+ * `email` is how tenant-admin status is granted (admin-emails allowlist) — pass it. */
68
+ getUser: (req: Request) => Promise<AppUser | null> | AppUser | null;
69
+ /** Override the apikeys plane base (tenant/user provisioning). Default https://apikeys.apiblaze.com */
70
+ base?: string;
71
+ /** Override the iam plane base. Default https://iam.apiblaze.com */
72
+ iamBase?: string;
73
+ }
74
+ type GroupsAction = {
75
+ action: 'snapshot';
76
+ } | {
77
+ action: 'create-group';
78
+ name: string;
79
+ description?: string;
80
+ } | {
81
+ action: 'rename-group';
82
+ groupId: string;
83
+ name?: string;
84
+ description?: string;
85
+ } | {
86
+ action: 'delete-group';
87
+ groupId: string;
88
+ } | {
89
+ action: 'add-member';
90
+ groupId: string;
91
+ abzSub: string;
92
+ role?: 'member' | 'admin';
93
+ } | {
94
+ action: 'change-role';
95
+ groupId: string;
96
+ abzSub: string;
97
+ role: 'member' | 'admin';
98
+ } | {
99
+ action: 'remove-member';
100
+ groupId: string;
101
+ abzSub: string;
102
+ } | {
103
+ action: 'add-subgroup';
104
+ groupId: string;
105
+ childGroupId: string;
106
+ } | {
107
+ action: 'remove-subgroup';
108
+ groupId: string;
109
+ childGroupId: string;
110
+ } | {
111
+ action: 'group-detail';
112
+ groupId: string;
113
+ } | {
114
+ action: 'provision-observed';
115
+ consumerUserId: string;
116
+ email?: string;
117
+ displayName?: string;
118
+ groups?: string[];
119
+ };
120
+ declare function createApiblazeGroups(cfg: ApiblazeGroupsConfig): {
121
+ handler: (req: Request) => Promise<Response>;
122
+ run: (user: AppUser, op: GroupsAction) => Promise<{
123
+ status: number;
124
+ data: unknown;
125
+ }>;
126
+ };
127
+
128
+ /**
129
+ * apiblaze/server — the server-side half of the apiblaze widgets.
3
130
  *
4
131
  * Holds the producer's CP key (server-only, never shipped to the browser) and turns
5
- * a logged-in user into API-key operations against apiblaze.com. The browser widget
6
- * (`apiblaze/react`) talks ONLY to your own backend; your backend talks to apiblaze —
7
- * so there are no cross-domain cookies, no secrets in the client, and no auth wiring
8
- * beyond "read my session."
132
+ * a logged-in user into apiblaze operations. The browser widgets (`apiblaze/react`)
133
+ * talk ONLY to your own backend; your backend talks to apiblaze — so there are no
134
+ * cross-domain cookies, no secrets in the client, and no auth wiring beyond
135
+ * "read my session."
136
+ *
137
+ * TWO faces over ONE shared substrate (core.ts — one CP credential, one
138
+ * ensure-tenant/ensure-user path):
139
+ * - createApiblazeKeys → API-key management (<ApiKeyWidget/>)
140
+ * - createApiblazeGroups → users & groups IAM (<UsersGroupsWidget/>)
9
141
  *
10
142
  * Usage (Next.js App Router — app/api/apiblaze/keys/route.ts):
11
143
  *
@@ -17,34 +149,41 @@
17
149
  * getUser: async () => {
18
150
  * const s = await auth();
19
151
  * if (!s?.user) return null;
20
- * return { tenant: s.user.orgId ?? s.user.id, userId: s.user.id, email: s.user.email ?? undefined, label: s.user.name ?? undefined };
152
+ * return {
153
+ * tenant: s.user.orgId ?? s.user.id,
154
+ * userId: s.user.id,
155
+ * email: s.user.email ?? undefined,
156
+ * // Which key types this person may create. Omit → ['call-only'].
157
+ * // false → no API access. A list of 2+ → the widget shows a picker.
158
+ * keyTypes: s.user.isEngineer ? ['manager', 'call-only'] : undefined,
159
+ * };
21
160
  * },
22
- * resolveScopes: (u) => u.isEngineer ? ['call', 'use-premium'] : ['call'], // optional
23
161
  * });
24
162
  * export const GET = keys.handler;
25
163
  * export const POST = keys.handler;
164
+ *
165
+ * SECURITY: eligibility is decided HERE, on your server, from your session —
166
+ * never from the browser. When a user is eligible for more than one type the
167
+ * browser sends its pick, but this handler only honors it if it's in the user's
168
+ * allowed set; otherwise it's rejected. `keyTypes: false` is enforced server-side
169
+ * (403) before anything is provisioned, not merely hidden in the UI.
26
170
  */
27
- interface AppUser {
28
- /** The producer's stable id for the consumer COMPANY — becomes the tenant (isolation). */
29
- tenant: string;
30
- /** The producer's stable id for the PERSON — the key creator/owner (list scope). */
31
- userId: string;
32
- /** Optional, stored now for possible future portal parity. */
33
- email?: string;
34
- /** Optional display name (cosmetic). */
35
- label?: string;
36
- /** Any extra fields your resolveScopes wants (e.g. isEngineer). */
37
- [k: string]: unknown;
38
- }
171
+
39
172
  interface ApiblazeKeysConfig {
40
- /** The producer CP key from the apiblaze dashboard Developers section. Server-only. */
173
+ /** The producer CP key from the apiblaze dashboard Developers section. Server-only.
174
+ * Use a purpose-built "widget" key (not a full admin key) so the platform's own
175
+ * subset rule is a real backstop under your eligibility logic. */
41
176
  cpKey: string;
42
177
  /** Return the logged-in user from the request/session, or null if unauthenticated. */
43
178
  getUser: (req: Request) => Promise<AppUser | null> | AppUser | null;
44
- /** Permission set to mint for a user. Default ['call']. Server-side only, so a browser
45
- * can never escalate return more for your own engineers here. */
46
- resolveScopes?: (user: AppUser) => string[] | Promise<string[]>;
47
- /** Default expiry for minted keys (seconds); enables re-reveal. Default 1 year. */
179
+ /** Optional override for eligibility, if you'd rather compute it here than inline
180
+ * in getUser. Same contract as AppUser.keyTypes (undefined default, false/[] → deny).
181
+ * Takes precedence over user.keyTypes when provided. */
182
+ resolveKeyTypes?: (user: AppUser) => (string[] | false) | Promise<string[] | false>;
183
+ /** Key lifetime in seconds. Omit for a DURABLE key (no expiry) — the right
184
+ * default for a production API key a developer embeds, so it never silently
185
+ * stops working. A durable key is shown ONCE at creation (not re-revealable);
186
+ * set an expiry if you want keys that stay revealable until they expire. */
48
187
  keyExpiresInSeconds?: number;
49
188
  /** Override the apiblaze base (tests/self-host). Default https://apikeys.apiblaze.com */
50
189
  base?: string;
@@ -53,6 +192,7 @@ type Action = {
53
192
  action: 'list';
54
193
  } | {
55
194
  action: 'create';
195
+ keyType?: string;
56
196
  } | {
57
197
  action: 'reveal';
58
198
  keyId: string;
@@ -71,4 +211,4 @@ declare function createApiblazeKeys(cfg: ApiblazeKeysConfig): {
71
211
  }>;
72
212
  };
73
213
 
74
- export { type ApiblazeKeysConfig, type AppUser, createApiblazeKeys };
214
+ export { type ApiblazeGroupsConfig, type ApiblazeKeysConfig, type AppUser, type GroupsAction, PlaneError, createApiblazeGroups, createApiblazeKeys };