@odla-ai/chapter 0.12.0 → 0.14.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 +9 -0
- package/dist/index.cjs +118 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -1
- package/dist/index.d.ts +95 -1
- package/dist/index.js +118 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -241,6 +241,15 @@ These bite silently — a smoke test won't catch them:
|
|
|
241
241
|
it, because the moment it would fire — approval — is a host route (approval is
|
|
242
242
|
out of scope, above). Send it from your own approve handler via `sendTemplated`;
|
|
243
243
|
the seed existing does not mean the built-ins send it.
|
|
244
|
+
- **Server-side Clerk is exported, even though the admin *routes* aren't.** A role
|
|
245
|
+
change is your host route — but you don't hand-roll the Clerk REST calls for it.
|
|
246
|
+
Beside `createClerkUser`/`createClerkInvitation`/`canChangeRole`, chapter exports
|
|
247
|
+
the odla→Clerk write half: `clerkGetUserByEmail`, `clerkGetUser`, `clerkListUsers`
|
|
248
|
+
(auto-paginated — never a silent 100-user cap), and `clerkSetRole`. Two
|
|
249
|
+
load-bearing semantics: an absent `public_metadata.role` reads as the lowest rung
|
|
250
|
+
(`"provisional"`), and `clerkSetRole` merge-`PATCH`es only `{ role }` so it never
|
|
251
|
+
clobbers a separately-written `public_metadata.profile`. All take an injectable
|
|
252
|
+
`fetch` and the vault `clerk_secret_key`.
|
|
244
253
|
|
|
245
254
|
### Verify from the types, not this file
|
|
246
255
|
|
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,12 @@ __export(index_exports, {
|
|
|
34
34
|
canceledPatch: () => canceledPatch,
|
|
35
35
|
chapterDb: () => chapterDb,
|
|
36
36
|
clampArray: () => clampArray,
|
|
37
|
+
clerkGetUser: () => clerkGetUser,
|
|
38
|
+
clerkGetUserByEmail: () => clerkGetUserByEmail,
|
|
39
|
+
clerkIntegration: () => clerkIntegration,
|
|
37
40
|
clerkInviteRequest: () => clerkInviteRequest,
|
|
41
|
+
clerkListUsers: () => clerkListUsers,
|
|
42
|
+
clerkSetRole: () => clerkSetRole,
|
|
38
43
|
clerkUserRequest: () => clerkUserRequest,
|
|
39
44
|
createChapterIntegration: () => createChapterIntegration,
|
|
40
45
|
createClerkInvitation: () => createClerkInvitation,
|
|
@@ -972,6 +977,119 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
|
|
|
972
977
|
return { ...healed, refreshed };
|
|
973
978
|
}
|
|
974
979
|
|
|
980
|
+
// src/clerk-roles.ts
|
|
981
|
+
var CLERK_API = "https://api.clerk.com";
|
|
982
|
+
var DEFAULT_ROLE = "provisional";
|
|
983
|
+
var PAGE = 100;
|
|
984
|
+
function toRecord(u) {
|
|
985
|
+
if (typeof u.id !== "string") return null;
|
|
986
|
+
const pm = u.public_metadata ?? {};
|
|
987
|
+
const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
|
|
988
|
+
const email = u.email_addresses?.[0]?.email_address;
|
|
989
|
+
return { id: u.id, email: typeof email === "string" ? email : void 0, role, publicMetadata: pm };
|
|
990
|
+
}
|
|
991
|
+
async function clerkGet(path, secretKey, fetchImpl) {
|
|
992
|
+
const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
|
|
993
|
+
if (!res.ok) throw new Error(`clerk GET ${path} \u2192 ${res.status}`);
|
|
994
|
+
return res.json();
|
|
995
|
+
}
|
|
996
|
+
async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
|
|
997
|
+
const data = await clerkGet(`/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, secretKey, fetchImpl).catch(() => null);
|
|
998
|
+
const user = Array.isArray(data) ? data[0] : void 0;
|
|
999
|
+
return user ? toRecord(user) : null;
|
|
1000
|
+
}
|
|
1001
|
+
async function clerkGetUser(secretKey, id2, fetchImpl = fetch) {
|
|
1002
|
+
const data = await clerkGet(`/v1/users/${encodeURIComponent(id2)}`, secretKey, fetchImpl).catch(() => null);
|
|
1003
|
+
return data ? toRecord(data) : null;
|
|
1004
|
+
}
|
|
1005
|
+
async function clerkListUsers(secretKey, fetchImpl = fetch) {
|
|
1006
|
+
const out = [];
|
|
1007
|
+
for (let offset = 0; ; offset += PAGE) {
|
|
1008
|
+
const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
|
|
1009
|
+
const page = Array.isArray(data) ? data : [];
|
|
1010
|
+
for (const u of page) {
|
|
1011
|
+
const record = toRecord(u);
|
|
1012
|
+
if (record) out.push(record);
|
|
1013
|
+
}
|
|
1014
|
+
if (page.length < PAGE) break;
|
|
1015
|
+
}
|
|
1016
|
+
return out;
|
|
1017
|
+
}
|
|
1018
|
+
async function clerkSetRole(secretKey, id2, role, fetchImpl = fetch) {
|
|
1019
|
+
const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id2)}/metadata`, {
|
|
1020
|
+
method: "PATCH",
|
|
1021
|
+
headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
|
|
1022
|
+
body: JSON.stringify({ public_metadata: { role } })
|
|
1023
|
+
});
|
|
1024
|
+
return res.ok;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/clerk-integration.ts
|
|
1028
|
+
var clerkIntegration = {
|
|
1029
|
+
id: "clerk",
|
|
1030
|
+
title: "Clerk end-user auth",
|
|
1031
|
+
npm: "@odla-ai/chapter",
|
|
1032
|
+
settings: [
|
|
1033
|
+
{
|
|
1034
|
+
key: "publishableKey",
|
|
1035
|
+
description: "Clerk publishable key (pk_*). Public by design; served to the SPA (loads clerk-js from Clerk's CDN via @odla-ai/auth-clerk).",
|
|
1036
|
+
public: true,
|
|
1037
|
+
pattern: "pk_",
|
|
1038
|
+
perEnv: true,
|
|
1039
|
+
source: "apps-registry auth[env].publishableKey (from odla.config.mjs auth.clerk.<env>)"
|
|
1040
|
+
}
|
|
1041
|
+
],
|
|
1042
|
+
secrets: [
|
|
1043
|
+
{
|
|
1044
|
+
key: "CLERK_WEBHOOK_SECRET",
|
|
1045
|
+
description: "Svix signing secret for Clerk user webhooks \u2014 verifies $users sync events.",
|
|
1046
|
+
pattern: "whsec_",
|
|
1047
|
+
mode: "full",
|
|
1048
|
+
vault: true
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
key: "clerk_secret_key",
|
|
1052
|
+
description: "Clerk backend key (sk_*) \u2014 powers the odla->Clerk writes (account create, role + profile via public_metadata) and lets odla-db resolve user email/name via the Clerk API.",
|
|
1053
|
+
pattern: "sk_",
|
|
1054
|
+
mode: "full",
|
|
1055
|
+
vault: true
|
|
1056
|
+
}
|
|
1057
|
+
],
|
|
1058
|
+
syncs: [
|
|
1059
|
+
{
|
|
1060
|
+
engine: "@odla-ai/db $users (Clerk webhook -> tenant graph)",
|
|
1061
|
+
direction: "provider->odla",
|
|
1062
|
+
entity: "$users",
|
|
1063
|
+
// Matches the webhook mapper: the mirror carries id + primary email + name + avatar.
|
|
1064
|
+
fields: ["id", "email", "name", "imageUrl"],
|
|
1065
|
+
webhook: "whsec_ (svix-signed)",
|
|
1066
|
+
onDelete: "tombstone (never row-delete)"
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
engine: "@odla-ai/chapter clerk.ts (Clerk Backend API via vault clerk_secret_key)",
|
|
1070
|
+
direction: "odla->provider",
|
|
1071
|
+
entity: "clerk user",
|
|
1072
|
+
// Separate merge-PATCHes so a role write never clobbers a profile write.
|
|
1073
|
+
fields: ["public_metadata.role", "public_metadata.profile"],
|
|
1074
|
+
onDelete: "n/a (writes only)"
|
|
1075
|
+
}
|
|
1076
|
+
],
|
|
1077
|
+
provision: {
|
|
1078
|
+
human: [
|
|
1079
|
+
"Run `npx clerk auth login` \u2014 the one interactive step; the agent then creates + configures the Clerk app with the Clerk CLI (no pk_ to hand-paste).",
|
|
1080
|
+
'For auth mode "full": create the Clerk user-sync webhook and store its whsec_, plus the sk_ as clerk_secret_key, in the tenant vault (Studio, write-only).'
|
|
1081
|
+
],
|
|
1082
|
+
cli: [
|
|
1083
|
+
"Clerk CLI (`npx clerk apps create/link/config patch`) creates + configures the instance and pulls the pk_.",
|
|
1084
|
+
"odla `provision` records it: setAuth(env, publishableKey) -> apps-registry (issuer/JWKS derived from the key)."
|
|
1085
|
+
],
|
|
1086
|
+
doctor: [
|
|
1087
|
+
"registry auth[env] present when the app mounts <SignIn>?",
|
|
1088
|
+
'auth mode "full" => whsec_ and clerk_secret_key present in the tenant vault?'
|
|
1089
|
+
]
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
975
1093
|
// src/session.ts
|
|
976
1094
|
function applicationSummary(app) {
|
|
977
1095
|
return {
|