permdock 0.0.0 → 0.1.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 +268 -1
- package/dist/a2a/index.d.ts +178 -0
- package/dist/a2a/index.js +271 -0
- package/dist/ai-sdk/index.d.ts +74 -0
- package/dist/ai-sdk/index.js +137 -0
- package/dist/approval-required-TzNB9oQA.js +6 -0
- package/dist/approvals/index.d.ts +3 -0
- package/dist/approvals/index.js +4 -0
- package/dist/approvals-DaxY-Bdg.js +218 -0
- package/dist/approvers-aH0PJkmf.js +20 -0
- package/dist/arazzo-CTeVuXr5.js +70 -0
- package/dist/arazzo-aoTkqYsJ.js +54 -0
- package/dist/authzen/index.d.ts +48 -0
- package/dist/authzen/index.js +336 -0
- package/dist/better-auth/index.d.ts +92 -0
- package/dist/better-auth/index.js +324 -0
- package/dist/budget-JqcRqyhK.d.ts +11 -0
- package/dist/budget-NbEtbJC_.js +11 -0
- package/dist/capability-B68OFnZx.js +171 -0
- package/dist/capability-BDG_nkxt.d.ts +99 -0
- package/dist/catalog/index.d.ts +12 -0
- package/dist/catalog/index.js +2 -0
- package/dist/catalog-B0E6bggc.js +48 -0
- package/dist/catalog-H-vFGwWK.js +55 -0
- package/dist/catalog-doc-BSo_Q_-6.js +298 -0
- package/dist/catalog-idsHc0vo.js +2 -0
- package/dist/catalog-path-CnI0XTFK.js +12 -0
- package/dist/claude-agent/index.d.ts +81 -0
- package/dist/claude-agent/index.js +83 -0
- package/dist/clerk/index.d.ts +56 -0
- package/dist/clerk/index.js +280 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +20 -0
- package/dist/cli/index.d.ts +46 -0
- package/dist/cli/index.js +5 -0
- package/dist/cloud/index.d.ts +91 -0
- package/dist/cloud/index.js +390 -0
- package/dist/cloud-BNQTrmrr.js +50 -0
- package/dist/cloud-Cuxr6Czw.js +117 -0
- package/dist/collect-B7_TIfmg.js +49 -0
- package/dist/collect-O-tOtbnk.js +350 -0
- package/dist/collect-fRPAQIWb.js +2 -0
- package/dist/commands-BMh_Etm2.js +93 -0
- package/dist/compact-y6vfu2_f.js +19 -0
- package/dist/compare-pfyVnBjE.js +12 -0
- package/dist/compile-CQbvyIBr.d.ts +30 -0
- package/dist/compile-D22iwvuV.js +558 -0
- package/dist/config-BPYmjTOp.js +26 -0
- package/dist/connection-JvqwHODC.js +194 -0
- package/dist/context-CKEAabd7.js +73 -0
- package/dist/context-gtAwAILx.js +6 -0
- package/dist/convex/index.d.ts +40 -0
- package/dist/convex/index.js +55 -0
- package/dist/create-BKF3dgFk.d.ts +162 -0
- package/dist/create-CSkhDU9T.js +222 -0
- package/dist/credential-B_rkUNlY.js +326 -0
- package/dist/credentials-BgRX3WEg.js +268 -0
- package/dist/describe-Bt1YD5uS.js +99 -0
- package/dist/describe-DernyItf.d.ts +37 -0
- package/dist/diff-BUzf5Poi.js +48 -0
- package/dist/diff-x5Il8LVa.js +418 -0
- package/dist/digest-B3kc_PgQ.d.ts +19 -0
- package/dist/digest-BI-6aa6d.js +27 -0
- package/dist/doctor-BGle8Gqi.js +1314 -0
- package/dist/doctor-Bqo6FwUa.js +53 -0
- package/dist/drizzle/index.d.ts +72 -0
- package/dist/drizzle/index.js +160 -0
- package/dist/duration-xK2jGFVc.js +25 -0
- package/dist/elysia/index.d.ts +71 -0
- package/dist/elysia/index.js +103 -0
- package/dist/errors-DSMnAVNa.js +43 -0
- package/dist/errors-DbZ1YOnb.d.ts +138 -0
- package/dist/errors-Iy71s7Oj.js +177 -0
- package/dist/errors-m3u9TmDu.js +27 -0
- package/dist/evaluations-DsAU9ppl.js +479 -0
- package/dist/eve/index.d.ts +95 -0
- package/dist/eve/index.js +181 -0
- package/dist/express/index.d.ts +46 -0
- package/dist/express/index.js +108 -0
- package/dist/fastify/index.d.ts +56 -0
- package/dist/fastify/index.js +141 -0
- package/dist/files-B_wAGYmt.js +121 -0
- package/dist/fixtures-Cvaw0gDG.js +64 -0
- package/dist/freeze-y1_jQL4v.js +31 -0
- package/dist/from-snapshot-BMGnhmgv.js +552 -0
- package/dist/generate-CH7ThJ14.js +173 -0
- package/dist/grantee-Bv04_Vzf.js +521 -0
- package/dist/headers-B5RRv3Xm.js +6 -0
- package/dist/headers-C8SHQccD.d.ts +6 -0
- package/dist/helpers-DnSOrhB9.js +682 -0
- package/dist/hono/index.d.ts +70 -0
- package/dist/hono/index.js +131 -0
- package/dist/hosted-BwKQ1BTA.js +265 -0
- package/dist/http-DPuDaChb.d.ts +13 -0
- package/dist/http-GgVApX1k.js +111 -0
- package/dist/index-CltcQ4Bf.d.ts +138 -0
- package/dist/index-LF_hS-yn.d.ts +177 -0
- package/dist/index-fG6NSXlc.d.ts +9 -0
- package/dist/index-xIxJLwOQ.d.ts +415 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +21 -0
- package/dist/jwt/index.d.ts +87 -0
- package/dist/jwt/index.js +571 -0
- package/dist/kernel-YOGCWNLf.js +211 -0
- package/dist/kysely/index.d.ts +77 -0
- package/dist/kysely/index.js +129 -0
- package/dist/load-BZ24CpZP.js +69 -0
- package/dist/load-QOOm1z70.js +2 -0
- package/dist/load-jose-BL6LH4bL.js +11 -0
- package/dist/lru-8q4WZjBV.js +28 -0
- package/dist/manifest-DDQbC6K0.d.ts +118 -0
- package/dist/map-DowSO96j.js +158 -0
- package/dist/map-claims-B_6I1Rn-.js +360 -0
- package/dist/map-error-RJ3ZsUyA.js +16 -0
- package/dist/markers-B0hYLvMD.js +60 -0
- package/dist/may-use-CnFEjVEp.js +37 -0
- package/dist/mcp/index.d.ts +127 -0
- package/dist/mcp/index.js +563 -0
- package/dist/nest/index.d.ts +80 -0
- package/dist/nest/index.js +270 -0
- package/dist/next/client.d.ts +24 -0
- package/dist/next/client.js +31 -0
- package/dist/next/index.d.ts +110 -0
- package/dist/next/index.js +195 -0
- package/dist/next/plugin.d.ts +14 -0
- package/dist/next/plugin.js +2 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node/index.js +66 -0
- package/dist/openai/index.d.ts +71 -0
- package/dist/openai/index.js +105 -0
- package/dist/openapi/index.d.ts +93 -0
- package/dist/openapi/index.js +3 -0
- package/dist/openapi-CwePaF7C.js +164 -0
- package/dist/openapi-DWD0Uc_4.js +250 -0
- package/dist/openapi-DcqM8nU7.js +306 -0
- package/dist/openapi-import-BWRUNNeW.js +5959 -0
- package/dist/openapi-schema-D8j_T9tO.js +6902 -0
- package/dist/orpc/index.d.ts +64 -0
- package/dist/orpc/index.js +188 -0
- package/dist/otel/index.d.ts +13 -0
- package/dist/otel/index.js +202 -0
- package/dist/package-root-zGkxO3oN.js +30 -0
- package/dist/parse-DPfIvFMU.js +135 -0
- package/dist/pdp/index.d.ts +22 -0
- package/dist/pdp/index.js +795 -0
- package/dist/peer-BOr_kJE1.js +31 -0
- package/dist/permdock-yoZ7EjxX.js +2956 -0
- package/dist/permissions-B4apnahQ.d.ts +185 -0
- package/dist/permissions-B8vCg_bI.d.ts +52 -0
- package/dist/permissions-CofRqesP.js +142 -0
- package/dist/permissions-DC7GROPA.js +600 -0
- package/dist/pg-4GJWDUxG.js +953 -0
- package/dist/pins-CqE1jZuF.js +12 -0
- package/dist/plugin-4gr7IxFJ.js +65 -0
- package/dist/policy-ANp36PnI.js +971 -0
- package/dist/policy-CInmNGkM.d.ts +1959 -0
- package/dist/policy-Cq2fB8Q5.d.ts +20 -0
- package/dist/prisma/index.d.ts +135 -0
- package/dist/prisma/index.js +430 -0
- package/dist/problem-Cgp9-KZA.js +226 -0
- package/dist/prompt-CXYhLfsS.js +32 -0
- package/dist/protected-B4K3BKoX.js +138 -0
- package/dist/protected-BuPAycJT.d.ts +26 -0
- package/dist/protected-wY7nXhY9.d.ts +23 -0
- package/dist/provider-_wx46HpB.js +48 -0
- package/dist/provider-wTAUda2v.d.ts +6 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +6 -0
- package/dist/react/provider-client.d.ts +2 -0
- package/dist/react/provider-client.js +3 -0
- package/dist/react-native/index.d.ts +42 -0
- package/dist/react-native/index.js +174 -0
- package/dist/refs-ChLSxbST.js +90 -0
- package/dist/relations-r4soIFZB.js +426 -0
- package/dist/revocations-Cy06JNx2.d.ts +21 -0
- package/dist/rls-BOFU3chG.js +233 -0
- package/dist/rls-CJVja9Ww.js +2119 -0
- package/dist/rls-graph-DxQ39PJj.js +371 -0
- package/dist/rls-helpers-BxRuif0l.js +1346 -0
- package/dist/rls-import-CXV6LBlS.js +942 -0
- package/dist/rls-migrate-DrFPbf8p.js +366 -0
- package/dist/row-check-BajTgQZ-.d.ts +43 -0
- package/dist/run-CJ6UvS3a.js +155 -0
- package/dist/runtime-DP9ttFh1.js +27 -0
- package/dist/saas-BRrZzJkM.js +1193 -0
- package/dist/schema-P8yoma__.js +296 -0
- package/dist/scim/index.d.ts +2 -0
- package/dist/scim/index.js +2 -0
- package/dist/scim-9NFa0XVZ.js +1445 -0
- package/dist/scopes-cKVp_WTw.js +212 -0
- package/dist/server/index.d.ts +123 -0
- package/dist/server/index.js +8 -0
- package/dist/sha256-BO0swRXL.js +150 -0
- package/dist/sink-Bd9PSyfq.js +141 -0
- package/dist/sink-Cfr9-7YC.d.ts +104 -0
- package/dist/skills-C_lpV4Jx.js +50 -0
- package/dist/skills-Dx3QLnMY.js +110 -0
- package/dist/snapshot-DE8dE_hz.js +1051 -0
- package/dist/solid/index.d.ts +63 -0
- package/dist/solid/index.js +197 -0
- package/dist/source-DoBBYJi2.js +6 -0
- package/dist/sources-DOpW5Ila.d.ts +347 -0
- package/dist/sources-DVHM3Mwq.js +284 -0
- package/dist/sql-Bll1d7y0.js +22 -0
- package/dist/src-CTNT-PBW.js +430 -0
- package/dist/ssf/index.d.ts +3 -0
- package/dist/ssf/index.js +622 -0
- package/dist/store-Bq90395i.d.ts +27 -0
- package/dist/store-C6hFW6ta.js +508 -0
- package/dist/stores-CkG2HrSg.js +140 -0
- package/dist/stores-CvOnu_2T.d.ts +41 -0
- package/dist/stream-BhMfZOwr.d.ts +12 -0
- package/dist/stream-Q_xlB6Ko.js +52 -0
- package/dist/style-CTm1AXDT.js +22 -0
- package/dist/subject-C6dLxqHU.js +24 -0
- package/dist/subject-settings-xt7V845g.js +82 -0
- package/dist/supabase/index.d.ts +67 -0
- package/dist/supabase/index.js +4 -0
- package/dist/supabase/middleware.d.ts +77 -0
- package/dist/supabase/middleware.js +68 -0
- package/dist/supabase-D8UlBA7Z.js +83 -0
- package/dist/supabase-DTxGtYdi.js +796 -0
- package/dist/supabase-hook-DW-xP4hU.js +2 -0
- package/dist/supabase-hook-D_OqGb_V.js +1528 -0
- package/dist/svelte/Protected.svelte +34 -0
- package/dist/svelte/index.d.ts +10 -0
- package/dist/svelte/index.js +53 -0
- package/dist/svelte/runtime.d.ts +6 -0
- package/dist/svelte/runtime.js +3 -0
- package/dist/svelte/source.d.ts +10 -0
- package/dist/svelte/source.js +8 -0
- package/dist/terminal/index.d.ts +171 -0
- package/dist/terminal/index.js +821 -0
- package/dist/testing/index.d.ts +656 -0
- package/dist/testing/index.js +3208 -0
- package/dist/testing/saas/index.d.ts +194 -0
- package/dist/testing/saas/index.js +3 -0
- package/dist/testing/saas/permissions.d.ts +2 -0
- package/dist/testing/saas/permissions.js +2 -0
- package/dist/text-diff-BdkkYSbi.js +15 -0
- package/dist/thenable-okc2xDsj.js +10 -0
- package/dist/timeout-D51niuQP.js +8 -0
- package/dist/trpc/index.d.ts +80 -0
- package/dist/trpc/index.js +205 -0
- package/dist/types-B8w5PFtS.d.ts +141 -0
- package/dist/types-B9dmiiab.d.ts +66 -0
- package/dist/types-BBK-exvJ.d.ts +177 -0
- package/dist/types-BvBk1wgn.d.ts +118 -0
- package/dist/types-CxquUByI.d.ts +9 -0
- package/dist/types-D6Z6-rKJ.d.ts +312 -0
- package/dist/types-DbWdEfcm.d.ts +87 -0
- package/dist/types-bva3i4Gf.d.ts +98 -0
- package/dist/types-i4BAOvdb.d.ts +97 -0
- package/dist/unplugin/index.d.ts +7 -0
- package/dist/unplugin/index.js +30 -0
- package/dist/usage-1ozMubLY.js +42 -0
- package/dist/usage-CUnK6Ou9.js +530 -0
- package/dist/usage-PyEor6GA.js +2 -0
- package/dist/validity-D0uRmJKJ.js +224 -0
- package/dist/verifier-B90agnhl.js +461 -0
- package/dist/version-CnSh33Fj.js +16 -0
- package/dist/vocabulary-Dk0fs-Jk.d.ts +56 -0
- package/dist/vue/index.d.ts +57 -0
- package/dist/vue/index.js +203 -0
- package/dist/webmcp/index.d.ts +75 -0
- package/dist/webmcp/index.js +313 -0
- package/package.json +423 -4
- package/rulesets/permdock.yaml +29 -0
- package/schemas/catalog-v1.json +507 -0
- package/schemas/openapi/oas-3.1.json +1298 -0
- package/schemas/openapi/oas-3.2.json +1516 -0
- package/schemas/openapi/overlay-1.1.json +79 -0
- package/schemas/supabase-claims-v1.json +139 -0
- package/schemas/supabase-manifest-v1.json +217 -0
- package/skills/permdock/SKILL.md +66 -0
- package/skills/permdock-agents/SKILL.md +77 -0
- package/skills/permdock-agents/references/runtimes.md +211 -0
- package/skills/permdock-approvals/SKILL.md +80 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +83 -0
- package/skills/permdock-audit/SKILL.md +67 -0
- package/skills/permdock-credentials/SKILL.md +55 -0
- package/skills/permdock-credentials/references/tokens-keys-links.md +127 -0
- package/skills/permdock-data/SKILL.md +57 -0
- package/skills/permdock-data/references/queries-and-rls.md +54 -0
- package/skills/permdock-data/references/relationships.md +65 -0
- package/skills/permdock-tenancy/SKILL.md +77 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +83 -0
- package/skills/permdock-wire/SKILL.md +116 -0
- package/skills/permdock-wire/references/adapters.md +480 -0
- package/skills/permdock-wire/references/policy-options.md +43 -0
|
@@ -0,0 +1,1445 @@
|
|
|
1
|
+
import { n as isReadonlyArray, t as compact } from "./compact-y6vfu2_f.js";
|
|
2
|
+
import { n as freezeDeep } from "./freeze-y1_jQL4v.js";
|
|
3
|
+
import { n as sha256 } from "./sha256-BO0swRXL.js";
|
|
4
|
+
import { a as memorySink, i as membershipEvent } from "./sink-Bd9PSyfq.js";
|
|
5
|
+
//#region src/scim/auth.ts
|
|
6
|
+
function hexToBytes(hex) {
|
|
7
|
+
if (hex.length % 2 !== 0) return;
|
|
8
|
+
const out = new Uint8Array(hex.length / 2);
|
|
9
|
+
for (let index = 0; index < out.length; index += 1) {
|
|
10
|
+
const byte = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
11
|
+
if (Number.isNaN(byte)) return;
|
|
12
|
+
out[index] = byte;
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
function timingSafeEqual(left, right) {
|
|
17
|
+
if (left.length !== right.length) return false;
|
|
18
|
+
let diff = 0;
|
|
19
|
+
for (let index = 0; index < left.length; index += 1) diff |= (left[index] ?? 0) ^ (right[index] ?? 0);
|
|
20
|
+
return diff === 0;
|
|
21
|
+
}
|
|
22
|
+
function bearerToken(request) {
|
|
23
|
+
const header = request.headers.get("authorization");
|
|
24
|
+
if (header === null) return;
|
|
25
|
+
return /^Bearer\s+(\S+)$/iu.exec(header)?.[1];
|
|
26
|
+
}
|
|
27
|
+
async function matchStaticToken(tenant, token, options) {
|
|
28
|
+
const expected = hexToBytes(await options.lookup(tenant) ?? "");
|
|
29
|
+
const actual = sha256(token);
|
|
30
|
+
if (expected === void 0) {
|
|
31
|
+
timingSafeEqual(actual, actual);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return timingSafeEqual(actual, expected);
|
|
35
|
+
}
|
|
36
|
+
function tenantClaim(claims) {
|
|
37
|
+
if (typeof claims["tenant"] === "string" && claims["tenant"] !== "") return claims["tenant"];
|
|
38
|
+
if (typeof claims["tid"] === "string" && claims["tid"] !== "") return claims["tid"];
|
|
39
|
+
}
|
|
40
|
+
async function authenticateScim(input) {
|
|
41
|
+
const bearer = bearerToken(input.request);
|
|
42
|
+
if (bearer === void 0) return {
|
|
43
|
+
ok: false,
|
|
44
|
+
status: 401
|
|
45
|
+
};
|
|
46
|
+
if (input.token !== void 0) {
|
|
47
|
+
if (await matchStaticToken(input.tenant, bearer, input.token)) return {
|
|
48
|
+
ok: true,
|
|
49
|
+
credential: { kind: "token" }
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (input.verifier === void 0) return {
|
|
53
|
+
ok: false,
|
|
54
|
+
status: 401
|
|
55
|
+
};
|
|
56
|
+
const verified = await input.verifier.verify(bearer, compact({ audience: input.audience }));
|
|
57
|
+
if (!verified.ok) return {
|
|
58
|
+
ok: false,
|
|
59
|
+
status: 401
|
|
60
|
+
};
|
|
61
|
+
if (tenantClaim(verified.claims) !== input.tenant) return {
|
|
62
|
+
ok: false,
|
|
63
|
+
status: 403
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
ok: true,
|
|
67
|
+
credential: compact({
|
|
68
|
+
kind: "jwt",
|
|
69
|
+
iss: typeof verified.claims.iss === "string" ? verified.claims.iss : void 0
|
|
70
|
+
})
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function sha256Hex(value) {
|
|
74
|
+
const bytes = sha256(value);
|
|
75
|
+
let hex = "";
|
|
76
|
+
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
77
|
+
return hex;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/scim/types.ts
|
|
81
|
+
const SCIM_CONTENT_TYPE = "application/scim+json";
|
|
82
|
+
const USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
|
|
83
|
+
const GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group";
|
|
84
|
+
const LIST_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
|
|
85
|
+
const ERROR_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:Error";
|
|
86
|
+
const PATCH_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:PatchOp";
|
|
87
|
+
const ROLES_EXTENSION = "urn:permdock:scim:schemas:extension:roles:1.0";
|
|
88
|
+
var DirectoryUniquenessError = class extends Error {
|
|
89
|
+
name = "DirectoryUniquenessError";
|
|
90
|
+
code = "uniqueness";
|
|
91
|
+
constructor(message = "uniqueness") {
|
|
92
|
+
super(message);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var DirectoryNotFoundError = class extends Error {
|
|
96
|
+
name = "DirectoryNotFoundError";
|
|
97
|
+
code = "not-found";
|
|
98
|
+
constructor(message = "not found") {
|
|
99
|
+
super(message);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
function isDirectoryUniquenessError(error) {
|
|
103
|
+
return error instanceof DirectoryUniquenessError;
|
|
104
|
+
}
|
|
105
|
+
function isDirectoryNotFoundError(error) {
|
|
106
|
+
return error instanceof DirectoryNotFoundError;
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/scim/discovery.ts
|
|
110
|
+
const SCHEMA_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Schema";
|
|
111
|
+
function serviceProviderConfig() {
|
|
112
|
+
return {
|
|
113
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
|
114
|
+
patch: { supported: true },
|
|
115
|
+
bulk: {
|
|
116
|
+
supported: false,
|
|
117
|
+
maxOperations: 0,
|
|
118
|
+
maxPayloadSize: 0
|
|
119
|
+
},
|
|
120
|
+
filter: {
|
|
121
|
+
supported: true,
|
|
122
|
+
maxResults: 200
|
|
123
|
+
},
|
|
124
|
+
changePassword: { supported: false },
|
|
125
|
+
sort: { supported: false },
|
|
126
|
+
etag: { supported: false },
|
|
127
|
+
pagination: {
|
|
128
|
+
cursor: true,
|
|
129
|
+
index: true
|
|
130
|
+
},
|
|
131
|
+
authenticationSchemes: [{
|
|
132
|
+
type: "oauthbearertoken",
|
|
133
|
+
name: "OAuth Bearer Token",
|
|
134
|
+
description: "Static bearer or RFC 7523 JWT bearer bound to the tenant.",
|
|
135
|
+
specUri: "https://www.rfc-editor.org/rfc/rfc6750.html",
|
|
136
|
+
primary: true
|
|
137
|
+
}]
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function resourceTypes() {
|
|
141
|
+
return [{
|
|
142
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
|
143
|
+
id: "User",
|
|
144
|
+
name: "User",
|
|
145
|
+
endpoint: "/Users",
|
|
146
|
+
schema: USER_SCHEMA,
|
|
147
|
+
schemaExtensions: []
|
|
148
|
+
}, {
|
|
149
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
|
150
|
+
id: "Group",
|
|
151
|
+
name: "Group",
|
|
152
|
+
endpoint: "/Groups",
|
|
153
|
+
schema: GROUP_SCHEMA,
|
|
154
|
+
schemaExtensions: [{
|
|
155
|
+
schema: ROLES_EXTENSION,
|
|
156
|
+
required: false
|
|
157
|
+
}]
|
|
158
|
+
}];
|
|
159
|
+
}
|
|
160
|
+
function attribute(name, type, options = {}) {
|
|
161
|
+
return {
|
|
162
|
+
name,
|
|
163
|
+
type,
|
|
164
|
+
multiValued: options.multiValued ?? false,
|
|
165
|
+
required: options.required ?? false,
|
|
166
|
+
...options.uniqueness === void 0 ? {} : { uniqueness: options.uniqueness },
|
|
167
|
+
...options.subAttributes === void 0 ? {} : { subAttributes: options.subAttributes }
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function schemas() {
|
|
171
|
+
return [
|
|
172
|
+
{
|
|
173
|
+
schemas: [SCHEMA_SCHEMA],
|
|
174
|
+
id: USER_SCHEMA,
|
|
175
|
+
name: "User",
|
|
176
|
+
attributes: [
|
|
177
|
+
attribute("userName", "string", {
|
|
178
|
+
required: true,
|
|
179
|
+
uniqueness: "server"
|
|
180
|
+
}),
|
|
181
|
+
attribute("externalId", "string", { uniqueness: "server" }),
|
|
182
|
+
attribute("active", "boolean"),
|
|
183
|
+
attribute("emails", "complex", {
|
|
184
|
+
multiValued: true,
|
|
185
|
+
subAttributes: [
|
|
186
|
+
attribute("value", "string"),
|
|
187
|
+
attribute("primary", "boolean"),
|
|
188
|
+
attribute("type", "string")
|
|
189
|
+
]
|
|
190
|
+
})
|
|
191
|
+
]
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
schemas: [SCHEMA_SCHEMA],
|
|
195
|
+
id: GROUP_SCHEMA,
|
|
196
|
+
name: "Group",
|
|
197
|
+
attributes: [
|
|
198
|
+
attribute("displayName", "string", { required: true }),
|
|
199
|
+
attribute("externalId", "string", { uniqueness: "server" }),
|
|
200
|
+
attribute("members", "complex", {
|
|
201
|
+
multiValued: true,
|
|
202
|
+
subAttributes: [attribute("value", "string")]
|
|
203
|
+
})
|
|
204
|
+
]
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
schemas: [SCHEMA_SCHEMA],
|
|
208
|
+
id: ROLES_EXTENSION,
|
|
209
|
+
name: "PermDockRoles",
|
|
210
|
+
attributes: [attribute("roles", "string", { multiValued: true })]
|
|
211
|
+
}
|
|
212
|
+
];
|
|
213
|
+
}
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/scim/filter.ts
|
|
216
|
+
const COMPARE = /* @__PURE__ */ new Set([
|
|
217
|
+
"eq",
|
|
218
|
+
"ne",
|
|
219
|
+
"co",
|
|
220
|
+
"sw"
|
|
221
|
+
]);
|
|
222
|
+
function skipSpace(input, index) {
|
|
223
|
+
let cursor = index;
|
|
224
|
+
while (cursor < input.length && input[cursor] === " ") cursor += 1;
|
|
225
|
+
return cursor;
|
|
226
|
+
}
|
|
227
|
+
function readIdent(input, index) {
|
|
228
|
+
const start = skipSpace(input, index);
|
|
229
|
+
if (!/[A-Za-z]/u.test(input[start] ?? "")) return;
|
|
230
|
+
let cursor = start;
|
|
231
|
+
while (cursor < input.length && /[A-Za-z0-9._]/u.test(input[cursor] ?? "")) cursor += 1;
|
|
232
|
+
return {
|
|
233
|
+
value: input.slice(start, cursor),
|
|
234
|
+
next: cursor
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function readString(input, index) {
|
|
238
|
+
const start = skipSpace(input, index);
|
|
239
|
+
if (input[start] !== "\"") return;
|
|
240
|
+
let cursor = start + 1;
|
|
241
|
+
let value = "";
|
|
242
|
+
while (cursor < input.length) {
|
|
243
|
+
const char = input[cursor];
|
|
244
|
+
if (char === "\\") {
|
|
245
|
+
value += input[cursor + 1] ?? "";
|
|
246
|
+
cursor += 2;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (char === "\"") return {
|
|
250
|
+
value,
|
|
251
|
+
next: cursor + 1
|
|
252
|
+
};
|
|
253
|
+
value += char;
|
|
254
|
+
cursor += 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function readLiteral(input, index) {
|
|
258
|
+
const quoted = readString(input, index);
|
|
259
|
+
if (quoted !== void 0) return quoted;
|
|
260
|
+
const ident = readIdent(input, index);
|
|
261
|
+
if (ident === void 0) return;
|
|
262
|
+
if (ident.value.toLowerCase() === "true") return {
|
|
263
|
+
value: true,
|
|
264
|
+
next: ident.next
|
|
265
|
+
};
|
|
266
|
+
if (ident.value.toLowerCase() === "false") return {
|
|
267
|
+
value: false,
|
|
268
|
+
next: ident.next
|
|
269
|
+
};
|
|
270
|
+
return {
|
|
271
|
+
value: ident.value,
|
|
272
|
+
next: ident.next
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function parsePrimary(input, index) {
|
|
276
|
+
const start = skipSpace(input, index);
|
|
277
|
+
if (input[start] === "(") {
|
|
278
|
+
const inner = parseOr(input, start + 1);
|
|
279
|
+
if (inner === void 0) return;
|
|
280
|
+
const close = skipSpace(input, inner.next);
|
|
281
|
+
if (input[close] !== ")") return;
|
|
282
|
+
return {
|
|
283
|
+
filter: inner.filter,
|
|
284
|
+
next: close + 1
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const attribute = readIdent(input, start);
|
|
288
|
+
if (attribute === void 0) return;
|
|
289
|
+
const opToken = readIdent(input, attribute.next);
|
|
290
|
+
if (opToken === void 0) return;
|
|
291
|
+
const op = opToken.value.toLowerCase();
|
|
292
|
+
if (op === "pr") return {
|
|
293
|
+
filter: {
|
|
294
|
+
op: "pr",
|
|
295
|
+
attribute: attribute.value
|
|
296
|
+
},
|
|
297
|
+
next: opToken.next
|
|
298
|
+
};
|
|
299
|
+
if (!COMPARE.has(op)) return;
|
|
300
|
+
const value = readLiteral(input, opToken.next);
|
|
301
|
+
if (value === void 0) return;
|
|
302
|
+
return {
|
|
303
|
+
filter: {
|
|
304
|
+
op,
|
|
305
|
+
attribute: attribute.value,
|
|
306
|
+
value: value.value
|
|
307
|
+
},
|
|
308
|
+
next: value.next
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function parseAnd(input, index) {
|
|
312
|
+
const first = parsePrimary(input, index);
|
|
313
|
+
if (first === void 0) return;
|
|
314
|
+
const filters = [first.filter];
|
|
315
|
+
let cursor = first.next;
|
|
316
|
+
for (;;) {
|
|
317
|
+
const nextOp = readIdent(input, cursor);
|
|
318
|
+
if (nextOp?.value.toLowerCase() !== "and") break;
|
|
319
|
+
const next = parsePrimary(input, nextOp.next);
|
|
320
|
+
if (next === void 0) return;
|
|
321
|
+
filters.push(next.filter);
|
|
322
|
+
cursor = next.next;
|
|
323
|
+
}
|
|
324
|
+
if (filters.length === 1) return {
|
|
325
|
+
filter: first.filter,
|
|
326
|
+
next: cursor
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
filter: {
|
|
330
|
+
op: "and",
|
|
331
|
+
filters
|
|
332
|
+
},
|
|
333
|
+
next: cursor
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function parseOr(input, index) {
|
|
337
|
+
const first = parseAnd(input, index);
|
|
338
|
+
if (first === void 0) return;
|
|
339
|
+
const filters = [first.filter];
|
|
340
|
+
let cursor = first.next;
|
|
341
|
+
for (;;) {
|
|
342
|
+
const nextOp = readIdent(input, cursor);
|
|
343
|
+
if (nextOp?.value.toLowerCase() !== "or") break;
|
|
344
|
+
const next = parseAnd(input, nextOp.next);
|
|
345
|
+
if (next === void 0) return;
|
|
346
|
+
filters.push(next.filter);
|
|
347
|
+
cursor = next.next;
|
|
348
|
+
}
|
|
349
|
+
if (filters.length === 1) return {
|
|
350
|
+
filter: first.filter,
|
|
351
|
+
next: cursor
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
filter: {
|
|
355
|
+
op: "or",
|
|
356
|
+
filters
|
|
357
|
+
},
|
|
358
|
+
next: cursor
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function parseScimFilter(input) {
|
|
362
|
+
const trimmed = input.trim();
|
|
363
|
+
if (trimmed === "") return;
|
|
364
|
+
const parsed = parseOr(trimmed, 0);
|
|
365
|
+
if (parsed === void 0) return;
|
|
366
|
+
if (skipSpace(trimmed, parsed.next) !== trimmed.length) return;
|
|
367
|
+
return parsed.filter;
|
|
368
|
+
}
|
|
369
|
+
function readAttribute(target, attribute) {
|
|
370
|
+
if (attribute === "members.value") {
|
|
371
|
+
const members = target["members"];
|
|
372
|
+
if (!Array.isArray(members)) return;
|
|
373
|
+
return members.map((member) => {
|
|
374
|
+
if (member !== null && typeof member === "object" && "value" in member) return member.value;
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
const parts = attribute.split(".");
|
|
378
|
+
let current = target;
|
|
379
|
+
for (const part of parts) {
|
|
380
|
+
if (current === null || typeof current !== "object") return;
|
|
381
|
+
current = current[part];
|
|
382
|
+
}
|
|
383
|
+
return current;
|
|
384
|
+
}
|
|
385
|
+
function asString(value) {
|
|
386
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
387
|
+
return String(value);
|
|
388
|
+
}
|
|
389
|
+
function matchCompare(actual, op, expected) {
|
|
390
|
+
if (Array.isArray(actual)) return actual.some((item) => matchCompare(item, op, expected));
|
|
391
|
+
if (actual === void 0 || actual === null) return op === "ne";
|
|
392
|
+
if (typeof expected === "boolean" || typeof actual === "boolean") {
|
|
393
|
+
const left = asString(actual).toLowerCase();
|
|
394
|
+
const right = asString(expected).toLowerCase();
|
|
395
|
+
return op === "eq" ? left === right : op === "ne" ? left !== right : false;
|
|
396
|
+
}
|
|
397
|
+
const left = asString(actual);
|
|
398
|
+
const right = asString(expected);
|
|
399
|
+
switch (op) {
|
|
400
|
+
case "eq": return left === right;
|
|
401
|
+
case "ne": return left !== right;
|
|
402
|
+
case "co": return left.includes(right);
|
|
403
|
+
case "sw": return left.startsWith(right);
|
|
404
|
+
default: return op;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** RFC 7644 §3.4.2.2: `pr` needs a non-empty value; a multi-valued one needs an item. */
|
|
408
|
+
function present(value) {
|
|
409
|
+
if (Array.isArray(value)) return value.some(present);
|
|
410
|
+
return value !== void 0 && value !== null && value !== "";
|
|
411
|
+
}
|
|
412
|
+
function matchFilter(target, filter) {
|
|
413
|
+
if (filter === void 0) return true;
|
|
414
|
+
const record = target;
|
|
415
|
+
switch (filter.op) {
|
|
416
|
+
case "and": return filter.filters.every((item) => matchFilter(target, item));
|
|
417
|
+
case "or": return filter.filters.some((item) => matchFilter(target, item));
|
|
418
|
+
case "pr": return present(readAttribute(record, filter.attribute));
|
|
419
|
+
case "eq":
|
|
420
|
+
case "ne":
|
|
421
|
+
case "co":
|
|
422
|
+
case "sw": return matchCompare(readAttribute(record, filter.attribute), filter.op, filter.value);
|
|
423
|
+
default: return filter;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const SUPPORTED_FILTER_ATTRIBUTES = /* @__PURE__ */ new Set([
|
|
427
|
+
"id",
|
|
428
|
+
"userName",
|
|
429
|
+
"externalId",
|
|
430
|
+
"active",
|
|
431
|
+
"displayName",
|
|
432
|
+
"members.value"
|
|
433
|
+
]);
|
|
434
|
+
function filterSupported(filter) {
|
|
435
|
+
switch (filter.op) {
|
|
436
|
+
case "and":
|
|
437
|
+
case "or": return filter.filters.every(filterSupported);
|
|
438
|
+
case "pr":
|
|
439
|
+
case "eq":
|
|
440
|
+
case "ne":
|
|
441
|
+
case "co":
|
|
442
|
+
case "sw": return SUPPORTED_FILTER_ATTRIBUTES.has(filter.attribute);
|
|
443
|
+
default: return filter;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/scim/body.ts
|
|
448
|
+
function isRecord$1(value) {
|
|
449
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
450
|
+
}
|
|
451
|
+
async function readJson(request) {
|
|
452
|
+
const text = await request.text();
|
|
453
|
+
if (text === "") return;
|
|
454
|
+
return JSON.parse(text);
|
|
455
|
+
}
|
|
456
|
+
function schemasOk(body, required) {
|
|
457
|
+
const listed = body["schemas"];
|
|
458
|
+
if (!Array.isArray(listed)) return false;
|
|
459
|
+
return listed.includes(required) || listed.includes("urn:ietf:params:scim:api:messages:2.0:PatchOp");
|
|
460
|
+
}
|
|
461
|
+
function readRoles(body) {
|
|
462
|
+
const extension = body[ROLES_EXTENSION];
|
|
463
|
+
if (!isRecord$1(extension) || !Array.isArray(extension["roles"])) return;
|
|
464
|
+
return extension["roles"].filter((item) => typeof item === "string");
|
|
465
|
+
}
|
|
466
|
+
function userFromBody(body, id) {
|
|
467
|
+
if (typeof body["userName"] !== "string" || body["userName"] === "") return;
|
|
468
|
+
const emails = Array.isArray(body["emails"]) ? body["emails"].flatMap((item) => {
|
|
469
|
+
if (!isRecord$1(item) || typeof item["value"] !== "string") return [];
|
|
470
|
+
return [compact({
|
|
471
|
+
value: item["value"],
|
|
472
|
+
primary: typeof item["primary"] === "boolean" ? item["primary"] : void 0,
|
|
473
|
+
type: typeof item["type"] === "string" ? item["type"] : void 0
|
|
474
|
+
})];
|
|
475
|
+
}) : void 0;
|
|
476
|
+
const active = typeof body["active"] === "boolean" ? body["active"] : typeof body["active"] === "string" ? body["active"].toLowerCase() !== "false" : true;
|
|
477
|
+
return compact({
|
|
478
|
+
id,
|
|
479
|
+
userName: body["userName"],
|
|
480
|
+
externalId: typeof body["externalId"] === "string" ? body["externalId"] : void 0,
|
|
481
|
+
active,
|
|
482
|
+
emails,
|
|
483
|
+
meta: {
|
|
484
|
+
created: "",
|
|
485
|
+
lastModified: ""
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function groupFromBody(body, id, fallbackRoles) {
|
|
490
|
+
if (typeof body["displayName"] !== "string" || body["displayName"] === "") return;
|
|
491
|
+
const members = Array.isArray(body["members"]) ? body["members"].flatMap((item) => {
|
|
492
|
+
if (!isRecord$1(item) || typeof item["value"] !== "string") return [];
|
|
493
|
+
return [{ value: item["value"] }];
|
|
494
|
+
}) : [];
|
|
495
|
+
return compact({
|
|
496
|
+
id,
|
|
497
|
+
displayName: body["displayName"],
|
|
498
|
+
externalId: typeof body["externalId"] === "string" ? body["externalId"] : void 0,
|
|
499
|
+
members,
|
|
500
|
+
roles: readRoles(body) ?? fallbackRoles,
|
|
501
|
+
meta: {
|
|
502
|
+
created: "",
|
|
503
|
+
lastModified: ""
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
//#endregion
|
|
508
|
+
//#region src/scim/emit.ts
|
|
509
|
+
function reportUnknownRoles(roles, options) {
|
|
510
|
+
if (roles === void 0 || options.assignable === void 0) return;
|
|
511
|
+
const allowed = new Set(options.assignable);
|
|
512
|
+
for (const name of roles) if (!allowed.has(name)) options.onUnknownRole?.(name);
|
|
513
|
+
}
|
|
514
|
+
function membershipEventsForGroup(input) {
|
|
515
|
+
const previousIds = new Set(input.previous?.members.map((member) => member.value) ?? []);
|
|
516
|
+
const nextIds = new Set(input.next?.members.map((member) => member.value) ?? []);
|
|
517
|
+
const roles = input.roles;
|
|
518
|
+
const events = [];
|
|
519
|
+
for (const id of nextIds) {
|
|
520
|
+
if (previousIds.has(id)) continue;
|
|
521
|
+
events.push(membershipEvent({
|
|
522
|
+
source: "scim",
|
|
523
|
+
operation: "added",
|
|
524
|
+
principal: { id },
|
|
525
|
+
scope: "tenant",
|
|
526
|
+
id: input.tenant,
|
|
527
|
+
via: `group:${input.groupId}`,
|
|
528
|
+
roles: {
|
|
529
|
+
added: roles,
|
|
530
|
+
removed: []
|
|
531
|
+
}
|
|
532
|
+
}));
|
|
533
|
+
}
|
|
534
|
+
for (const id of previousIds) {
|
|
535
|
+
if (nextIds.has(id)) continue;
|
|
536
|
+
events.push(membershipEvent({
|
|
537
|
+
source: "scim",
|
|
538
|
+
operation: "removed",
|
|
539
|
+
principal: { id },
|
|
540
|
+
scope: "tenant",
|
|
541
|
+
id: input.tenant,
|
|
542
|
+
via: `group:${input.groupId}`,
|
|
543
|
+
roles: {
|
|
544
|
+
added: [],
|
|
545
|
+
removed: roles
|
|
546
|
+
}
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
return events;
|
|
550
|
+
}
|
|
551
|
+
async function emitDirectory(options, event, userIds, extra = [], known = []) {
|
|
552
|
+
const kind = event.resource.type === "User" && event.active === false ? "session-revoked" : "changed";
|
|
553
|
+
const sink = options.sink ?? memorySink();
|
|
554
|
+
try {
|
|
555
|
+
await sink.write([event, ...extra]);
|
|
556
|
+
} catch {}
|
|
557
|
+
try {
|
|
558
|
+
await options.onChange?.({
|
|
559
|
+
tenant: event.tenant,
|
|
560
|
+
userIds,
|
|
561
|
+
kind
|
|
562
|
+
});
|
|
563
|
+
} catch {}
|
|
564
|
+
await publish(options, kind, event.tenant, userIds, known);
|
|
565
|
+
}
|
|
566
|
+
/** Every identifier a principal may carry for these users, deduplicated. */
|
|
567
|
+
async function principalIds(options, tenant, userIds, known) {
|
|
568
|
+
const ids = /* @__PURE__ */ new Set();
|
|
569
|
+
const users = await Promise.all(userIds.map(async (id) => {
|
|
570
|
+
const hit = known.find((user) => user.id === id);
|
|
571
|
+
if (hit !== void 0) return hit;
|
|
572
|
+
try {
|
|
573
|
+
return await options.store.getUser(tenant, id);
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
}));
|
|
578
|
+
for (const [index, user] of users.entries()) {
|
|
579
|
+
ids.add(userIds[index] ?? "");
|
|
580
|
+
if (user !== null) {
|
|
581
|
+
ids.add(user.userName);
|
|
582
|
+
if (user.externalId !== void 0) ids.add(user.externalId);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
ids.delete("");
|
|
586
|
+
return [...ids];
|
|
587
|
+
}
|
|
588
|
+
async function publish(options, kind, tenant, userIds, known) {
|
|
589
|
+
const feed = options.revocations;
|
|
590
|
+
if (feed === void 0 || userIds.length === 0) return;
|
|
591
|
+
try {
|
|
592
|
+
for (const principal of await principalIds(options, tenant, userIds, known)) await feed.revoke({
|
|
593
|
+
principal,
|
|
594
|
+
tenant,
|
|
595
|
+
kind
|
|
596
|
+
});
|
|
597
|
+
} catch {}
|
|
598
|
+
}
|
|
599
|
+
function directoryEvent(input) {
|
|
600
|
+
return compact({
|
|
601
|
+
type: "directory",
|
|
602
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
603
|
+
source: "scim",
|
|
604
|
+
operation: input.operation,
|
|
605
|
+
tenant: input.tenant,
|
|
606
|
+
resource: {
|
|
607
|
+
type: input.type,
|
|
608
|
+
id: input.id
|
|
609
|
+
},
|
|
610
|
+
credential: input.credential,
|
|
611
|
+
active: input.active
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region src/scim/group-roles.ts
|
|
616
|
+
function groupRolesFor(groupRoles, id) {
|
|
617
|
+
if (groupRoles === void 0 || typeof id !== "string" || !Object.hasOwn(groupRoles, id)) return;
|
|
618
|
+
return groupRoles[id];
|
|
619
|
+
}
|
|
620
|
+
//#endregion
|
|
621
|
+
//#region src/scim/patch.ts
|
|
622
|
+
const FILTER_PATH = /^(?<attribute>[A-Za-z0-9._]+)\[(?<field>[A-Za-z0-9._]+)\s+eq\s+"(?<value>[^"]+)"\](?<rest>\.[A-Za-z0-9._]+)?$/u;
|
|
623
|
+
function isRecord(value) {
|
|
624
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
625
|
+
}
|
|
626
|
+
function coerceActive(value) {
|
|
627
|
+
if (typeof value === "boolean") return value;
|
|
628
|
+
if (typeof value === "string") {
|
|
629
|
+
const lower = value.toLowerCase();
|
|
630
|
+
if (lower === "true") return true;
|
|
631
|
+
if (lower === "false") return false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
function rolesPath(path) {
|
|
635
|
+
return path === "roles" || path === `urn:permdock:scim:schemas:extension:roles:1.0:roles` || path === `urn:permdock:scim:schemas:extension:roles:1.0.roles`;
|
|
636
|
+
}
|
|
637
|
+
function opName(value) {
|
|
638
|
+
if (typeof value !== "string") return;
|
|
639
|
+
const lower = value.toLowerCase();
|
|
640
|
+
if (lower === "add" || lower === "replace" || lower === "remove") return lower;
|
|
641
|
+
}
|
|
642
|
+
function normalizePatchOps(ops) {
|
|
643
|
+
const next = [];
|
|
644
|
+
for (const raw of ops) {
|
|
645
|
+
if (!isRecord(raw)) return;
|
|
646
|
+
const op = opName(raw["op"]);
|
|
647
|
+
if (op === void 0) return;
|
|
648
|
+
const path = typeof raw["path"] === "string" ? raw["path"] : void 0;
|
|
649
|
+
if (path === void 0) {
|
|
650
|
+
if (!isRecord(raw["value"])) {
|
|
651
|
+
next.push({
|
|
652
|
+
op,
|
|
653
|
+
value: raw["value"]
|
|
654
|
+
});
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
for (const [key, value] of Object.entries(raw["value"])) {
|
|
658
|
+
if (key === "urn:permdock:scim:schemas:extension:roles:1.0" && isRecord(value)) {
|
|
659
|
+
next.push({
|
|
660
|
+
op,
|
|
661
|
+
path: "roles",
|
|
662
|
+
value: value["roles"]
|
|
663
|
+
});
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (key === "active") {
|
|
667
|
+
next.push({
|
|
668
|
+
op,
|
|
669
|
+
path: "active",
|
|
670
|
+
value: coerceActive(value) ?? value
|
|
671
|
+
});
|
|
672
|
+
continue;
|
|
673
|
+
}
|
|
674
|
+
next.push({
|
|
675
|
+
op,
|
|
676
|
+
path: key,
|
|
677
|
+
value
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
if (rolesPath(path)) {
|
|
683
|
+
const roles = isRecord(raw["value"]) ? raw["value"]["roles"] : raw["value"];
|
|
684
|
+
next.push({
|
|
685
|
+
op,
|
|
686
|
+
path: "roles",
|
|
687
|
+
value: roles
|
|
688
|
+
});
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
const filtered = FILTER_PATH.exec(path);
|
|
692
|
+
if (filtered?.groups !== void 0) {
|
|
693
|
+
const attribute = filtered.groups["attribute"];
|
|
694
|
+
const field = filtered.groups["field"];
|
|
695
|
+
const value = filtered.groups["value"];
|
|
696
|
+
if (attribute === void 0 || field === void 0 || value === void 0) return;
|
|
697
|
+
if (op === "remove") {
|
|
698
|
+
next.push({
|
|
699
|
+
op,
|
|
700
|
+
path: attribute,
|
|
701
|
+
value: [{ [field]: value }]
|
|
702
|
+
});
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
next.push({
|
|
706
|
+
op,
|
|
707
|
+
path: attribute,
|
|
708
|
+
value: raw["value"] ?? [{ [field]: value }]
|
|
709
|
+
});
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (path === "active") {
|
|
713
|
+
next.push({
|
|
714
|
+
op,
|
|
715
|
+
path,
|
|
716
|
+
value: coerceActive(raw["value"]) ?? raw["value"]
|
|
717
|
+
});
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
next.push({
|
|
721
|
+
op,
|
|
722
|
+
path,
|
|
723
|
+
value: raw["value"]
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
return next;
|
|
727
|
+
}
|
|
728
|
+
function readPatchOperations(body) {
|
|
729
|
+
if (!isRecord(body)) return;
|
|
730
|
+
const operations = body["Operations"] ?? body["operations"];
|
|
731
|
+
if (!isReadonlyArray(operations)) return;
|
|
732
|
+
return operations;
|
|
733
|
+
}
|
|
734
|
+
//#endregion
|
|
735
|
+
//#region src/scim/render.ts
|
|
736
|
+
function scimResponse(status, body, extra) {
|
|
737
|
+
return new Response(JSON.stringify(body), {
|
|
738
|
+
status,
|
|
739
|
+
headers: {
|
|
740
|
+
"content-type": SCIM_CONTENT_TYPE,
|
|
741
|
+
...extra
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function scimError(status, scimType, detail) {
|
|
746
|
+
return scimResponse(status, {
|
|
747
|
+
schemas: [ERROR_SCHEMA],
|
|
748
|
+
status: String(status),
|
|
749
|
+
scimType,
|
|
750
|
+
detail
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function unauthorized() {
|
|
754
|
+
return new Response(null, {
|
|
755
|
+
status: 401,
|
|
756
|
+
headers: { "www-authenticate": "Bearer" }
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
function forbidden() {
|
|
760
|
+
return new Response(null, {
|
|
761
|
+
status: 403,
|
|
762
|
+
headers: { "www-authenticate": "Bearer" }
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
function mapStoreError(error) {
|
|
766
|
+
if (isDirectoryUniquenessError(error)) return scimError(409, "uniqueness", error.message);
|
|
767
|
+
if (isDirectoryNotFoundError(error)) return scimError(404, "invalidValue", "resource not found");
|
|
768
|
+
return scimError(500, "invalidValue", "store failed");
|
|
769
|
+
}
|
|
770
|
+
function listBody(result, render) {
|
|
771
|
+
return compact({
|
|
772
|
+
schemas: [LIST_SCHEMA],
|
|
773
|
+
totalResults: result.totalResults,
|
|
774
|
+
startIndex: result.startIndex,
|
|
775
|
+
itemsPerPage: result.itemsPerPage,
|
|
776
|
+
nextCursor: result.nextCursor,
|
|
777
|
+
Resources: result.Resources.map(render)
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
function renderUser(user, location) {
|
|
781
|
+
return compact({
|
|
782
|
+
schemas: [USER_SCHEMA],
|
|
783
|
+
id: user.id,
|
|
784
|
+
externalId: user.externalId,
|
|
785
|
+
userName: user.userName,
|
|
786
|
+
active: user.active,
|
|
787
|
+
emails: user.emails,
|
|
788
|
+
meta: {
|
|
789
|
+
resourceType: "User",
|
|
790
|
+
created: user.meta.created,
|
|
791
|
+
lastModified: user.meta.lastModified,
|
|
792
|
+
location
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
function renderGroup(group, location) {
|
|
797
|
+
const extension = group.roles === void 0 ? void 0 : { [ROLES_EXTENSION]: { roles: group.roles } };
|
|
798
|
+
return compact({
|
|
799
|
+
schemas: group.roles === void 0 ? [GROUP_SCHEMA] : [GROUP_SCHEMA, ROLES_EXTENSION],
|
|
800
|
+
id: group.id,
|
|
801
|
+
externalId: group.externalId,
|
|
802
|
+
displayName: group.displayName,
|
|
803
|
+
members: group.members,
|
|
804
|
+
...extension,
|
|
805
|
+
meta: {
|
|
806
|
+
resourceType: "Group",
|
|
807
|
+
created: group.meta.created,
|
|
808
|
+
lastModified: group.meta.lastModified,
|
|
809
|
+
location
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/scim/route.ts
|
|
815
|
+
const RESOURCES = /* @__PURE__ */ new Set([
|
|
816
|
+
"Users",
|
|
817
|
+
"Groups",
|
|
818
|
+
"ServiceProviderConfig",
|
|
819
|
+
"ResourceTypes",
|
|
820
|
+
"Schemas"
|
|
821
|
+
]);
|
|
822
|
+
function tenantFromPath(request) {
|
|
823
|
+
const parts = new URL(request.url).pathname.split("/").filter(Boolean);
|
|
824
|
+
const index = parts.findIndex((part) => RESOURCES.has(part));
|
|
825
|
+
if (index <= 0) return "";
|
|
826
|
+
const prev = parts[index - 1] ?? "";
|
|
827
|
+
if (prev === "v2" || prev === "scim") return "";
|
|
828
|
+
return prev;
|
|
829
|
+
}
|
|
830
|
+
function isKind(part) {
|
|
831
|
+
return RESOURCES.has(part);
|
|
832
|
+
}
|
|
833
|
+
function parseRoute(url) {
|
|
834
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
835
|
+
const kind = parts.find(isKind);
|
|
836
|
+
if (kind === void 0) return;
|
|
837
|
+
const id = parts[parts.indexOf(kind) + 1];
|
|
838
|
+
switch (kind) {
|
|
839
|
+
case "Users": return compact({
|
|
840
|
+
kind: "Users",
|
|
841
|
+
id
|
|
842
|
+
});
|
|
843
|
+
case "Groups": return compact({
|
|
844
|
+
kind: "Groups",
|
|
845
|
+
id
|
|
846
|
+
});
|
|
847
|
+
case "ServiceProviderConfig": return { kind: "ServiceProviderConfig" };
|
|
848
|
+
case "ResourceTypes": return compact({
|
|
849
|
+
kind: "ResourceTypes",
|
|
850
|
+
id
|
|
851
|
+
});
|
|
852
|
+
case "Schemas": return compact({
|
|
853
|
+
kind: "Schemas",
|
|
854
|
+
id
|
|
855
|
+
});
|
|
856
|
+
default: return kind;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function prefixOf(request) {
|
|
860
|
+
const url = new URL(request.url);
|
|
861
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
862
|
+
const index = parts.findIndex((part) => RESOURCES.has(part));
|
|
863
|
+
return {
|
|
864
|
+
origin: url.origin,
|
|
865
|
+
prefix: index === -1 ? parts : parts.slice(0, index)
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function locationOf(request, kind, id) {
|
|
869
|
+
const { origin, prefix } = prefixOf(request);
|
|
870
|
+
return `${origin}/${(id === void 0 ? [...prefix, kind] : [
|
|
871
|
+
...prefix,
|
|
872
|
+
kind,
|
|
873
|
+
id
|
|
874
|
+
]).join("/")}`;
|
|
875
|
+
}
|
|
876
|
+
function pageFrom(url) {
|
|
877
|
+
const startIndex = url.searchParams.get("startIndex");
|
|
878
|
+
const count = url.searchParams.get("count");
|
|
879
|
+
const cursor = url.searchParams.get("cursor");
|
|
880
|
+
return compact({
|
|
881
|
+
startIndex: startIndex === null ? void 0 : Math.trunc(Number(startIndex)),
|
|
882
|
+
count: count === null ? void 0 : Math.trunc(Number(count)),
|
|
883
|
+
cursor: cursor ?? void 0
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
887
|
+
//#region src/scim/handler.ts
|
|
888
|
+
function scimHandler(options) {
|
|
889
|
+
if (options.token === void 0 && options.verifier === void 0) throw new Error("scimHandler requires token or verifier");
|
|
890
|
+
if (options.verifier !== void 0 && options.audience === void 0) throw new Error("scimHandler requires audience with a verifier");
|
|
891
|
+
return async (request) => {
|
|
892
|
+
const url = new URL(request.url);
|
|
893
|
+
const route = parseRoute(url);
|
|
894
|
+
if (route === void 0) return scimError(404, "invalidValue", "unknown route");
|
|
895
|
+
let tenant;
|
|
896
|
+
try {
|
|
897
|
+
tenant = typeof options.tenant === "string" ? options.tenant : await options.tenant(request);
|
|
898
|
+
} catch {
|
|
899
|
+
return forbidden();
|
|
900
|
+
}
|
|
901
|
+
if (tenant === "") return forbidden();
|
|
902
|
+
const auth = await authenticateScim(compact({
|
|
903
|
+
request,
|
|
904
|
+
tenant,
|
|
905
|
+
audience: options.audience,
|
|
906
|
+
token: options.token,
|
|
907
|
+
verifier: options.verifier
|
|
908
|
+
}));
|
|
909
|
+
if (!auth.ok) return auth.status === 403 ? forbidden() : unauthorized();
|
|
910
|
+
try {
|
|
911
|
+
if (route.kind === "ServiceProviderConfig" || route.kind === "ResourceTypes" || route.kind === "Schemas") {
|
|
912
|
+
if (url.searchParams.has("filter")) return scimError(403, "invalidFilter", "discovery endpoints do not filter");
|
|
913
|
+
return discoveryResponse(request, route);
|
|
914
|
+
}
|
|
915
|
+
if (request.method === "GET" && route.id === void 0) {
|
|
916
|
+
const rawFilter = url.searchParams.get("filter");
|
|
917
|
+
const filter = rawFilter === null || rawFilter === "" ? void 0 : parseScimFilter(rawFilter);
|
|
918
|
+
if (rawFilter !== null && rawFilter !== "" && filter === void 0) return scimError(400, "invalidFilter", "unsupported filter");
|
|
919
|
+
if (filter !== void 0 && !filterSupported(filter)) return scimError(400, "invalidFilter", "unsupported filter");
|
|
920
|
+
const page = pageFrom(url);
|
|
921
|
+
if (route.kind === "Users") return scimResponse(200, listBody(await options.store.findUsers(tenant, filter, page), (user) => renderUser(user, locationOf(request, "Users", user.id))));
|
|
922
|
+
return scimResponse(200, listBody(await options.store.findGroups(tenant, filter, page), (group) => renderGroup(group, locationOf(request, "Groups", group.id))));
|
|
923
|
+
}
|
|
924
|
+
if (request.method === "GET" && route.id !== void 0) {
|
|
925
|
+
if (route.kind === "Users") {
|
|
926
|
+
const user = await options.store.getUser(tenant, route.id);
|
|
927
|
+
if (user === null) return scimError(404, "invalidValue", "resource not found");
|
|
928
|
+
return scimResponse(200, renderUser(user, locationOf(request, "Users", user.id)));
|
|
929
|
+
}
|
|
930
|
+
const group = await options.store.getGroup(tenant, route.id);
|
|
931
|
+
if (group === null) return scimError(404, "invalidValue", "resource not found");
|
|
932
|
+
return scimResponse(200, renderGroup(group, locationOf(request, "Groups", group.id)));
|
|
933
|
+
}
|
|
934
|
+
if (request.method === "DELETE" && route.id !== void 0) {
|
|
935
|
+
if (route.kind === "Users") {
|
|
936
|
+
const existing = await options.store.getUser(tenant, route.id);
|
|
937
|
+
await options.store.deleteUser(tenant, route.id);
|
|
938
|
+
await emitDirectory(options, directoryEvent({
|
|
939
|
+
operation: "delete",
|
|
940
|
+
tenant,
|
|
941
|
+
type: "User",
|
|
942
|
+
id: route.id,
|
|
943
|
+
credential: auth.credential,
|
|
944
|
+
active: false
|
|
945
|
+
}), existing === null ? [] : [existing.id], [], existing === null ? [] : [existing]);
|
|
946
|
+
return new Response(null, { status: 204 });
|
|
947
|
+
}
|
|
948
|
+
const existing = await options.store.getGroup(tenant, route.id);
|
|
949
|
+
await options.store.deleteGroup(tenant, route.id);
|
|
950
|
+
await emitDirectory(options, directoryEvent({
|
|
951
|
+
operation: "delete",
|
|
952
|
+
tenant,
|
|
953
|
+
type: "Group",
|
|
954
|
+
id: route.id,
|
|
955
|
+
credential: auth.credential
|
|
956
|
+
}), existing?.members.map((member) => member.value) ?? [], membershipEventsForGroup({
|
|
957
|
+
tenant,
|
|
958
|
+
groupId: route.id,
|
|
959
|
+
roles: existing?.roles ?? groupRolesFor(options.groupRoles, route.id) ?? [],
|
|
960
|
+
previous: existing,
|
|
961
|
+
next: null
|
|
962
|
+
}));
|
|
963
|
+
return new Response(null, { status: 204 });
|
|
964
|
+
}
|
|
965
|
+
const body = await readJson(request);
|
|
966
|
+
if (!isRecord$1(body)) return scimError(400, "invalidSyntax", "malformed body");
|
|
967
|
+
if (request.method === "POST" && route.id === void 0) {
|
|
968
|
+
if (route.kind === "Users") {
|
|
969
|
+
if (!schemasOk(body, "urn:ietf:params:scim:schemas:core:2.0:User")) return scimError(400, "invalidSyntax", "unsupported schema");
|
|
970
|
+
const parsed = userFromBody(body, "");
|
|
971
|
+
if (parsed === void 0) return scimError(400, "invalidValue", "userName is required");
|
|
972
|
+
const stored = await options.store.putUser(tenant, parsed);
|
|
973
|
+
const location = locationOf(request, "Users", stored.id);
|
|
974
|
+
await emitDirectory(options, directoryEvent({
|
|
975
|
+
operation: "create",
|
|
976
|
+
tenant,
|
|
977
|
+
type: "User",
|
|
978
|
+
id: stored.id,
|
|
979
|
+
credential: auth.credential,
|
|
980
|
+
active: stored.active
|
|
981
|
+
}), [stored.id]);
|
|
982
|
+
return scimResponse(201, renderUser(stored, location), { location });
|
|
983
|
+
}
|
|
984
|
+
if (!schemasOk(body, "urn:ietf:params:scim:schemas:core:2.0:Group")) return scimError(400, "invalidSyntax", "unsupported schema");
|
|
985
|
+
const parsed = groupFromBody(body, "", groupRolesFor(options.groupRoles, body["id"]));
|
|
986
|
+
if (parsed === void 0) return scimError(400, "invalidValue", "displayName is required");
|
|
987
|
+
reportUnknownRoles(parsed.roles, options);
|
|
988
|
+
const stored = await options.store.putGroup(tenant, parsed);
|
|
989
|
+
const location = locationOf(request, "Groups", stored.id);
|
|
990
|
+
await emitDirectory(options, directoryEvent({
|
|
991
|
+
operation: "create",
|
|
992
|
+
tenant,
|
|
993
|
+
type: "Group",
|
|
994
|
+
id: stored.id,
|
|
995
|
+
credential: auth.credential
|
|
996
|
+
}), stored.members.map((member) => member.value), membershipEventsForGroup({
|
|
997
|
+
tenant,
|
|
998
|
+
groupId: stored.id,
|
|
999
|
+
roles: stored.roles ?? groupRolesFor(options.groupRoles, stored.id) ?? [],
|
|
1000
|
+
previous: null,
|
|
1001
|
+
next: stored
|
|
1002
|
+
}));
|
|
1003
|
+
return scimResponse(201, renderGroup(stored, location), { location });
|
|
1004
|
+
}
|
|
1005
|
+
if (request.method === "PUT" && route.id !== void 0) {
|
|
1006
|
+
if (route.kind === "Users") {
|
|
1007
|
+
if (!schemasOk(body, "urn:ietf:params:scim:schemas:core:2.0:User")) return scimError(400, "invalidSyntax", "unsupported schema");
|
|
1008
|
+
if (await options.store.getUser(tenant, route.id) === null) return scimError(404, "invalidValue", "resource not found");
|
|
1009
|
+
const parsed = userFromBody(body, route.id);
|
|
1010
|
+
if (parsed === void 0) return scimError(400, "invalidValue", "userName is required");
|
|
1011
|
+
const stored = await options.store.putUser(tenant, parsed);
|
|
1012
|
+
const location = locationOf(request, "Users", stored.id);
|
|
1013
|
+
await emitDirectory(options, directoryEvent({
|
|
1014
|
+
operation: "replace",
|
|
1015
|
+
tenant,
|
|
1016
|
+
type: "User",
|
|
1017
|
+
id: stored.id,
|
|
1018
|
+
credential: auth.credential,
|
|
1019
|
+
active: stored.active
|
|
1020
|
+
}), [stored.id]);
|
|
1021
|
+
return scimResponse(200, renderUser(stored, location), { location });
|
|
1022
|
+
}
|
|
1023
|
+
if (!schemasOk(body, "urn:ietf:params:scim:schemas:core:2.0:Group")) return scimError(400, "invalidSyntax", "unsupported schema");
|
|
1024
|
+
const existing = await options.store.getGroup(tenant, route.id);
|
|
1025
|
+
if (existing === null) return scimError(404, "invalidValue", "resource not found");
|
|
1026
|
+
const parsed = groupFromBody(body, route.id, groupRolesFor(options.groupRoles, route.id));
|
|
1027
|
+
if (parsed === void 0) return scimError(400, "invalidValue", "displayName is required");
|
|
1028
|
+
reportUnknownRoles(parsed.roles, options);
|
|
1029
|
+
const stored = await options.store.putGroup(tenant, parsed);
|
|
1030
|
+
const location = locationOf(request, "Groups", stored.id);
|
|
1031
|
+
await emitDirectory(options, directoryEvent({
|
|
1032
|
+
operation: "replace",
|
|
1033
|
+
tenant,
|
|
1034
|
+
type: "Group",
|
|
1035
|
+
id: stored.id,
|
|
1036
|
+
credential: auth.credential
|
|
1037
|
+
}), stored.members.map((member) => member.value), membershipEventsForGroup({
|
|
1038
|
+
tenant,
|
|
1039
|
+
groupId: stored.id,
|
|
1040
|
+
roles: stored.roles ?? existing.roles ?? [],
|
|
1041
|
+
previous: existing,
|
|
1042
|
+
next: stored
|
|
1043
|
+
}));
|
|
1044
|
+
return scimResponse(200, renderGroup(stored, location), { location });
|
|
1045
|
+
}
|
|
1046
|
+
if (request.method === "PATCH" && route.id !== void 0) {
|
|
1047
|
+
const operations = readPatchOperations(body);
|
|
1048
|
+
if (operations === void 0) return scimError(400, "invalidSyntax", "Operations required");
|
|
1049
|
+
const normalized = normalizePatchOps(operations);
|
|
1050
|
+
if (normalized === void 0) return scimError(400, "invalidSyntax", "invalid patch");
|
|
1051
|
+
if (route.kind === "Users") {
|
|
1052
|
+
const stored = await options.store.patchUser(tenant, route.id, normalized);
|
|
1053
|
+
const location = locationOf(request, "Users", stored.id);
|
|
1054
|
+
await emitDirectory(options, directoryEvent({
|
|
1055
|
+
operation: "patch",
|
|
1056
|
+
tenant,
|
|
1057
|
+
type: "User",
|
|
1058
|
+
id: stored.id,
|
|
1059
|
+
credential: auth.credential,
|
|
1060
|
+
active: stored.active
|
|
1061
|
+
}), [stored.id]);
|
|
1062
|
+
return scimResponse(200, renderUser(stored, location), { location });
|
|
1063
|
+
}
|
|
1064
|
+
const existing = await options.store.getGroup(tenant, route.id);
|
|
1065
|
+
const stored = await options.store.patchGroup(tenant, route.id, normalized);
|
|
1066
|
+
reportUnknownRoles(stored.roles, options);
|
|
1067
|
+
const location = locationOf(request, "Groups", stored.id);
|
|
1068
|
+
await emitDirectory(options, directoryEvent({
|
|
1069
|
+
operation: "patch",
|
|
1070
|
+
tenant,
|
|
1071
|
+
type: "Group",
|
|
1072
|
+
id: stored.id,
|
|
1073
|
+
credential: auth.credential
|
|
1074
|
+
}), stored.members.map((member) => member.value), membershipEventsForGroup({
|
|
1075
|
+
tenant,
|
|
1076
|
+
groupId: stored.id,
|
|
1077
|
+
roles: stored.roles ?? existing?.roles ?? [],
|
|
1078
|
+
previous: existing,
|
|
1079
|
+
next: stored
|
|
1080
|
+
}));
|
|
1081
|
+
return scimResponse(200, renderGroup(stored, location), { location });
|
|
1082
|
+
}
|
|
1083
|
+
return scimError(405, "invalidValue", "method not allowed");
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (error instanceof SyntaxError) return scimError(400, "invalidSyntax", "malformed body");
|
|
1086
|
+
return mapStoreError(error);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
function discoveryResponse(request, route) {
|
|
1091
|
+
if (route.kind === "ServiceProviderConfig") return scimResponse(200, {
|
|
1092
|
+
...serviceProviderConfig(),
|
|
1093
|
+
meta: {
|
|
1094
|
+
resourceType: "ServiceProviderConfig",
|
|
1095
|
+
location: locationOf(request, route.kind)
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
const resourceType = route.kind === "Schemas" ? "Schema" : "ResourceType";
|
|
1099
|
+
const all = (route.kind === "Schemas" ? schemas() : resourceTypes()).map((item) => Object.assign({}, item, { meta: {
|
|
1100
|
+
resourceType,
|
|
1101
|
+
location: locationOf(request, route.kind, String(item["id"]))
|
|
1102
|
+
} }));
|
|
1103
|
+
if (route.id !== void 0) {
|
|
1104
|
+
const one = all.find((item) => item["id"] === route.id);
|
|
1105
|
+
return one === void 0 ? scimError(404, "invalidValue", `${resourceType} not found`) : scimResponse(200, one);
|
|
1106
|
+
}
|
|
1107
|
+
return scimResponse(200, listBody({
|
|
1108
|
+
Resources: all,
|
|
1109
|
+
totalResults: all.length,
|
|
1110
|
+
startIndex: 1,
|
|
1111
|
+
itemsPerPage: all.length
|
|
1112
|
+
}, (item) => item));
|
|
1113
|
+
}
|
|
1114
|
+
//#endregion
|
|
1115
|
+
//#region src/scim/source.ts
|
|
1116
|
+
function allowedRoles(roles, assignable, onUnknownRole) {
|
|
1117
|
+
if (roles === void 0) return [];
|
|
1118
|
+
if (assignable === void 0) return roles;
|
|
1119
|
+
const allowed = new Set(assignable);
|
|
1120
|
+
const kept = [];
|
|
1121
|
+
for (const name of roles) {
|
|
1122
|
+
if (allowed.has(name)) {
|
|
1123
|
+
kept.push(name);
|
|
1124
|
+
continue;
|
|
1125
|
+
}
|
|
1126
|
+
onUnknownRole?.(name);
|
|
1127
|
+
}
|
|
1128
|
+
return kept;
|
|
1129
|
+
}
|
|
1130
|
+
async function lookupUser(store, tenant, attribute, principalId) {
|
|
1131
|
+
return (await store.findUsers(tenant, {
|
|
1132
|
+
op: "eq",
|
|
1133
|
+
attribute,
|
|
1134
|
+
value: principalId
|
|
1135
|
+
}, {
|
|
1136
|
+
startIndex: 1,
|
|
1137
|
+
count: 1
|
|
1138
|
+
})).Resources[0] ?? null;
|
|
1139
|
+
}
|
|
1140
|
+
async function findUser(store, tenant, principalId, match) {
|
|
1141
|
+
const mode = match ?? "either";
|
|
1142
|
+
if (mode === "externalId" || mode === "either") {
|
|
1143
|
+
const user = await lookupUser(store, tenant, "externalId", principalId);
|
|
1144
|
+
if (user !== null) return user;
|
|
1145
|
+
}
|
|
1146
|
+
if (mode === "userName" || mode === "either") return lookupUser(store, tenant, "userName", principalId);
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
function directoryMembershipSource(store, options = {}) {
|
|
1150
|
+
return { async membershipsFor(principal, query) {
|
|
1151
|
+
const tenant = query.tenant;
|
|
1152
|
+
if (tenant === void 0 || tenant === "") return [];
|
|
1153
|
+
let user;
|
|
1154
|
+
try {
|
|
1155
|
+
user = await findUser(store, tenant, principal.id, options.match);
|
|
1156
|
+
} catch {
|
|
1157
|
+
return [];
|
|
1158
|
+
}
|
|
1159
|
+
if (user === null || !user.active) return [];
|
|
1160
|
+
let groups;
|
|
1161
|
+
try {
|
|
1162
|
+
groups = [...await store.groupsFor(tenant, user.id)];
|
|
1163
|
+
} catch {
|
|
1164
|
+
return [];
|
|
1165
|
+
}
|
|
1166
|
+
const memberships = [];
|
|
1167
|
+
for (const group of groups) {
|
|
1168
|
+
const mapped = group.roles ?? groupRolesFor(options.groupRoles, group.id);
|
|
1169
|
+
memberships.push(compact({
|
|
1170
|
+
tenant,
|
|
1171
|
+
roles: allowedRoles(mapped, options.assignable, options.onUnknownRole),
|
|
1172
|
+
via: `group:${group.id}`,
|
|
1173
|
+
managedBy: "idp"
|
|
1174
|
+
}));
|
|
1175
|
+
}
|
|
1176
|
+
return memberships;
|
|
1177
|
+
} };
|
|
1178
|
+
}
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#region src/scim/store.ts
|
|
1181
|
+
const DEFAULT_COUNT = 100;
|
|
1182
|
+
function now() {
|
|
1183
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1184
|
+
}
|
|
1185
|
+
function createdAt(at, ...candidates) {
|
|
1186
|
+
return candidates.find((value) => value !== void 0 && value !== "") ?? at;
|
|
1187
|
+
}
|
|
1188
|
+
function randomId(prefix) {
|
|
1189
|
+
const bytes = /* @__PURE__ */ new Uint8Array(8);
|
|
1190
|
+
crypto.getRandomValues(bytes);
|
|
1191
|
+
let hex = "";
|
|
1192
|
+
for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
|
|
1193
|
+
return `${prefix}${hex}`;
|
|
1194
|
+
}
|
|
1195
|
+
function directoryKey(tenant, id) {
|
|
1196
|
+
return `${tenant}\0${id}`;
|
|
1197
|
+
}
|
|
1198
|
+
function asError(error) {
|
|
1199
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
1200
|
+
}
|
|
1201
|
+
function pageOf(items, page) {
|
|
1202
|
+
const count = page.count ?? DEFAULT_COUNT;
|
|
1203
|
+
let start = 0;
|
|
1204
|
+
if (page.cursor !== void 0 && page.cursor !== "") {
|
|
1205
|
+
const decoded = Math.trunc(Number(page.cursor));
|
|
1206
|
+
start = Number.isFinite(decoded) && decoded > 0 ? decoded : 0;
|
|
1207
|
+
} else if (page.startIndex !== void 0 && page.startIndex > 0) start = page.startIndex - 1;
|
|
1208
|
+
const slice = items.slice(start, start + count);
|
|
1209
|
+
const nextIndex = start + slice.length;
|
|
1210
|
+
return compact({
|
|
1211
|
+
Resources: slice,
|
|
1212
|
+
totalResults: items.length,
|
|
1213
|
+
startIndex: start + 1,
|
|
1214
|
+
itemsPerPage: slice.length,
|
|
1215
|
+
nextCursor: nextIndex < items.length ? String(nextIndex) : void 0
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
function emailsOf(value) {
|
|
1219
|
+
if (!Array.isArray(value)) return;
|
|
1220
|
+
return value.flatMap((item) => {
|
|
1221
|
+
if (item === null || typeof item !== "object") return [];
|
|
1222
|
+
const record = item;
|
|
1223
|
+
if (typeof record.value !== "string") return [];
|
|
1224
|
+
return [compact({
|
|
1225
|
+
value: record.value,
|
|
1226
|
+
primary: typeof record.primary === "boolean" ? record.primary : void 0,
|
|
1227
|
+
type: typeof record.type === "string" ? record.type : void 0
|
|
1228
|
+
})];
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
function membersOf(value) {
|
|
1232
|
+
if (!Array.isArray(value)) return [];
|
|
1233
|
+
return value.flatMap((item) => {
|
|
1234
|
+
if (item === null || typeof item !== "object") return [];
|
|
1235
|
+
const record = item;
|
|
1236
|
+
return typeof record.value === "string" ? [{ value: record.value }] : [];
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
function rolesOf(value) {
|
|
1240
|
+
if (!Array.isArray(value)) return;
|
|
1241
|
+
return value.filter((item) => typeof item === "string");
|
|
1242
|
+
}
|
|
1243
|
+
function memberValues(value) {
|
|
1244
|
+
return membersOf(value).map((member) => member.value);
|
|
1245
|
+
}
|
|
1246
|
+
function applyUserOp(user, op) {
|
|
1247
|
+
const path = op.path ?? "";
|
|
1248
|
+
if (path === "active" && typeof op.value === "boolean") return {
|
|
1249
|
+
...user,
|
|
1250
|
+
active: op.value
|
|
1251
|
+
};
|
|
1252
|
+
if (path === "userName" && typeof op.value === "string") return {
|
|
1253
|
+
...user,
|
|
1254
|
+
userName: op.value
|
|
1255
|
+
};
|
|
1256
|
+
if (path === "externalId") return compact({
|
|
1257
|
+
...user,
|
|
1258
|
+
externalId: typeof op.value === "string" ? op.value : void 0
|
|
1259
|
+
});
|
|
1260
|
+
if (path === "emails") return compact({
|
|
1261
|
+
...user,
|
|
1262
|
+
emails: emailsOf(op.value)
|
|
1263
|
+
});
|
|
1264
|
+
return user;
|
|
1265
|
+
}
|
|
1266
|
+
function applyGroupOp(group, op) {
|
|
1267
|
+
const path = op.path ?? "";
|
|
1268
|
+
if (path === "displayName" && typeof op.value === "string") return {
|
|
1269
|
+
...group,
|
|
1270
|
+
displayName: op.value
|
|
1271
|
+
};
|
|
1272
|
+
if (path === "externalId") return compact({
|
|
1273
|
+
...group,
|
|
1274
|
+
externalId: typeof op.value === "string" ? op.value : void 0
|
|
1275
|
+
});
|
|
1276
|
+
if (path === "roles") return compact({
|
|
1277
|
+
...group,
|
|
1278
|
+
roles: rolesOf(op.value)
|
|
1279
|
+
});
|
|
1280
|
+
if (path !== "members") return group;
|
|
1281
|
+
const incoming = memberValues(op.value);
|
|
1282
|
+
const current = group.members.map((member) => member.value);
|
|
1283
|
+
if (op.op === "replace") return {
|
|
1284
|
+
...group,
|
|
1285
|
+
members: incoming.map((value) => ({ value }))
|
|
1286
|
+
};
|
|
1287
|
+
if (op.op === "add") {
|
|
1288
|
+
const next = [...current];
|
|
1289
|
+
for (const value of incoming) if (!next.includes(value)) next.push(value);
|
|
1290
|
+
return {
|
|
1291
|
+
...group,
|
|
1292
|
+
members: next.map((value) => ({ value }))
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
...group,
|
|
1297
|
+
members: current.filter((value) => !incoming.includes(value)).map((value) => ({ value }))
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function stampUser(user, created) {
|
|
1301
|
+
const at = now();
|
|
1302
|
+
return freezeDeep({
|
|
1303
|
+
...user,
|
|
1304
|
+
meta: compact({
|
|
1305
|
+
created: createdAt(at, created, user.meta.created),
|
|
1306
|
+
lastModified: at,
|
|
1307
|
+
resourceType: "User",
|
|
1308
|
+
location: user.meta.location
|
|
1309
|
+
})
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
function stampGroup(group, created) {
|
|
1313
|
+
const at = now();
|
|
1314
|
+
return freezeDeep({
|
|
1315
|
+
...group,
|
|
1316
|
+
meta: compact({
|
|
1317
|
+
created: createdAt(at, created, group.meta.created),
|
|
1318
|
+
lastModified: at,
|
|
1319
|
+
resourceType: "Group",
|
|
1320
|
+
location: group.meta.location
|
|
1321
|
+
})
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
function memoryDirectoryStore() {
|
|
1325
|
+
const users = /* @__PURE__ */ new Map();
|
|
1326
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1327
|
+
function usersIn(tenant) {
|
|
1328
|
+
const found = [];
|
|
1329
|
+
for (const [key, user] of users) if (key.startsWith(`${tenant}\0`)) found.push(user);
|
|
1330
|
+
return found;
|
|
1331
|
+
}
|
|
1332
|
+
function groupsIn(tenant) {
|
|
1333
|
+
const found = [];
|
|
1334
|
+
for (const [key, group] of groups) if (key.startsWith(`${tenant}\0`)) found.push(group);
|
|
1335
|
+
return found;
|
|
1336
|
+
}
|
|
1337
|
+
function assertUserUnique(tenant, user, ignoreId) {
|
|
1338
|
+
for (const existing of usersIn(tenant)) {
|
|
1339
|
+
if (existing.id === ignoreId) continue;
|
|
1340
|
+
if (existing.userName === user.userName) throw new DirectoryUniquenessError("userName");
|
|
1341
|
+
if (user.externalId !== void 0 && existing.externalId === user.externalId) throw new DirectoryUniquenessError("externalId");
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function assertGroupUnique(tenant, group, ignoreId) {
|
|
1345
|
+
for (const existing of groupsIn(tenant)) {
|
|
1346
|
+
if (existing.id === ignoreId) continue;
|
|
1347
|
+
if (group.externalId !== void 0 && existing.externalId === group.externalId) throw new DirectoryUniquenessError("externalId");
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return {
|
|
1351
|
+
getUser(tenant, id) {
|
|
1352
|
+
return Promise.resolve(users.get(directoryKey(tenant, id)) ?? null);
|
|
1353
|
+
},
|
|
1354
|
+
findUsers(tenant, filter, page) {
|
|
1355
|
+
const matched = usersIn(tenant).filter((user) => matchFilter(user, filter));
|
|
1356
|
+
return Promise.resolve(pageOf(matched, page));
|
|
1357
|
+
},
|
|
1358
|
+
putUser(tenant, user) {
|
|
1359
|
+
try {
|
|
1360
|
+
const id = user.id === "" ? randomId("u_") : user.id;
|
|
1361
|
+
const existing = users.get(directoryKey(tenant, id));
|
|
1362
|
+
const next = stampUser({
|
|
1363
|
+
...user,
|
|
1364
|
+
id
|
|
1365
|
+
}, existing?.meta.created);
|
|
1366
|
+
assertUserUnique(tenant, next, id);
|
|
1367
|
+
users.set(directoryKey(tenant, id), next);
|
|
1368
|
+
return Promise.resolve(next);
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
return Promise.reject(asError(error));
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
patchUser(tenant, id, ops) {
|
|
1374
|
+
try {
|
|
1375
|
+
const existing = users.get(directoryKey(tenant, id));
|
|
1376
|
+
if (existing === void 0) throw new DirectoryNotFoundError();
|
|
1377
|
+
let next = existing;
|
|
1378
|
+
for (const op of ops) next = applyUserOp(next, op);
|
|
1379
|
+
next = stampUser(next, existing.meta.created);
|
|
1380
|
+
assertUserUnique(tenant, next, id);
|
|
1381
|
+
users.set(directoryKey(tenant, id), next);
|
|
1382
|
+
return Promise.resolve(next);
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
return Promise.reject(asError(error));
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
deleteUser(tenant, id) {
|
|
1388
|
+
if (!users.delete(directoryKey(tenant, id))) return Promise.reject(new DirectoryNotFoundError());
|
|
1389
|
+
for (const group of groupsIn(tenant)) {
|
|
1390
|
+
if (!group.members.some((member) => member.value === id)) continue;
|
|
1391
|
+
groups.set(directoryKey(tenant, group.id), stampGroup({
|
|
1392
|
+
...group,
|
|
1393
|
+
members: group.members.filter((member) => member.value !== id)
|
|
1394
|
+
}, group.meta.created));
|
|
1395
|
+
}
|
|
1396
|
+
return Promise.resolve();
|
|
1397
|
+
},
|
|
1398
|
+
getGroup(tenant, id) {
|
|
1399
|
+
return Promise.resolve(groups.get(directoryKey(tenant, id)) ?? null);
|
|
1400
|
+
},
|
|
1401
|
+
findGroups(tenant, filter, page) {
|
|
1402
|
+
const matched = groupsIn(tenant).filter((group) => matchFilter(group, filter));
|
|
1403
|
+
return Promise.resolve(pageOf(matched, page));
|
|
1404
|
+
},
|
|
1405
|
+
putGroup(tenant, group) {
|
|
1406
|
+
try {
|
|
1407
|
+
const id = group.id === "" ? randomId("g_") : group.id;
|
|
1408
|
+
const existing = groups.get(directoryKey(tenant, id));
|
|
1409
|
+
const next = stampGroup({
|
|
1410
|
+
...group,
|
|
1411
|
+
id,
|
|
1412
|
+
members: group.members
|
|
1413
|
+
}, existing?.meta.created);
|
|
1414
|
+
assertGroupUnique(tenant, next, id);
|
|
1415
|
+
groups.set(directoryKey(tenant, id), next);
|
|
1416
|
+
return Promise.resolve(next);
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
return Promise.reject(asError(error));
|
|
1419
|
+
}
|
|
1420
|
+
},
|
|
1421
|
+
patchGroup(tenant, id, ops) {
|
|
1422
|
+
try {
|
|
1423
|
+
const existing = groups.get(directoryKey(tenant, id));
|
|
1424
|
+
if (existing === void 0) throw new DirectoryNotFoundError();
|
|
1425
|
+
let next = existing;
|
|
1426
|
+
for (const op of ops) next = applyGroupOp(next, op);
|
|
1427
|
+
next = stampGroup(next, existing.meta.created);
|
|
1428
|
+
assertGroupUnique(tenant, next, id);
|
|
1429
|
+
groups.set(directoryKey(tenant, id), next);
|
|
1430
|
+
return Promise.resolve(next);
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
return Promise.reject(asError(error));
|
|
1433
|
+
}
|
|
1434
|
+
},
|
|
1435
|
+
deleteGroup(tenant, id) {
|
|
1436
|
+
if (!groups.delete(directoryKey(tenant, id))) return Promise.reject(new DirectoryNotFoundError());
|
|
1437
|
+
return Promise.resolve();
|
|
1438
|
+
},
|
|
1439
|
+
groupsFor(tenant, userId) {
|
|
1440
|
+
return Promise.resolve(groupsIn(tenant).filter((group) => group.members.some((member) => member.value === userId)));
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
//#endregion
|
|
1445
|
+
export { sha256Hex as S, PATCH_SCHEMA as _, normalizePatchOps as a, USER_SCHEMA as b, parseScimFilter as c, serviceProviderConfig as d, DirectoryNotFoundError as f, LIST_SCHEMA as g, GROUP_SCHEMA as h, tenantFromPath as i, resourceTypes as l, ERROR_SCHEMA as m, directoryMembershipSource as n, filterSupported as o, DirectoryUniquenessError as p, scimHandler as r, matchFilter as s, memoryDirectoryStore as t, schemas as u, ROLES_EXTENSION as v, authenticateScim as x, SCIM_CONTENT_TYPE as y };
|