@c1m-public/platform-auth 0.9.2 → 0.11.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 +18 -0
- package/dist/access/index.d.ts +1 -1
- package/dist/access/index.js +25 -10
- package/dist/access/next.d.ts +2 -2
- package/dist/access/next.js +25 -10
- package/dist/access/react.d.ts +5 -5
- package/dist/access/react.js +6 -6
- package/dist/{index-CbsCWtku.d.ts → index-HL6QFIZy.d.ts} +11 -11
- package/dist/{index-BnchfjP_.d.ts → index-aKe58m6n.d.ts} +4 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.js +39 -13
- package/dist/mcp-tokens.d.ts +79 -0
- package/dist/mcp-tokens.js +357 -0
- package/dist/next.d.ts +2 -3
- package/dist/next.js +50 -20
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ need — the boundary is the subpath.
|
|
|
11
11
|
```
|
|
12
12
|
. session: token reads, handoff, proxy gate, refresh (Web Request/Response)
|
|
13
13
|
./next session, server-only guard (Next route handlers)
|
|
14
|
+
./mcp-tokens self-service MCP PAT lifecycle (framework-neutral)
|
|
14
15
|
./access authorization: snapshot, keys, predicates (framework-neutral)
|
|
15
16
|
./access/next access loader: require / forbidden / invalidate (Next, server-only)
|
|
16
17
|
./access/react <Can>, useCan, AccessProvider (client)
|
|
@@ -19,6 +20,23 @@ need — the boundary is the subpath.
|
|
|
19
20
|
`next` and `react` are **optional** peer dependencies: a BFF that only reads
|
|
20
21
|
sessions never needs them.
|
|
21
22
|
|
|
23
|
+
## `./mcp-tokens` — MCP personal access tokens
|
|
24
|
+
|
|
25
|
+
`createMCPTokenClient` lists, mints, and revokes the signed-in platform user's
|
|
26
|
+
own MCP tokens under `/api/v1/platform/auth/me/mcp-tokens`. The client resolves
|
|
27
|
+
the bearer on every request, never retries mint or revoke, and emits bounded
|
|
28
|
+
observations containing operation metadata but no URL, bearer, or PAT material.
|
|
29
|
+
|
|
30
|
+
The subpath also exports `MCP_ACCESS_PERMISSION`,
|
|
31
|
+
`MCP_TOKENS_MANAGE_PERMISSION`, `MCP_TOOL_PERMISSIONS`,
|
|
32
|
+
`MCP_WILDCARD_TOOL_PERMISSIONS`, the request/response types, and the structural
|
|
33
|
+
`MCPTokenAPIError` predicate. The permission ceiling `["*"]` follows future
|
|
34
|
+
read-only tool permissions; mutating permissions must always be named
|
|
35
|
+
explicitly. A minted `raw_token` is a one-time value: callers must keep it out
|
|
36
|
+
of URLs, logs, storage, and telemetry. A `recent_authentication_required` API
|
|
37
|
+
error is a step-up challenge, not an expired session; send the user through the
|
|
38
|
+
host application's re-authentication path before retrying the mint.
|
|
39
|
+
|
|
22
40
|
## `.` — session
|
|
23
41
|
|
|
24
42
|
| Export | Purpose |
|
package/dist/access/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, A as AccessSnapshot, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, P as PermissionCheckOptions, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-
|
|
1
|
+
export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, A as AccessSnapshot, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, P as PermissionCheckOptions, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-HL6QFIZy.js';
|
package/dist/access/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/access/generated/keys.ts
|
|
2
2
|
var PLATFORM_PERMISSIONS = [
|
|
3
|
+
"affiliates.enrollments.manage",
|
|
3
4
|
"affiliates.finance.manage",
|
|
4
5
|
"affiliates.partners.manage",
|
|
5
6
|
"affiliates.programmes.manage",
|
|
@@ -12,20 +13,25 @@ var PLATFORM_PERMISSIONS = [
|
|
|
12
13
|
"platform.audit.view",
|
|
13
14
|
"platform.auth.manage",
|
|
14
15
|
"platform.billing.manage",
|
|
16
|
+
"platform.billing.view",
|
|
15
17
|
"platform.channels.manage",
|
|
16
18
|
"platform.channels.view",
|
|
17
19
|
"platform.config.manage",
|
|
18
20
|
"platform.docs.manage",
|
|
19
21
|
"platform.docs.view",
|
|
22
|
+
"platform.mcp.access",
|
|
23
|
+
"platform.mcp.tokens.manage",
|
|
20
24
|
"platform.observability.manage",
|
|
21
25
|
"platform.observability.view",
|
|
22
26
|
"platform.offboarding.view",
|
|
23
27
|
"platform.ops.manage",
|
|
28
|
+
"platform.ops.view",
|
|
24
29
|
"platform.support.impersonate",
|
|
25
30
|
"platform.support.sessions",
|
|
26
31
|
"platform.tenants.archive",
|
|
27
32
|
"platform.tenants.manage",
|
|
28
33
|
"platform.tenants.restore",
|
|
34
|
+
"platform.tenants.view",
|
|
29
35
|
"platform.users.deactivate",
|
|
30
36
|
"platform.users.delete",
|
|
31
37
|
"platform.users.invite",
|
|
@@ -36,16 +42,20 @@ var PLATFORM_PERMISSIONS = [
|
|
|
36
42
|
var DASH_PERMISSIONS = [
|
|
37
43
|
"dash.apps.manage",
|
|
38
44
|
"dash.billing.manage",
|
|
45
|
+
"dash.billing.view",
|
|
39
46
|
"dash.channels.manage",
|
|
40
47
|
"dash.customers.manage",
|
|
41
48
|
"dash.domains.manage",
|
|
49
|
+
"dash.domains.view",
|
|
42
50
|
"dash.members.manage",
|
|
43
51
|
"dash.ownership.transfer",
|
|
44
52
|
"dash.plans.manage",
|
|
53
|
+
"dash.plans.view",
|
|
45
54
|
"dash.publish",
|
|
46
55
|
"dash.settings.manage",
|
|
47
56
|
"dash.workspaces.admin",
|
|
48
|
-
"dash.workspaces.manage"
|
|
57
|
+
"dash.workspaces.manage",
|
|
58
|
+
"dash.workspaces.view"
|
|
49
59
|
];
|
|
50
60
|
var DASH_ROLES = [
|
|
51
61
|
"dash.admin",
|
|
@@ -176,20 +186,20 @@ function parseAccessSnapshot(input) {
|
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
// src/access/predicates.ts
|
|
179
|
-
function entryCoversApp(entry,
|
|
189
|
+
function entryCoversApp(entry, dashId) {
|
|
180
190
|
switch (entry.scope_type) {
|
|
181
191
|
case "platform":
|
|
182
192
|
return true;
|
|
183
193
|
case "saas_app":
|
|
184
|
-
if (
|
|
185
|
-
return entry.scope_id === "*" || entry.scope_id.toLowerCase() ===
|
|
194
|
+
if (dashId === void 0) return true;
|
|
195
|
+
return entry.scope_id === "*" || entry.scope_id.toLowerCase() === dashId.toLowerCase();
|
|
186
196
|
default:
|
|
187
197
|
return false;
|
|
188
198
|
}
|
|
189
199
|
}
|
|
190
200
|
function hasPermission(snapshot, key, opts = {}) {
|
|
191
201
|
if (!snapshot.active) return false;
|
|
192
|
-
return snapshot.permissions.some((entry) => entry.key === key && entryCoversApp(entry, opts.
|
|
202
|
+
return snapshot.permissions.some((entry) => entry.key === key && entryCoversApp(entry, opts.dashId));
|
|
193
203
|
}
|
|
194
204
|
function hasAnyPermission(snapshot, keys, opts = {}) {
|
|
195
205
|
return keys.some((key) => hasPermission(snapshot, key, opts));
|
|
@@ -204,11 +214,11 @@ function hasWorkspacePermission(snapshot, workspaceId, key) {
|
|
|
204
214
|
}
|
|
205
215
|
function managedDashes(snapshot) {
|
|
206
216
|
if (!snapshot.active) return [];
|
|
207
|
-
return Object.entries(snapshot.dash_roles).map(([
|
|
217
|
+
return Object.entries(snapshot.dash_roles).map(([dashId, role]) => ({ dashId, role })).sort((a, b) => a.dashId.localeCompare(b.dashId));
|
|
208
218
|
}
|
|
209
|
-
function dashRole(snapshot,
|
|
219
|
+
function dashRole(snapshot, dashId) {
|
|
210
220
|
if (!snapshot.active) return "";
|
|
211
|
-
return snapshot.dash_roles[
|
|
221
|
+
return snapshot.dash_roles[dashId] ?? "";
|
|
212
222
|
}
|
|
213
223
|
function isPlatformOwner(snapshot) {
|
|
214
224
|
return snapshot.active && snapshot.is_owner;
|
|
@@ -229,7 +239,7 @@ function appScopes(snapshot, key) {
|
|
|
229
239
|
function permissionsFor(snapshot, opts = {}) {
|
|
230
240
|
if (!snapshot.active) return [];
|
|
231
241
|
const keys = /* @__PURE__ */ new Set();
|
|
232
|
-
for (const entry of snapshot.permissions) if (entryCoversApp(entry, opts.
|
|
242
|
+
for (const entry of snapshot.permissions) if (entryCoversApp(entry, opts.dashId)) keys.add(entry.key);
|
|
233
243
|
return [...keys].sort();
|
|
234
244
|
}
|
|
235
245
|
|
|
@@ -256,6 +266,11 @@ function forwardedRequestHeaders(request, init) {
|
|
|
256
266
|
return applyForwardedClientHeaders(request.headers, new Headers(init));
|
|
257
267
|
}
|
|
258
268
|
|
|
269
|
+
// src/forwarding.ts
|
|
270
|
+
function forwardedRequestHeaders2(request, init) {
|
|
271
|
+
return forwardedRequestHeaders(request, init);
|
|
272
|
+
}
|
|
273
|
+
|
|
259
274
|
// src/internal.ts
|
|
260
275
|
function resolveValue(value) {
|
|
261
276
|
return typeof value === "function" ? value() : value;
|
|
@@ -279,7 +294,7 @@ async function timedFetch(url, options = {}) {
|
|
|
279
294
|
var DEFAULT_ACCESS_ENDPOINT_PATH = "/api/v1/platform/access";
|
|
280
295
|
async function fetchAccessSnapshot(opts) {
|
|
281
296
|
const url = `${resolveValue(opts.apiUrl).replace(/\/$/, "")}${opts.endpointPath ?? DEFAULT_ACCESS_ENDPOINT_PATH}`;
|
|
282
|
-
const headers = opts.request ?
|
|
297
|
+
const headers = opts.request ? forwardedRequestHeaders2(opts.request, opts.headers) : new Headers(opts.headers);
|
|
283
298
|
headers.set("Authorization", `Bearer ${opts.accessToken}`);
|
|
284
299
|
headers.set("Accept", "application/json");
|
|
285
300
|
let response;
|
package/dist/access/next.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as AccessSnapshot, P as PermissionCheckOptions, R as Resolvable } from '../index-
|
|
2
|
-
export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-
|
|
1
|
+
import { A as AccessSnapshot, P as PermissionCheckOptions, R as Resolvable } from '../index-HL6QFIZy.js';
|
|
2
|
+
export { a as AccessEntry, b as AccessErrorCode, c as AccessScopeType, d as AccessSnapshotError, D as DASH_PERMISSIONS, e as DASH_ROLES, f as DEFAULT_ACCESS_ENDPOINT_PATH, g as DashPermissionKey, h as DashRole, F as FetchAccessSnapshotOptions, L as LEADBROKER_PERMISSIONS, i as LeadBrokerPermissionKey, j as PIPELINE_STAGES, k as PLATFORM_PERMISSIONS, l as PermissionKey, m as PipelineStage, n as PlatformPermissionKey, W as WORKSPACE_PERMISSIONS, o as WORKSPACE_ROLES, p as WorkspacePermissionKey, q as WorkspaceRole, r as appScopes, s as dashRole, t as emptySnapshot, u as entryCoversApp, v as fetchAccessSnapshot, w as hasAllPermissions, x as hasAnyPermission, y as hasPermission, z as hasWorkspacePermission, B as isAccessSnapshotError, C as isPipelineStage, E as isPlatformOwner, G as managedDashes, H as parseAccessSnapshot, I as permissionsFor } from '../index-HL6QFIZy.js';
|
|
3
3
|
|
|
4
4
|
/** Default cache tag for one subject's snapshot: `access:<subjectId>`. */
|
|
5
5
|
declare function accessCacheTag(subjectId: string): string;
|
package/dist/access/next.js
CHANGED
|
@@ -87,6 +87,11 @@ function forwardedRequestHeaders(request, init) {
|
|
|
87
87
|
return applyForwardedClientHeaders(request.headers, new Headers(init));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// src/forwarding.ts
|
|
91
|
+
function forwardedRequestHeaders2(request, init) {
|
|
92
|
+
return forwardedRequestHeaders(request, init);
|
|
93
|
+
}
|
|
94
|
+
|
|
90
95
|
// src/access/snapshot.ts
|
|
91
96
|
var SCOPE_TYPES = /* @__PURE__ */ new Set(["platform", "saas_app", "workspace"]);
|
|
92
97
|
function emptySnapshot(subjectId = "") {
|
|
@@ -166,7 +171,7 @@ function parseAccessSnapshot(input) {
|
|
|
166
171
|
var DEFAULT_ACCESS_ENDPOINT_PATH = "/api/v1/platform/access";
|
|
167
172
|
async function fetchAccessSnapshot(opts) {
|
|
168
173
|
const url = `${resolveValue(opts.apiUrl).replace(/\/$/, "")}${opts.endpointPath ?? DEFAULT_ACCESS_ENDPOINT_PATH}`;
|
|
169
|
-
const headers = opts.request ?
|
|
174
|
+
const headers = opts.request ? forwardedRequestHeaders2(opts.request, opts.headers) : new Headers(opts.headers);
|
|
170
175
|
headers.set("Authorization", `Bearer ${opts.accessToken}`);
|
|
171
176
|
headers.set("Accept", "application/json");
|
|
172
177
|
let response;
|
|
@@ -192,20 +197,20 @@ async function fetchAccessSnapshot(opts) {
|
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
// src/access/predicates.ts
|
|
195
|
-
function entryCoversApp(entry,
|
|
200
|
+
function entryCoversApp(entry, dashId) {
|
|
196
201
|
switch (entry.scope_type) {
|
|
197
202
|
case "platform":
|
|
198
203
|
return true;
|
|
199
204
|
case "saas_app":
|
|
200
|
-
if (
|
|
201
|
-
return entry.scope_id === "*" || entry.scope_id.toLowerCase() ===
|
|
205
|
+
if (dashId === void 0) return true;
|
|
206
|
+
return entry.scope_id === "*" || entry.scope_id.toLowerCase() === dashId.toLowerCase();
|
|
202
207
|
default:
|
|
203
208
|
return false;
|
|
204
209
|
}
|
|
205
210
|
}
|
|
206
211
|
function hasPermission(snapshot, key, opts = {}) {
|
|
207
212
|
if (!snapshot.active) return false;
|
|
208
|
-
return snapshot.permissions.some((entry) => entry.key === key && entryCoversApp(entry, opts.
|
|
213
|
+
return snapshot.permissions.some((entry) => entry.key === key && entryCoversApp(entry, opts.dashId));
|
|
209
214
|
}
|
|
210
215
|
function hasAnyPermission(snapshot, keys, opts = {}) {
|
|
211
216
|
return keys.some((key) => hasPermission(snapshot, key, opts));
|
|
@@ -220,11 +225,11 @@ function hasWorkspacePermission(snapshot, workspaceId, key) {
|
|
|
220
225
|
}
|
|
221
226
|
function managedDashes(snapshot) {
|
|
222
227
|
if (!snapshot.active) return [];
|
|
223
|
-
return Object.entries(snapshot.dash_roles).map(([
|
|
228
|
+
return Object.entries(snapshot.dash_roles).map(([dashId, role]) => ({ dashId, role })).sort((a, b) => a.dashId.localeCompare(b.dashId));
|
|
224
229
|
}
|
|
225
|
-
function dashRole(snapshot,
|
|
230
|
+
function dashRole(snapshot, dashId) {
|
|
226
231
|
if (!snapshot.active) return "";
|
|
227
|
-
return snapshot.dash_roles[
|
|
232
|
+
return snapshot.dash_roles[dashId] ?? "";
|
|
228
233
|
}
|
|
229
234
|
function isPlatformOwner(snapshot) {
|
|
230
235
|
return snapshot.active && snapshot.is_owner;
|
|
@@ -245,12 +250,13 @@ function appScopes(snapshot, key) {
|
|
|
245
250
|
function permissionsFor(snapshot, opts = {}) {
|
|
246
251
|
if (!snapshot.active) return [];
|
|
247
252
|
const keys = /* @__PURE__ */ new Set();
|
|
248
|
-
for (const entry of snapshot.permissions) if (entryCoversApp(entry, opts.
|
|
253
|
+
for (const entry of snapshot.permissions) if (entryCoversApp(entry, opts.dashId)) keys.add(entry.key);
|
|
249
254
|
return [...keys].sort();
|
|
250
255
|
}
|
|
251
256
|
|
|
252
257
|
// src/access/generated/keys.ts
|
|
253
258
|
var PLATFORM_PERMISSIONS = [
|
|
259
|
+
"affiliates.enrollments.manage",
|
|
254
260
|
"affiliates.finance.manage",
|
|
255
261
|
"affiliates.partners.manage",
|
|
256
262
|
"affiliates.programmes.manage",
|
|
@@ -263,20 +269,25 @@ var PLATFORM_PERMISSIONS = [
|
|
|
263
269
|
"platform.audit.view",
|
|
264
270
|
"platform.auth.manage",
|
|
265
271
|
"platform.billing.manage",
|
|
272
|
+
"platform.billing.view",
|
|
266
273
|
"platform.channels.manage",
|
|
267
274
|
"platform.channels.view",
|
|
268
275
|
"platform.config.manage",
|
|
269
276
|
"platform.docs.manage",
|
|
270
277
|
"platform.docs.view",
|
|
278
|
+
"platform.mcp.access",
|
|
279
|
+
"platform.mcp.tokens.manage",
|
|
271
280
|
"platform.observability.manage",
|
|
272
281
|
"platform.observability.view",
|
|
273
282
|
"platform.offboarding.view",
|
|
274
283
|
"platform.ops.manage",
|
|
284
|
+
"platform.ops.view",
|
|
275
285
|
"platform.support.impersonate",
|
|
276
286
|
"platform.support.sessions",
|
|
277
287
|
"platform.tenants.archive",
|
|
278
288
|
"platform.tenants.manage",
|
|
279
289
|
"platform.tenants.restore",
|
|
290
|
+
"platform.tenants.view",
|
|
280
291
|
"platform.users.deactivate",
|
|
281
292
|
"platform.users.delete",
|
|
282
293
|
"platform.users.invite",
|
|
@@ -287,16 +298,20 @@ var PLATFORM_PERMISSIONS = [
|
|
|
287
298
|
var DASH_PERMISSIONS = [
|
|
288
299
|
"dash.apps.manage",
|
|
289
300
|
"dash.billing.manage",
|
|
301
|
+
"dash.billing.view",
|
|
290
302
|
"dash.channels.manage",
|
|
291
303
|
"dash.customers.manage",
|
|
292
304
|
"dash.domains.manage",
|
|
305
|
+
"dash.domains.view",
|
|
293
306
|
"dash.members.manage",
|
|
294
307
|
"dash.ownership.transfer",
|
|
295
308
|
"dash.plans.manage",
|
|
309
|
+
"dash.plans.view",
|
|
296
310
|
"dash.publish",
|
|
297
311
|
"dash.settings.manage",
|
|
298
312
|
"dash.workspaces.admin",
|
|
299
|
-
"dash.workspaces.manage"
|
|
313
|
+
"dash.workspaces.manage",
|
|
314
|
+
"dash.workspaces.view"
|
|
300
315
|
];
|
|
301
316
|
var DASH_ROLES = [
|
|
302
317
|
"dash.admin",
|
package/dist/access/react.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ interface AccessSnapshot {
|
|
|
28
28
|
* Dash bundle (`dash.owner` / `dash.admin`) per saas_app id, mirrored from
|
|
29
29
|
* the subject's dash membership. This is the admission list for the dash
|
|
30
30
|
* area: the dashes this subject runs. What they may *do* on one is a normal
|
|
31
|
-
* `hasPermission(snapshot, key, {
|
|
31
|
+
* `hasPermission(snapshot, key, { dashId })` check — platform managers hold
|
|
32
32
|
* their authority platform-wide and so appear here not at all.
|
|
33
33
|
*/
|
|
34
34
|
dash_roles: Record<string, string>;
|
|
@@ -40,10 +40,10 @@ interface AccessSnapshot {
|
|
|
40
40
|
|
|
41
41
|
interface PermissionCheckOptions {
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
* entries and any saas_app entry satisfy the check.
|
|
43
|
+
* Dash slug the check is about. Omit for "anywhere": platform
|
|
44
|
+
* entries and any saas_app-scoped entry satisfy the check.
|
|
45
45
|
*/
|
|
46
|
-
|
|
46
|
+
dashId?: string;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
interface AccessProviderProps {
|
|
@@ -70,6 +70,6 @@ interface CanProps extends PermissionCheckOptions {
|
|
|
70
70
|
children: ReactNode;
|
|
71
71
|
}
|
|
72
72
|
/** Render-gate. Presentation only — the backend remains the security boundary. */
|
|
73
|
-
declare function Can({ permission, mode, workspaceId,
|
|
73
|
+
declare function Can({ permission, mode, workspaceId, dashId, not, fallback, children }: CanProps): react.JSX.Element;
|
|
74
74
|
|
|
75
75
|
export { AccessProvider, type AccessProviderProps, Can, type CanProps, useAccess, useAccessOrEmpty, useCan, useCanWorkspace, useIsPlatformOwner };
|
package/dist/access/react.js
CHANGED
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
import { createContext, useContext, useMemo } from "react";
|
|
5
5
|
|
|
6
6
|
// src/access/predicates.ts
|
|
7
|
-
function entryCoversApp(entry,
|
|
7
|
+
function entryCoversApp(entry, dashId) {
|
|
8
8
|
switch (entry.scope_type) {
|
|
9
9
|
case "platform":
|
|
10
10
|
return true;
|
|
11
11
|
case "saas_app":
|
|
12
|
-
if (
|
|
13
|
-
return entry.scope_id === "*" || entry.scope_id.toLowerCase() ===
|
|
12
|
+
if (dashId === void 0) return true;
|
|
13
|
+
return entry.scope_id === "*" || entry.scope_id.toLowerCase() === dashId.toLowerCase();
|
|
14
14
|
default:
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
function hasPermission(snapshot, key, opts = {}) {
|
|
19
19
|
if (!snapshot.active) return false;
|
|
20
|
-
return snapshot.permissions.some((entry) => entry.key === key && entryCoversApp(entry, opts.
|
|
20
|
+
return snapshot.permissions.some((entry) => entry.key === key && entryCoversApp(entry, opts.dashId));
|
|
21
21
|
}
|
|
22
22
|
function hasWorkspacePermission(snapshot, workspaceId, key) {
|
|
23
23
|
if (!snapshot.active) return false;
|
|
@@ -69,10 +69,10 @@ function useCanWorkspace(workspaceId) {
|
|
|
69
69
|
function useIsPlatformOwner() {
|
|
70
70
|
return isPlatformOwner(useAccess());
|
|
71
71
|
}
|
|
72
|
-
function Can({ permission, mode = "all", workspaceId,
|
|
72
|
+
function Can({ permission, mode = "all", workspaceId, dashId, not = false, fallback = null, children }) {
|
|
73
73
|
const snapshot = useAccess();
|
|
74
74
|
const keys = typeof permission === "string" ? [permission] : permission;
|
|
75
|
-
const check = (key) => workspaceId !== void 0 ? hasWorkspacePermission(snapshot, workspaceId, key) : hasPermission(snapshot, key, {
|
|
75
|
+
const check = (key) => workspaceId !== void 0 ? hasWorkspacePermission(snapshot, workspaceId, key) : hasPermission(snapshot, key, { dashId });
|
|
76
76
|
const allowed = mode === "any" ? keys.some(check) : keys.every(check);
|
|
77
77
|
return /* @__PURE__ */ jsx(Fragment, { children: allowed !== not ? children : fallback });
|
|
78
78
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Platform-operator permission keys. */
|
|
2
|
-
declare const PLATFORM_PERMISSIONS: readonly ["affiliates.finance.manage", "affiliates.partners.manage", "affiliates.programmes.manage", "affiliates.view", "linkservice.domains.manage", "linkservice.links.manage", "linkservice.qr.manage", "linkservice.view", "platform.admin.view", "platform.audit.view", "platform.auth.manage", "platform.billing.manage", "platform.channels.manage", "platform.channels.view", "platform.config.manage", "platform.docs.manage", "platform.docs.view", "platform.observability.manage", "platform.observability.view", "platform.offboarding.view", "platform.ops.manage", "platform.support.impersonate", "platform.support.sessions", "platform.tenants.archive", "platform.tenants.manage", "platform.tenants.restore", "platform.users.deactivate", "platform.users.delete", "platform.users.invite", "platform.users.manage_access", "platform.users.reactivate", "platform.users.view"];
|
|
2
|
+
declare const PLATFORM_PERMISSIONS: readonly ["affiliates.enrollments.manage", "affiliates.finance.manage", "affiliates.partners.manage", "affiliates.programmes.manage", "affiliates.view", "linkservice.domains.manage", "linkservice.links.manage", "linkservice.qr.manage", "linkservice.view", "platform.admin.view", "platform.audit.view", "platform.auth.manage", "platform.billing.manage", "platform.billing.view", "platform.channels.manage", "platform.channels.view", "platform.config.manage", "platform.docs.manage", "platform.docs.view", "platform.mcp.access", "platform.mcp.tokens.manage", "platform.observability.manage", "platform.observability.view", "platform.offboarding.view", "platform.ops.manage", "platform.ops.view", "platform.support.impersonate", "platform.support.sessions", "platform.tenants.archive", "platform.tenants.manage", "platform.tenants.restore", "platform.tenants.view", "platform.users.deactivate", "platform.users.delete", "platform.users.invite", "platform.users.manage_access", "platform.users.reactivate", "platform.users.view"];
|
|
3
3
|
type PlatformPermissionKey = (typeof PLATFORM_PERMISSIONS)[number];
|
|
4
4
|
/** Dash-owner permission keys (granted at dash scope, mirrored from the membership role). */
|
|
5
|
-
declare const DASH_PERMISSIONS: readonly ["dash.apps.manage", "dash.billing.manage", "dash.channels.manage", "dash.customers.manage", "dash.domains.manage", "dash.members.manage", "dash.ownership.transfer", "dash.plans.manage", "dash.publish", "dash.settings.manage", "dash.workspaces.admin", "dash.workspaces.manage"];
|
|
5
|
+
declare const DASH_PERMISSIONS: readonly ["dash.apps.manage", "dash.billing.manage", "dash.billing.view", "dash.channels.manage", "dash.customers.manage", "dash.domains.manage", "dash.domains.view", "dash.members.manage", "dash.ownership.transfer", "dash.plans.manage", "dash.plans.view", "dash.publish", "dash.settings.manage", "dash.workspaces.admin", "dash.workspaces.manage", "dash.workspaces.view"];
|
|
6
6
|
type DashPermissionKey = (typeof DASH_PERMISSIONS)[number];
|
|
7
7
|
/** Membership-mirrored dash role bundles (grant keys at dash scope). */
|
|
8
8
|
declare const DASH_ROLES: readonly ["dash.admin", "dash.owner"];
|
|
@@ -65,7 +65,7 @@ interface AccessSnapshot {
|
|
|
65
65
|
* Dash bundle (`dash.owner` / `dash.admin`) per saas_app id, mirrored from
|
|
66
66
|
* the subject's dash membership. This is the admission list for the dash
|
|
67
67
|
* area: the dashes this subject runs. What they may *do* on one is a normal
|
|
68
|
-
* `hasPermission(snapshot, key, {
|
|
68
|
+
* `hasPermission(snapshot, key, { dashId })` check — platform managers hold
|
|
69
69
|
* their authority platform-wide and so appear here not at all.
|
|
70
70
|
*/
|
|
71
71
|
dash_roles: Record<string, string>;
|
|
@@ -80,17 +80,17 @@ declare function parseAccessSnapshot(input: unknown): AccessSnapshot;
|
|
|
80
80
|
|
|
81
81
|
interface PermissionCheckOptions {
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
84
|
-
* entries and any saas_app entry satisfy the check.
|
|
83
|
+
* Dash slug the check is about. Omit for "anywhere": platform
|
|
84
|
+
* entries and any saas_app-scoped entry satisfy the check.
|
|
85
85
|
*/
|
|
86
|
-
|
|
86
|
+
dashId?: string;
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
89
|
* Mirrors c1m-server authz.entryCoversApp: platform scope covers every app;
|
|
90
90
|
* saas_app scope covers "*" or a case-insensitive slug match; workspace-scoped
|
|
91
91
|
* entries never satisfy an app-level check.
|
|
92
92
|
*/
|
|
93
|
-
declare function entryCoversApp(entry: AccessEntry,
|
|
93
|
+
declare function entryCoversApp(entry: AccessEntry, dashId: string | undefined): boolean;
|
|
94
94
|
/** True when the subject is active and holds `key` for the given app (or anywhere). */
|
|
95
95
|
declare function hasPermission(snapshot: AccessSnapshot, key: string, opts?: PermissionCheckOptions): boolean;
|
|
96
96
|
declare function hasAnyPermission(snapshot: AccessSnapshot, keys: readonly string[], opts?: PermissionCheckOptions): boolean;
|
|
@@ -108,14 +108,14 @@ declare function hasWorkspacePermission(snapshot: AccessSnapshot, workspaceId: s
|
|
|
108
108
|
* `hasPermission(snapshot, "platform.tenants.manage")` instead.
|
|
109
109
|
*/
|
|
110
110
|
declare function managedDashes(snapshot: AccessSnapshot): Array<{
|
|
111
|
-
|
|
111
|
+
dashId: string;
|
|
112
112
|
role: string;
|
|
113
113
|
}>;
|
|
114
114
|
/** The dash bundle held on one dash, or "" when the subject holds none there. */
|
|
115
|
-
declare function dashRole(snapshot: AccessSnapshot,
|
|
115
|
+
declare function dashRole(snapshot: AccessSnapshot, dashId: string): string;
|
|
116
116
|
declare function isPlatformOwner(snapshot: AccessSnapshot): boolean;
|
|
117
117
|
/**
|
|
118
|
-
* The
|
|
118
|
+
* The dash scopes under which `key` is held. Returns `{ all: true }` when a
|
|
119
119
|
* platform-scoped or "*" entry grants it everywhere; otherwise the explicit
|
|
120
120
|
* app slugs (empty = nowhere). Mirrors authz.AppPermissionScope.
|
|
121
121
|
*/
|
|
@@ -123,7 +123,7 @@ declare function appScopes(snapshot: AccessSnapshot, key: string): {
|
|
|
123
123
|
all: boolean;
|
|
124
124
|
apps: string[];
|
|
125
125
|
};
|
|
126
|
-
/** Distinct permission keys held for `
|
|
126
|
+
/** Distinct permission keys held for `dashId` (or anywhere). Handy for nav/badges. */
|
|
127
127
|
declare function permissionsFor(snapshot: AccessSnapshot, opts?: PermissionCheckOptions): string[];
|
|
128
128
|
|
|
129
129
|
/** A value that may be fixed or resolved per call (env read at request time). */
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import '@c1m-public/transport';
|
|
2
|
-
|
|
3
1
|
interface SessionUser {
|
|
4
2
|
id: string;
|
|
5
3
|
email: string;
|
|
@@ -59,6 +57,9 @@ declare function parseAuthResponse(value: unknown): AuthResponse | null;
|
|
|
59
57
|
*/
|
|
60
58
|
declare function safeInternalPath(raw: string | null | undefined): string | null;
|
|
61
59
|
|
|
60
|
+
declare function applyForwardedClientHeaders(incoming: Headers, headers?: Headers): Headers;
|
|
61
|
+
declare function forwardedRequestHeaders(request: Request, init?: HeadersInit): Headers;
|
|
62
|
+
|
|
62
63
|
interface CookieOptions {
|
|
63
64
|
httpOnly?: boolean;
|
|
64
65
|
secure?: boolean;
|
|
@@ -232,4 +233,4 @@ interface SessionGateInput {
|
|
|
232
233
|
}
|
|
233
234
|
declare function evaluateSessionGate(input: SessionGateInput): Promise<SessionGateDecision>;
|
|
234
235
|
|
|
235
|
-
export { type AsyncResolvable as A,
|
|
236
|
+
export { type AsyncResolvable as A, decodeTokenPayload as B, type CookieOptions as C, type DeviceIdentity as D, deleteCookie as E, evaluateSessionGate as F, forwardedRequestHeaders as G, HANDOFF_ERROR_FALLBACK as H, handoffErrorMessage as I, hasSessionShape as J, hasTokenAudience as K, isAuthErrorCode as L, isHandoffErrorCode as M, isTokenExpired as N, isTokenExpiringSoon as O, parseAuthResponse as P, parseRefreshEnvelope as Q, type Resolvable as R, type SessionData as S, type TokenPayload as T, readCookie as U, safeInternalPath as V, serializeCookie as W, setCookie as X, sourceStartPath as Y, tokenMaxAgeSeconds as Z, AUTH_ERROR_MESSAGES as a, type AuthResponse as b, type DeviceIdentityConfig as c, type DeviceIdentityStore as d, HANDOFF_ERROR_MESSAGES as e, type HandoffSource as f, type HandoffSourceConfig as g, type HandoffSourceTarget as h, type HandoffTarget as i, type HandoffTargetConfig as j, type RefreshedSession as k, SSO_ERROR_MESSAGES as l, type SessionGateDecision as m, type SessionGateInput as n, type SessionRefresher as o, type SessionRefresherConfig as p, type SessionUser as q, applyForwardedClientHeaders as r, authErrorMessage as s, brokerPaths as t, createDeviceIdentity as u, createHandoffSource as v, createHandoffTarget as w, createSessionRefresher as x, decodeBase64Url as y, decodeDefaultSession as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, C as CookieOptions, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as RefreshedSession, l as SSO_ERROR_MESSAGES, S as SessionData, m as SessionGateDecision, n as SessionGateInput, o as SessionRefresher, p as SessionRefresherConfig, q as SessionUser, T as TokenPayload, r as
|
|
2
|
-
export { applyForwardedClientHeaders, forwardedRequestHeaders } from '@c1m-public/transport';
|
|
1
|
+
export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, C as CookieOptions, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as RefreshedSession, l as SSO_ERROR_MESSAGES, S as SessionData, m as SessionGateDecision, n as SessionGateInput, o as SessionRefresher, p as SessionRefresherConfig, q as SessionUser, T as TokenPayload, r as applyForwardedClientHeaders, s as authErrorMessage, t as brokerPaths, u as createDeviceIdentity, v as createHandoffSource, w as createHandoffTarget, x as createSessionRefresher, y as decodeBase64Url, z as decodeDefaultSession, B as decodeTokenPayload, E as deleteCookie, F as evaluateSessionGate, G as forwardedRequestHeaders, I as handoffErrorMessage, J as hasSessionShape, K as hasTokenAudience, L as isAuthErrorCode, M as isHandoffErrorCode, N as isTokenExpired, O as isTokenExpiringSoon, P as parseAuthResponse, Q as parseRefreshEnvelope, U as readCookie, V as safeInternalPath, W as serializeCookie, X as setCookie, Y as sourceStartPath, Z as tokenMaxAgeSeconds } from './index-aKe58m6n.js';
|
package/dist/index.js
CHANGED
|
@@ -123,6 +123,14 @@ function forwardedRequestHeaders(request, init) {
|
|
|
123
123
|
return applyForwardedClientHeaders(request.headers, new Headers(init));
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// src/forwarding.ts
|
|
127
|
+
function applyForwardedClientHeaders2(incoming, headers = new Headers()) {
|
|
128
|
+
return applyForwardedClientHeaders(incoming, headers);
|
|
129
|
+
}
|
|
130
|
+
function forwardedRequestHeaders2(request, init) {
|
|
131
|
+
return forwardedRequestHeaders(request, init);
|
|
132
|
+
}
|
|
133
|
+
|
|
126
134
|
// src/cookies.ts
|
|
127
135
|
function readCookie(request, name) {
|
|
128
136
|
const header = request.headers.get("cookie");
|
|
@@ -151,12 +159,27 @@ function serializeCookie(name, value, options = {}) {
|
|
|
151
159
|
return parts.join("; ");
|
|
152
160
|
}
|
|
153
161
|
function setCookie(response, name, value, options) {
|
|
154
|
-
response
|
|
155
|
-
return response;
|
|
162
|
+
return appendSetCookie(response, serializeCookie(name, value, options));
|
|
156
163
|
}
|
|
157
164
|
function deleteCookie(response, name, options) {
|
|
158
|
-
response
|
|
159
|
-
|
|
165
|
+
return appendSetCookie(response, serializeCookie(name, "", { ...options, maxAge: 0 }));
|
|
166
|
+
}
|
|
167
|
+
function appendSetCookie(response, cookie) {
|
|
168
|
+
try {
|
|
169
|
+
response.headers.append("Set-Cookie", cookie);
|
|
170
|
+
return response;
|
|
171
|
+
} catch (error) {
|
|
172
|
+
if (!(error instanceof TypeError) || !/immutable/i.test(error.message)) {
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
const mutable = new Response(response.body, {
|
|
176
|
+
status: response.status,
|
|
177
|
+
statusText: response.statusText,
|
|
178
|
+
headers: response.headers
|
|
179
|
+
});
|
|
180
|
+
mutable.headers.append("Set-Cookie", cookie);
|
|
181
|
+
return mutable;
|
|
182
|
+
}
|
|
160
183
|
}
|
|
161
184
|
|
|
162
185
|
// src/device-cookie.ts
|
|
@@ -316,8 +339,11 @@ function createHandoffTarget(config) {
|
|
|
316
339
|
const cookieOptions = { httpOnly: true, secure, sameSite: "lax", path: "/", maxAge: HANDOFF_COOKIE_TTL_SECONDS };
|
|
317
340
|
const timeout = config.timeoutMs ?? config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
318
341
|
const clearHandoffCookies = (response) => {
|
|
319
|
-
|
|
320
|
-
|
|
342
|
+
let result = response;
|
|
343
|
+
for (const name of Object.values(cookies)) {
|
|
344
|
+
result = deleteCookie(result, name, { path: "/", secure });
|
|
345
|
+
}
|
|
346
|
+
return result;
|
|
321
347
|
};
|
|
322
348
|
const fail = (origin, code) => clearHandoffCookies(loginErrorRedirect(origin, config.loginPath, code));
|
|
323
349
|
return {
|
|
@@ -335,11 +361,11 @@ function createHandoffTarget(config) {
|
|
|
335
361
|
const start = new URL(sourceStartPath(config.app), sourceOrigin);
|
|
336
362
|
start.searchParams.set("code_challenge", await s256(verifier));
|
|
337
363
|
start.searchParams.set("state", state);
|
|
338
|
-
|
|
339
|
-
setCookie(response, cookies.verifier, verifier, cookieOptions);
|
|
340
|
-
setCookie(response, cookies.state, state, cookieOptions);
|
|
364
|
+
let response = redirect(start);
|
|
365
|
+
response = setCookie(response, cookies.verifier, verifier, cookieOptions);
|
|
366
|
+
response = setCookie(response, cookies.state, state, cookieOptions);
|
|
341
367
|
const from = safeInternalPath(url.searchParams.get("from"));
|
|
342
|
-
if (from) setCookie(response, cookies.from, from, cookieOptions);
|
|
368
|
+
if (from) response = setCookie(response, cookies.from, from, cookieOptions);
|
|
343
369
|
return response;
|
|
344
370
|
},
|
|
345
371
|
/** GET `?code=&state=` */
|
|
@@ -355,7 +381,7 @@ function createHandoffTarget(config) {
|
|
|
355
381
|
const device = config.device.read(request);
|
|
356
382
|
const respond = (response) => config.device.persist(response, device);
|
|
357
383
|
try {
|
|
358
|
-
const headers =
|
|
384
|
+
const headers = forwardedRequestHeaders2(request, {
|
|
359
385
|
"Content-Type": "application/json",
|
|
360
386
|
"X-Client-Type": config.clientType,
|
|
361
387
|
"X-Client-Device-ID": device.value
|
|
@@ -404,7 +430,7 @@ function createHandoffSource(config) {
|
|
|
404
430
|
const token = await config.getAccessToken(request);
|
|
405
431
|
if (!token) return sourceLogin();
|
|
406
432
|
try {
|
|
407
|
-
const headers =
|
|
433
|
+
const headers = forwardedRequestHeaders2(request, {
|
|
408
434
|
"Content-Type": "application/json",
|
|
409
435
|
"X-Client-Type": config.clientType,
|
|
410
436
|
Authorization: `Bearer ${token}`
|
|
@@ -531,4 +557,4 @@ async function evaluateSessionGate(input) {
|
|
|
531
557
|
return { kind: "redirect_login" };
|
|
532
558
|
}
|
|
533
559
|
|
|
534
|
-
export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, SSO_ERROR_MESSAGES, applyForwardedClientHeaders, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, forwardedRequestHeaders, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds };
|
|
560
|
+
export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, SSO_ERROR_MESSAGES, applyForwardedClientHeaders2 as applyForwardedClientHeaders, authErrorMessage, brokerPaths, createDeviceIdentity, createHandoffSource, createHandoffTarget, createSessionRefresher, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, forwardedRequestHeaders2 as forwardedRequestHeaders, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isTokenExpired, isTokenExpiringSoon, parseAuthResponse, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
declare const MCP_TOOL_PERMISSIONS: readonly ["dash.domains.view", "dash.workspaces.view", "platform.audit.view", "platform.ops.manage", "platform.ops.view", "platform.tenants.view"];
|
|
2
|
+
declare const MCP_WILDCARD_TOOL_PERMISSIONS: readonly ["dash.domains.view", "dash.workspaces.view", "platform.audit.view", "platform.ops.view", "platform.tenants.view"];
|
|
3
|
+
type MCPToolPermission = (typeof MCP_TOOL_PERMISSIONS)[number];
|
|
4
|
+
|
|
5
|
+
declare const MCP_ACCESS_PERMISSION: "platform.mcp.access";
|
|
6
|
+
declare const MCP_TOKENS_MANAGE_PERMISSION: "platform.mcp.tokens.manage";
|
|
7
|
+
declare const MCP_PERMISSION_WILDCARD: "*";
|
|
8
|
+
type MCPTokenPermission = typeof MCP_ACCESS_PERMISSION | typeof MCP_PERMISSION_WILDCARD | MCPToolPermission;
|
|
9
|
+
type MCPTokenOperation = "platform_user.auth.mcp_tokens.list" | "platform_user.auth.mcp_tokens.mint" | "platform_user.auth.mcp_tokens.revoke";
|
|
10
|
+
type MCPTokenRequestOutcome = "success" | "api_error" | "network_error" | "timeout" | "invalid_response" | "cancelled";
|
|
11
|
+
interface MCPTokenView {
|
|
12
|
+
token_id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
/** Recorded ceilings may include permissions added by a newer server. */
|
|
15
|
+
permissions: string[];
|
|
16
|
+
app_allowlist: string[];
|
|
17
|
+
created_at: string;
|
|
18
|
+
expires_at: string;
|
|
19
|
+
last_used_at?: string;
|
|
20
|
+
revoked_at?: string;
|
|
21
|
+
}
|
|
22
|
+
interface MCPTokenPage {
|
|
23
|
+
items: MCPTokenView[];
|
|
24
|
+
next_cursor?: string;
|
|
25
|
+
has_more: boolean;
|
|
26
|
+
mcp_enabled: boolean;
|
|
27
|
+
}
|
|
28
|
+
interface MCPTokenListParams {
|
|
29
|
+
cursor?: string;
|
|
30
|
+
limit?: number;
|
|
31
|
+
}
|
|
32
|
+
interface MintMCPTokenInput {
|
|
33
|
+
name: string;
|
|
34
|
+
expires_in_days: number;
|
|
35
|
+
permissions: MCPTokenPermission[];
|
|
36
|
+
app_allowlist: string[];
|
|
37
|
+
}
|
|
38
|
+
interface MintMCPTokenResponse {
|
|
39
|
+
token: MCPTokenView;
|
|
40
|
+
raw_token: string;
|
|
41
|
+
}
|
|
42
|
+
interface MCPTokenRequestObservation {
|
|
43
|
+
operation: MCPTokenOperation;
|
|
44
|
+
method: string;
|
|
45
|
+
outcome: MCPTokenRequestOutcome;
|
|
46
|
+
status: number;
|
|
47
|
+
requestId: string;
|
|
48
|
+
durationMs: number;
|
|
49
|
+
attempts: number;
|
|
50
|
+
}
|
|
51
|
+
interface CreateMCPTokenClientOptions {
|
|
52
|
+
baseUrl: string;
|
|
53
|
+
getAccessToken: () => Promise<string | null | undefined> | string | null | undefined;
|
|
54
|
+
getHeaders?: () => Promise<HeadersInit | undefined> | HeadersInit | undefined;
|
|
55
|
+
fetch?: typeof globalThis.fetch;
|
|
56
|
+
/** Per attempt (default 10000). */
|
|
57
|
+
timeoutMs?: number;
|
|
58
|
+
/** Additional attempts for GET only (default 1). Mint and revoke are never retried. */
|
|
59
|
+
retryAttempts?: number;
|
|
60
|
+
/** Cap on one retry delay (default 2000). */
|
|
61
|
+
maxRetryDelayMs?: number;
|
|
62
|
+
observe?: (event: MCPTokenRequestObservation) => void;
|
|
63
|
+
signal?: AbortSignal;
|
|
64
|
+
}
|
|
65
|
+
declare class MCPTokenAPIError extends Error {
|
|
66
|
+
readonly status: number;
|
|
67
|
+
readonly code: string;
|
|
68
|
+
readonly requestId: string;
|
|
69
|
+
constructor(message: string, status: number, code: string, requestId: string);
|
|
70
|
+
}
|
|
71
|
+
declare function isMCPTokenAPIError(error: unknown): error is MCPTokenAPIError;
|
|
72
|
+
declare function createMCPTokenClient(options: CreateMCPTokenClientOptions): {
|
|
73
|
+
readonly list: (params?: MCPTokenListParams) => Promise<MCPTokenPage>;
|
|
74
|
+
readonly mint: (input: MintMCPTokenInput) => Promise<MintMCPTokenResponse>;
|
|
75
|
+
readonly revoke: (tokenId: string) => Promise<void>;
|
|
76
|
+
};
|
|
77
|
+
type MCPTokenClient = ReturnType<typeof createMCPTokenClient>;
|
|
78
|
+
|
|
79
|
+
export { type CreateMCPTokenClientOptions, MCPTokenAPIError, type MCPTokenClient, type MCPTokenListParams, type MCPTokenOperation, type MCPTokenPage, type MCPTokenPermission, type MCPTokenRequestObservation, type MCPTokenRequestOutcome, type MCPTokenView, type MCPToolPermission, MCP_ACCESS_PERMISSION, MCP_PERMISSION_WILDCARD, MCP_TOKENS_MANAGE_PERMISSION, MCP_TOOL_PERMISSIONS, MCP_WILDCARD_TOOL_PERMISSIONS, type MintMCPTokenInput, type MintMCPTokenResponse, createMCPTokenClient, isMCPTokenAPIError };
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// ../transport/src/index.ts
|
|
2
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
|
|
3
|
+
var TransportError = class extends Error {
|
|
4
|
+
constructor(failure) {
|
|
5
|
+
super(failure.message || failure.outcome);
|
|
6
|
+
this.failure = failure;
|
|
7
|
+
this.name = "TransportError";
|
|
8
|
+
}
|
|
9
|
+
failure;
|
|
10
|
+
};
|
|
11
|
+
function isTransportError(error) {
|
|
12
|
+
return error instanceof TransportError || typeof error === "object" && error !== null && error.name === "TransportError";
|
|
13
|
+
}
|
|
14
|
+
function positiveNumber(value, label, fail) {
|
|
15
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw fail(`${label} must be a positive finite number`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function nonNegativeInteger(value, label, fail) {
|
|
19
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw fail(`${label} must be a non-negative safe integer`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function requiredString(value, label, fail) {
|
|
23
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
24
|
+
if (!normalized) throw fail(`${label} is required`);
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
function backoffDelay(attempt, maxDelayMs) {
|
|
28
|
+
const base = Math.min(250 * 2 ** Math.max(attempt, 0), maxDelayMs);
|
|
29
|
+
return Math.floor(Math.random() * base);
|
|
30
|
+
}
|
|
31
|
+
function retryDelay(response, attempt, maxDelayMs) {
|
|
32
|
+
const value = response.headers.get("Retry-After");
|
|
33
|
+
if (!value) return backoffDelay(attempt, maxDelayMs);
|
|
34
|
+
const seconds = Number(value);
|
|
35
|
+
if (Number.isFinite(seconds)) return Math.min(Math.max(seconds * 1e3, 0), maxDelayMs);
|
|
36
|
+
const date = Date.parse(value);
|
|
37
|
+
return Number.isNaN(date) ? backoffDelay(attempt, maxDelayMs) : Math.min(Math.max(date - Date.now(), 0), maxDelayMs);
|
|
38
|
+
}
|
|
39
|
+
function wait(ms) {
|
|
40
|
+
return ms <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, ms));
|
|
41
|
+
}
|
|
42
|
+
function combineSignals(...signals) {
|
|
43
|
+
const active = signals.filter((signal) => Boolean(signal));
|
|
44
|
+
if (active.length === 0) return void 0;
|
|
45
|
+
if (active.length === 1) return active[0];
|
|
46
|
+
return AbortSignal.any(active);
|
|
47
|
+
}
|
|
48
|
+
function isRetryableRequest(method, headers, retry) {
|
|
49
|
+
if (method === "GET") return true;
|
|
50
|
+
return retry.idempotentWrites === true && headers.has("Idempotency-Key");
|
|
51
|
+
}
|
|
52
|
+
async function performRequest(transport, url, init, observe, decode) {
|
|
53
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
54
|
+
const headers = new Headers(init.headers);
|
|
55
|
+
const startedAt = Date.now();
|
|
56
|
+
const retryable = isRetryableRequest(method, headers, transport.retry);
|
|
57
|
+
const maxAttempts = retryable ? transport.retry.attempts : 0;
|
|
58
|
+
let attempts = 0;
|
|
59
|
+
let status = 0;
|
|
60
|
+
let requestId = "";
|
|
61
|
+
let outcome = "network_error";
|
|
62
|
+
const fail = (failure) => {
|
|
63
|
+
outcome = failure.outcome;
|
|
64
|
+
throw new TransportError({ requestId, ...failure });
|
|
65
|
+
};
|
|
66
|
+
try {
|
|
67
|
+
let response;
|
|
68
|
+
for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
|
|
69
|
+
attempts = attempt + 1;
|
|
70
|
+
const controller = new AbortController();
|
|
71
|
+
const timeout = setTimeout(() => controller.abort(), transport.timeoutMs);
|
|
72
|
+
try {
|
|
73
|
+
response = await transport.fetch(url, {
|
|
74
|
+
...init,
|
|
75
|
+
method,
|
|
76
|
+
headers,
|
|
77
|
+
signal: combineSignals(transport.signal, init.signal, controller.signal)
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
if (controller.signal.aborted) fail({ outcome: "timeout", status: 0, code: "", message: "" });
|
|
81
|
+
if (transport.signal?.aborted || init.signal?.aborted) fail({ outcome: "cancelled", status: 0, code: "", message: "" });
|
|
82
|
+
if (attempt < maxAttempts) {
|
|
83
|
+
await wait(backoffDelay(attempt, transport.retry.maxDelayMs));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
fail({ outcome: "network_error", status: 0, code: "", message: "" });
|
|
87
|
+
} finally {
|
|
88
|
+
clearTimeout(timeout);
|
|
89
|
+
}
|
|
90
|
+
if (!RETRYABLE_STATUSES.has(response.status) || attempt === maxAttempts) break;
|
|
91
|
+
await wait(retryDelay(response, attempt, transport.retry.maxDelayMs));
|
|
92
|
+
}
|
|
93
|
+
if (!response) return fail({ outcome: "network_error", status: 0, code: "", message: "" });
|
|
94
|
+
status = response.status;
|
|
95
|
+
requestId = response.headers.get("X-Request-ID") ?? "";
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const envelope = await response.json().catch(() => ({}));
|
|
98
|
+
if (typeof envelope.request_id === "string") requestId = envelope.request_id;
|
|
99
|
+
return fail({
|
|
100
|
+
outcome: "api_error",
|
|
101
|
+
status,
|
|
102
|
+
code: typeof envelope.code === "string" ? envelope.code : "",
|
|
103
|
+
message: typeof envelope.message === "string" ? envelope.message : "",
|
|
104
|
+
requestId
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
let payload;
|
|
108
|
+
try {
|
|
109
|
+
if (status === 204) {
|
|
110
|
+
payload = void 0;
|
|
111
|
+
} else {
|
|
112
|
+
const text = await response.text();
|
|
113
|
+
const parsed = text ? JSON.parse(text) : void 0;
|
|
114
|
+
payload = decode ? decode(parsed) : parsed;
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
return fail({ outcome: "invalid_response", status, code: "", message: "", requestId });
|
|
118
|
+
}
|
|
119
|
+
outcome = "success";
|
|
120
|
+
return { payload, status, requestId };
|
|
121
|
+
} finally {
|
|
122
|
+
if (observe) {
|
|
123
|
+
try {
|
|
124
|
+
observe({ method, outcome, status, requestId, durationMs: Math.max(0, Date.now() - startedAt), attempts });
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/access/generated/mcp-tools.ts
|
|
132
|
+
var MCP_TOOL_PERMISSIONS = [
|
|
133
|
+
"dash.domains.view",
|
|
134
|
+
"dash.workspaces.view",
|
|
135
|
+
"platform.audit.view",
|
|
136
|
+
"platform.ops.manage",
|
|
137
|
+
"platform.ops.view",
|
|
138
|
+
"platform.tenants.view"
|
|
139
|
+
];
|
|
140
|
+
var MCP_WILDCARD_TOOL_PERMISSIONS = [
|
|
141
|
+
"dash.domains.view",
|
|
142
|
+
"dash.workspaces.view",
|
|
143
|
+
"platform.audit.view",
|
|
144
|
+
"platform.ops.view",
|
|
145
|
+
"platform.tenants.view"
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
// src/mcp-tokens.ts
|
|
149
|
+
var MCP_ACCESS_PERMISSION = "platform.mcp.access";
|
|
150
|
+
var MCP_TOKENS_MANAGE_PERMISSION = "platform.mcp.tokens.manage";
|
|
151
|
+
var MCP_PERMISSION_WILDCARD = "*";
|
|
152
|
+
var MCPTokenAPIError = class extends Error {
|
|
153
|
+
constructor(message, status, code, requestId) {
|
|
154
|
+
super(message);
|
|
155
|
+
this.status = status;
|
|
156
|
+
this.code = code;
|
|
157
|
+
this.requestId = requestId;
|
|
158
|
+
this.name = "MCPTokenAPIError";
|
|
159
|
+
}
|
|
160
|
+
status;
|
|
161
|
+
code;
|
|
162
|
+
requestId;
|
|
163
|
+
};
|
|
164
|
+
function isMCPTokenAPIError(error) {
|
|
165
|
+
return error instanceof MCPTokenAPIError || typeof error === "object" && error !== null && error.name === "MCPTokenAPIError" && typeof error.status === "number" && typeof error.code === "string" && typeof error.requestId === "string";
|
|
166
|
+
}
|
|
167
|
+
var configurationError = (message) => new Error(message);
|
|
168
|
+
var basePath = "/api/v1/platform/auth/me/mcp-tokens";
|
|
169
|
+
var tokenIDPattern = /^[0-9a-f]{32}$/;
|
|
170
|
+
var rawTokenPattern = /^sd_pat_([0-9a-f]{32})_[A-Za-z0-9_-]{43}$/;
|
|
171
|
+
var timestampPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/;
|
|
172
|
+
function invalidResponse() {
|
|
173
|
+
throw new Error("invalid MCP token response");
|
|
174
|
+
}
|
|
175
|
+
function objectValue(value) {
|
|
176
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) invalidResponse();
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
function responseString(value, options = {}) {
|
|
180
|
+
if (value === void 0 && options.optional) return void 0;
|
|
181
|
+
if (typeof value !== "string" || value.length === 0) invalidResponse();
|
|
182
|
+
if (options.tokenID && !tokenIDPattern.test(value)) invalidResponse();
|
|
183
|
+
if (options.timestamp && (!timestampPattern.test(value) || !Number.isFinite(Date.parse(value)))) invalidResponse();
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
function responseStrings(value) {
|
|
187
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry.length === 0)) invalidResponse();
|
|
188
|
+
return [...value];
|
|
189
|
+
}
|
|
190
|
+
function parseMCPTokenView(value) {
|
|
191
|
+
const item = objectValue(value);
|
|
192
|
+
return {
|
|
193
|
+
token_id: responseString(item.token_id, { tokenID: true }),
|
|
194
|
+
name: responseString(item.name),
|
|
195
|
+
permissions: responseStrings(item.permissions),
|
|
196
|
+
app_allowlist: responseStrings(item.app_allowlist),
|
|
197
|
+
created_at: responseString(item.created_at, { timestamp: true }),
|
|
198
|
+
expires_at: responseString(item.expires_at, { timestamp: true }),
|
|
199
|
+
last_used_at: responseString(item.last_used_at, { optional: true, timestamp: true }),
|
|
200
|
+
revoked_at: responseString(item.revoked_at, { optional: true, timestamp: true })
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function parseMCPTokenPage(value) {
|
|
204
|
+
const page = objectValue(value);
|
|
205
|
+
if (!Array.isArray(page.items) || typeof page.has_more !== "boolean" || typeof page.mcp_enabled !== "boolean") invalidResponse();
|
|
206
|
+
const nextCursor = responseString(page.next_cursor, { optional: true });
|
|
207
|
+
if (Boolean(nextCursor) !== page.has_more) invalidResponse();
|
|
208
|
+
return {
|
|
209
|
+
items: page.items.map(parseMCPTokenView),
|
|
210
|
+
next_cursor: nextCursor,
|
|
211
|
+
has_more: page.has_more,
|
|
212
|
+
mcp_enabled: page.mcp_enabled
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function parseMintMCPTokenResponse(value) {
|
|
216
|
+
const response = objectValue(value);
|
|
217
|
+
const token = parseMCPTokenView(response.token);
|
|
218
|
+
const rawToken = responseString(response.raw_token);
|
|
219
|
+
const match = rawToken.match(rawTokenPattern);
|
|
220
|
+
if (!match || match[1] !== token.token_id) invalidResponse();
|
|
221
|
+
return { token, raw_token: rawToken };
|
|
222
|
+
}
|
|
223
|
+
function normalizedStrings(values, label, max) {
|
|
224
|
+
if (!Array.isArray(values)) throw configurationError(`${label} must be an array`);
|
|
225
|
+
const normalized = [...new Set(values.map((value) => requiredString(value, label, configurationError)))];
|
|
226
|
+
if (normalized.length > max) throw configurationError(`${label} must contain at most ${max} entries`);
|
|
227
|
+
return normalized;
|
|
228
|
+
}
|
|
229
|
+
function mintInput(value) {
|
|
230
|
+
const name = requiredString(value.name, "name", configurationError);
|
|
231
|
+
if (name.length > 64) throw configurationError("name must be 64 characters or fewer");
|
|
232
|
+
if (!Number.isSafeInteger(value.expires_in_days) || value.expires_in_days < 1 || value.expires_in_days > 90) {
|
|
233
|
+
throw configurationError("expires_in_days must be an integer from 1 to 90");
|
|
234
|
+
}
|
|
235
|
+
const permissions = normalizedStrings(value.permissions, "permissions", 32);
|
|
236
|
+
if (permissions.includes(MCP_PERMISSION_WILDCARD)) {
|
|
237
|
+
if (permissions.length !== 1) {
|
|
238
|
+
throw configurationError('permissions "*" must be the only entry');
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
if (!permissions.includes(MCP_ACCESS_PERMISSION)) throw configurationError(`permissions must include ${MCP_ACCESS_PERMISSION}`);
|
|
242
|
+
if (permissions.length < 2) throw configurationError("permissions must include at least one MCP tool permission");
|
|
243
|
+
const allowedPermissions = /* @__PURE__ */ new Set([MCP_ACCESS_PERMISSION, ...MCP_TOOL_PERMISSIONS]);
|
|
244
|
+
if (permissions.some((permission) => !allowedPermissions.has(permission))) {
|
|
245
|
+
throw configurationError("permissions contains a key that is not available to MCP tools");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const appAllowlist = normalizedStrings(value.app_allowlist, "app_allowlist", 32);
|
|
249
|
+
if (appAllowlist.includes("*") && appAllowlist.length !== 1) {
|
|
250
|
+
throw configurationError('app_allowlist "*" must be the only entry');
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
name,
|
|
254
|
+
expires_in_days: value.expires_in_days,
|
|
255
|
+
permissions,
|
|
256
|
+
app_allowlist: appAllowlist
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function tokenIDInput(value) {
|
|
260
|
+
const tokenID = requiredString(value, "tokenId", configurationError);
|
|
261
|
+
if (!tokenIDPattern.test(tokenID)) throw configurationError("tokenId must be a 32-character hexadecimal MCP token id");
|
|
262
|
+
return tokenID;
|
|
263
|
+
}
|
|
264
|
+
function createMCPTokenClient(options) {
|
|
265
|
+
const baseUrl = requiredString(options.baseUrl, "baseUrl", configurationError).replace(/\/+$/, "");
|
|
266
|
+
const transport = {
|
|
267
|
+
fetch: (input, init) => (options.fetch ?? globalThis.fetch)(input, init),
|
|
268
|
+
timeoutMs: positiveNumber(options.timeoutMs ?? 1e4, "timeoutMs", configurationError),
|
|
269
|
+
retry: {
|
|
270
|
+
attempts: nonNegativeInteger(options.retryAttempts ?? 1, "retryAttempts", configurationError),
|
|
271
|
+
maxDelayMs: positiveNumber(options.maxRetryDelayMs ?? 2e3, "maxRetryDelayMs", configurationError),
|
|
272
|
+
idempotentWrites: false
|
|
273
|
+
},
|
|
274
|
+
signal: options.signal
|
|
275
|
+
};
|
|
276
|
+
async function request(operation, path, decode, init = {}) {
|
|
277
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
278
|
+
const report = options.observe && ((event) => {
|
|
279
|
+
options.observe?.({ operation, ...event });
|
|
280
|
+
});
|
|
281
|
+
let headers;
|
|
282
|
+
try {
|
|
283
|
+
const accessToken = requiredString(await options.getAccessToken() ?? "", "access token", configurationError);
|
|
284
|
+
headers = new Headers(await options.getHeaders?.());
|
|
285
|
+
new Headers(init.headers).forEach((value, key) => headers.set(key, value));
|
|
286
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
287
|
+
headers.set("Accept", "application/json");
|
|
288
|
+
if (init.body !== void 0) headers.set("Content-Type", "application/json");
|
|
289
|
+
} catch (error) {
|
|
290
|
+
try {
|
|
291
|
+
report?.({ method, outcome: "network_error", status: 0, requestId: "", durationMs: 0, attempts: 0 });
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const result = await performRequest(
|
|
298
|
+
transport,
|
|
299
|
+
`${baseUrl}${path}`,
|
|
300
|
+
{ ...init, method, headers, cache: "no-store" },
|
|
301
|
+
report,
|
|
302
|
+
decode
|
|
303
|
+
);
|
|
304
|
+
return result.payload;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (!isTransportError(error)) throw error;
|
|
307
|
+
const { outcome, status, code, message, requestId } = error.failure;
|
|
308
|
+
switch (outcome) {
|
|
309
|
+
case "timeout":
|
|
310
|
+
throw new MCPTokenAPIError("MCP token request timed out", 0, "mcp_token_request_timeout", "");
|
|
311
|
+
case "cancelled":
|
|
312
|
+
throw new MCPTokenAPIError("MCP token request was cancelled", 0, "mcp_token_request_cancelled", "");
|
|
313
|
+
case "network_error":
|
|
314
|
+
throw new MCPTokenAPIError("MCP token service is unavailable", 0, "mcp_token_service_unavailable", "");
|
|
315
|
+
case "invalid_response":
|
|
316
|
+
throw new MCPTokenAPIError("MCP token service returned an invalid response", status, "mcp_token_invalid_response", requestId);
|
|
317
|
+
case "api_error":
|
|
318
|
+
throw new MCPTokenAPIError(message || code || "MCP token request failed", status, code, requestId);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
list(params = {}) {
|
|
324
|
+
const query = new URLSearchParams();
|
|
325
|
+
const cursor = params.cursor?.trim();
|
|
326
|
+
if (cursor) query.set("cursor", cursor);
|
|
327
|
+
if (params.limit !== void 0) {
|
|
328
|
+
const limit = positiveNumber(params.limit, "limit", configurationError);
|
|
329
|
+
if (!Number.isSafeInteger(limit) || limit > 100) throw configurationError("limit must be an integer from 1 to 100");
|
|
330
|
+
query.set("limit", String(limit));
|
|
331
|
+
}
|
|
332
|
+
return request(
|
|
333
|
+
"platform_user.auth.mcp_tokens.list",
|
|
334
|
+
`${basePath}${query.size ? `?${query}` : ""}`,
|
|
335
|
+
parseMCPTokenPage
|
|
336
|
+
);
|
|
337
|
+
},
|
|
338
|
+
mint(input) {
|
|
339
|
+
return request("platform_user.auth.mcp_tokens.mint", basePath, parseMintMCPTokenResponse, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
body: JSON.stringify(mintInput(input))
|
|
342
|
+
});
|
|
343
|
+
},
|
|
344
|
+
revoke(tokenId) {
|
|
345
|
+
return request(
|
|
346
|
+
"platform_user.auth.mcp_tokens.revoke",
|
|
347
|
+
`${basePath}/${tokenIDInput(tokenId)}`,
|
|
348
|
+
(payload) => {
|
|
349
|
+
if (payload !== void 0) invalidResponse();
|
|
350
|
+
},
|
|
351
|
+
{ method: "DELETE" }
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export { MCPTokenAPIError, MCP_ACCESS_PERMISSION, MCP_PERMISSION_WILDCARD, MCP_TOKENS_MANAGE_PERMISSION, MCP_TOOL_PERMISSIONS, MCP_WILDCARD_TOOL_PERMISSIONS, createMCPTokenClient, isMCPTokenAPIError };
|
package/dist/next.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { R as Resolvable, A as AsyncResolvable, S as SessionData, C as CookieOptions } from './index-
|
|
2
|
-
export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as RefreshedSession, l as SSO_ERROR_MESSAGES, m as SessionGateDecision, n as SessionGateInput, o as SessionRefresher, p as SessionRefresherConfig, q as SessionUser, T as TokenPayload, r as
|
|
3
|
-
export { applyForwardedClientHeaders, forwardedRequestHeaders } from '@c1m-public/transport';
|
|
1
|
+
import { R as Resolvable, A as AsyncResolvable, S as SessionData, C as CookieOptions } from './index-aKe58m6n.js';
|
|
2
|
+
export { a as AUTH_ERROR_MESSAGES, b as AuthResponse, D as DeviceIdentity, c as DeviceIdentityConfig, d as DeviceIdentityStore, H as HANDOFF_ERROR_FALLBACK, e as HANDOFF_ERROR_MESSAGES, f as HandoffSource, g as HandoffSourceConfig, h as HandoffSourceTarget, i as HandoffTarget, j as HandoffTargetConfig, k as RefreshedSession, l as SSO_ERROR_MESSAGES, m as SessionGateDecision, n as SessionGateInput, o as SessionRefresher, p as SessionRefresherConfig, q as SessionUser, T as TokenPayload, r as applyForwardedClientHeaders, s as authErrorMessage, t as brokerPaths, u as createDeviceIdentity, v as createHandoffSource, w as createHandoffTarget, x as createSessionRefresher, y as decodeBase64Url, z as decodeDefaultSession, B as decodeTokenPayload, E as deleteCookie, F as evaluateSessionGate, G as forwardedRequestHeaders, I as handoffErrorMessage, J as hasSessionShape, K as hasTokenAudience, L as isAuthErrorCode, M as isHandoffErrorCode, N as isTokenExpired, O as isTokenExpiringSoon, P as parseAuthResponse, Q as parseRefreshEnvelope, U as readCookie, V as safeInternalPath, W as serializeCookie, X as setCookie, Y as sourceStartPath, Z as tokenMaxAgeSeconds } from './index-aKe58m6n.js';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* The error vocabulary every first-party platform surface uses at its edge.
|
package/dist/next.js
CHANGED
|
@@ -128,6 +128,14 @@ function forwardedRequestHeaders(request, init) {
|
|
|
128
128
|
return applyForwardedClientHeaders(request.headers, new Headers(init));
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// src/forwarding.ts
|
|
132
|
+
function applyForwardedClientHeaders2(incoming, headers = new Headers()) {
|
|
133
|
+
return applyForwardedClientHeaders(incoming, headers);
|
|
134
|
+
}
|
|
135
|
+
function forwardedRequestHeaders2(request, init) {
|
|
136
|
+
return forwardedRequestHeaders(request, init);
|
|
137
|
+
}
|
|
138
|
+
|
|
131
139
|
// src/cookies.ts
|
|
132
140
|
function readCookie(request, name) {
|
|
133
141
|
const header = request.headers.get("cookie");
|
|
@@ -156,12 +164,27 @@ function serializeCookie(name, value, options = {}) {
|
|
|
156
164
|
return parts.join("; ");
|
|
157
165
|
}
|
|
158
166
|
function setCookie(response, name, value, options) {
|
|
159
|
-
response
|
|
160
|
-
return response;
|
|
167
|
+
return appendSetCookie(response, serializeCookie(name, value, options));
|
|
161
168
|
}
|
|
162
169
|
function deleteCookie(response, name, options) {
|
|
163
|
-
response
|
|
164
|
-
|
|
170
|
+
return appendSetCookie(response, serializeCookie(name, "", { ...options, maxAge: 0 }));
|
|
171
|
+
}
|
|
172
|
+
function appendSetCookie(response, cookie) {
|
|
173
|
+
try {
|
|
174
|
+
response.headers.append("Set-Cookie", cookie);
|
|
175
|
+
return response;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (!(error instanceof TypeError) || !/immutable/i.test(error.message)) {
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
const mutable = new Response(response.body, {
|
|
181
|
+
status: response.status,
|
|
182
|
+
statusText: response.statusText,
|
|
183
|
+
headers: response.headers
|
|
184
|
+
});
|
|
185
|
+
mutable.headers.append("Set-Cookie", cookie);
|
|
186
|
+
return mutable;
|
|
187
|
+
}
|
|
165
188
|
}
|
|
166
189
|
|
|
167
190
|
// src/device-cookie.ts
|
|
@@ -321,8 +344,11 @@ function createHandoffTarget(config) {
|
|
|
321
344
|
const cookieOptions = { httpOnly: true, secure, sameSite: "lax", path: "/", maxAge: HANDOFF_COOKIE_TTL_SECONDS };
|
|
322
345
|
const timeout = config.timeoutMs ?? config.fetchTimeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
323
346
|
const clearHandoffCookies = (response) => {
|
|
324
|
-
|
|
325
|
-
|
|
347
|
+
let result = response;
|
|
348
|
+
for (const name of Object.values(cookies)) {
|
|
349
|
+
result = deleteCookie(result, name, { path: "/", secure });
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
326
352
|
};
|
|
327
353
|
const fail = (origin, code) => clearHandoffCookies(loginErrorRedirect(origin, config.loginPath, code));
|
|
328
354
|
return {
|
|
@@ -340,11 +366,11 @@ function createHandoffTarget(config) {
|
|
|
340
366
|
const start = new URL(sourceStartPath(config.app), sourceOrigin);
|
|
341
367
|
start.searchParams.set("code_challenge", await s256(verifier));
|
|
342
368
|
start.searchParams.set("state", state);
|
|
343
|
-
|
|
344
|
-
setCookie(response, cookies.verifier, verifier, cookieOptions);
|
|
345
|
-
setCookie(response, cookies.state, state, cookieOptions);
|
|
369
|
+
let response = redirect(start);
|
|
370
|
+
response = setCookie(response, cookies.verifier, verifier, cookieOptions);
|
|
371
|
+
response = setCookie(response, cookies.state, state, cookieOptions);
|
|
346
372
|
const from = safeInternalPath(url.searchParams.get("from"));
|
|
347
|
-
if (from) setCookie(response, cookies.from, from, cookieOptions);
|
|
373
|
+
if (from) response = setCookie(response, cookies.from, from, cookieOptions);
|
|
348
374
|
return response;
|
|
349
375
|
},
|
|
350
376
|
/** GET `?code=&state=` */
|
|
@@ -360,7 +386,7 @@ function createHandoffTarget(config) {
|
|
|
360
386
|
const device = config.device.read(request);
|
|
361
387
|
const respond = (response) => config.device.persist(response, device);
|
|
362
388
|
try {
|
|
363
|
-
const headers =
|
|
389
|
+
const headers = forwardedRequestHeaders2(request, {
|
|
364
390
|
"Content-Type": "application/json",
|
|
365
391
|
"X-Client-Type": config.clientType,
|
|
366
392
|
"X-Client-Device-ID": device.value
|
|
@@ -409,7 +435,7 @@ function createHandoffSource(config) {
|
|
|
409
435
|
const token = await config.getAccessToken(request);
|
|
410
436
|
if (!token) return sourceLogin();
|
|
411
437
|
try {
|
|
412
|
-
const headers =
|
|
438
|
+
const headers = forwardedRequestHeaders2(request, {
|
|
413
439
|
"Content-Type": "application/json",
|
|
414
440
|
"X-Client-Type": config.clientType,
|
|
415
441
|
Authorization: `Bearer ${token}`
|
|
@@ -597,7 +623,7 @@ async function controlRequest(run) {
|
|
|
597
623
|
async function buildForwardedRequestHeaders(initHeaders) {
|
|
598
624
|
const headers$1 = new Headers(initHeaders);
|
|
599
625
|
try {
|
|
600
|
-
|
|
626
|
+
applyForwardedClientHeaders2(await headers(), headers$1);
|
|
601
627
|
} catch {
|
|
602
628
|
}
|
|
603
629
|
return headers$1;
|
|
@@ -747,10 +773,11 @@ function createSessionStore(config) {
|
|
|
747
773
|
}
|
|
748
774
|
function writeSessionCookies(response, accessToken, refreshToken, rememberMe = config.defaultRememberMe ?? true) {
|
|
749
775
|
assertAudience(accessToken);
|
|
750
|
-
setCookie(response, names.session, accessToken, cookieOptions(rememberMe, accessToken));
|
|
751
|
-
if (refreshToken)
|
|
752
|
-
|
|
753
|
-
|
|
776
|
+
let result = setCookie(response, names.session, accessToken, cookieOptions(rememberMe, accessToken));
|
|
777
|
+
if (refreshToken) {
|
|
778
|
+
result = setCookie(result, names.refreshToken, refreshToken, cookieOptions(rememberMe, refreshToken));
|
|
779
|
+
}
|
|
780
|
+
return setCookie(result, names.rememberMe, rememberMe ? "true" : "false", cookieOptions(rememberMe));
|
|
754
781
|
}
|
|
755
782
|
async function getAccessToken() {
|
|
756
783
|
const store = await jar("getAccessToken");
|
|
@@ -805,8 +832,11 @@ function createSessionStore(config) {
|
|
|
805
832
|
}
|
|
806
833
|
}
|
|
807
834
|
function clearSessionCookies(response) {
|
|
808
|
-
|
|
809
|
-
|
|
835
|
+
let result = response;
|
|
836
|
+
for (const name of [names.session, names.refreshToken, names.rememberMe]) {
|
|
837
|
+
result = deleteCookie(result, name, clearOptions());
|
|
838
|
+
}
|
|
839
|
+
return result;
|
|
810
840
|
}
|
|
811
841
|
async function requireSession() {
|
|
812
842
|
const session = await getSession();
|
|
@@ -834,4 +864,4 @@ function createSessionStore(config) {
|
|
|
834
864
|
};
|
|
835
865
|
}
|
|
836
866
|
|
|
837
|
-
export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, PlatformServerFetchError, RecentAuthenticationRequiredError, SSOReauthenticationRequiredError, SSO_ERROR_MESSAGES, SessionExpiredError, applyForwardedClientHeaders, authErrorMessage, authenticationErrorFor, brokerPaths, buildForwardedRequestHeaders, controlRequest, createDeviceIdentity, createHandoffSource, createHandoffTarget, createPlatformFetch, createSessionRefresher, createSessionStore, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, forwardedRequestHeaders, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isPlatformServerFetchError, isRecentAuthenticationRequiredError, isSSOReauthenticationRequiredError, isSessionExpiredError, isTokenExpired, isTokenExpiringSoon, mapControlError, parseAuthResponse, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds };
|
|
867
|
+
export { AUTH_ERROR_MESSAGES, HANDOFF_ERROR_FALLBACK, HANDOFF_ERROR_MESSAGES, PlatformServerFetchError, RecentAuthenticationRequiredError, SSOReauthenticationRequiredError, SSO_ERROR_MESSAGES, SessionExpiredError, applyForwardedClientHeaders2 as applyForwardedClientHeaders, authErrorMessage, authenticationErrorFor, brokerPaths, buildForwardedRequestHeaders, controlRequest, createDeviceIdentity, createHandoffSource, createHandoffTarget, createPlatformFetch, createSessionRefresher, createSessionStore, decodeBase64Url, decodeDefaultSession, decodeTokenPayload, deleteCookie, evaluateSessionGate, forwardedRequestHeaders2 as forwardedRequestHeaders, handoffErrorMessage, hasSessionShape, hasTokenAudience, isAuthErrorCode, isHandoffErrorCode, isPlatformServerFetchError, isRecentAuthenticationRequiredError, isSSOReauthenticationRequiredError, isSessionExpiredError, isTokenExpired, isTokenExpiringSoon, mapControlError, parseAuthResponse, parseRefreshEnvelope, readCookie, safeInternalPath, serializeCookie, setCookie, sourceStartPath, tokenMaxAgeSeconds };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c1m-public/platform-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Session and authorization helpers for C1M platform surfaces. The root is runtime-neutral (Web Request/Response): token/session reads, the first-party handoff (PKCE target + source), proxy session gate and coalesced refresh; ./next adds the server-only guard; ./access carries the resolved access snapshot, typed permission keys, predicates, a Next loader and React bindings",
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
"types": "./dist/next.d.ts",
|
|
21
21
|
"default": "./dist/next.js"
|
|
22
22
|
},
|
|
23
|
+
"./mcp-tokens": {
|
|
24
|
+
"types": "./dist/mcp-tokens.d.ts",
|
|
25
|
+
"default": "./dist/mcp-tokens.js"
|
|
26
|
+
},
|
|
23
27
|
"./access": {
|
|
24
28
|
"types": "./dist/access/index.d.ts",
|
|
25
29
|
"default": "./dist/access/index.js"
|