@odla-ai/chapter 0.12.0 → 0.13.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 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,11 @@ __export(index_exports, {
34
34
  canceledPatch: () => canceledPatch,
35
35
  chapterDb: () => chapterDb,
36
36
  clampArray: () => clampArray,
37
+ clerkGetUser: () => clerkGetUser,
38
+ clerkGetUserByEmail: () => clerkGetUserByEmail,
37
39
  clerkInviteRequest: () => clerkInviteRequest,
40
+ clerkListUsers: () => clerkListUsers,
41
+ clerkSetRole: () => clerkSetRole,
38
42
  clerkUserRequest: () => clerkUserRequest,
39
43
  createChapterIntegration: () => createChapterIntegration,
40
44
  createClerkInvitation: () => createClerkInvitation,
@@ -972,6 +976,53 @@ async function createClerkUser(secretKey, input, fetchImpl = fetch) {
972
976
  return { ...healed, refreshed };
973
977
  }
974
978
 
979
+ // src/clerk-roles.ts
980
+ var CLERK_API = "https://api.clerk.com";
981
+ var DEFAULT_ROLE = "provisional";
982
+ var PAGE = 100;
983
+ function toRecord(u) {
984
+ if (typeof u.id !== "string") return null;
985
+ const pm = u.public_metadata ?? {};
986
+ const role = typeof pm.role === "string" && pm.role ? pm.role : DEFAULT_ROLE;
987
+ const email = u.email_addresses?.[0]?.email_address;
988
+ return { id: u.id, email: typeof email === "string" ? email : void 0, role, publicMetadata: pm };
989
+ }
990
+ async function clerkGet(path, secretKey, fetchImpl) {
991
+ const res = await fetchImpl(`${CLERK_API}${path}`, { headers: { authorization: `Bearer ${secretKey}` } });
992
+ if (!res.ok) throw new Error(`clerk GET ${path} \u2192 ${res.status}`);
993
+ return res.json();
994
+ }
995
+ async function clerkGetUserByEmail(secretKey, email, fetchImpl = fetch) {
996
+ const data = await clerkGet(`/v1/users?email_address=${encodeURIComponent(email)}&limit=1`, secretKey, fetchImpl).catch(() => null);
997
+ const user = Array.isArray(data) ? data[0] : void 0;
998
+ return user ? toRecord(user) : null;
999
+ }
1000
+ async function clerkGetUser(secretKey, id2, fetchImpl = fetch) {
1001
+ const data = await clerkGet(`/v1/users/${encodeURIComponent(id2)}`, secretKey, fetchImpl).catch(() => null);
1002
+ return data ? toRecord(data) : null;
1003
+ }
1004
+ async function clerkListUsers(secretKey, fetchImpl = fetch) {
1005
+ const out = [];
1006
+ for (let offset = 0; ; offset += PAGE) {
1007
+ const data = await clerkGet(`/v1/users?limit=${PAGE}&offset=${offset}`, secretKey, fetchImpl);
1008
+ const page = Array.isArray(data) ? data : [];
1009
+ for (const u of page) {
1010
+ const record = toRecord(u);
1011
+ if (record) out.push(record);
1012
+ }
1013
+ if (page.length < PAGE) break;
1014
+ }
1015
+ return out;
1016
+ }
1017
+ async function clerkSetRole(secretKey, id2, role, fetchImpl = fetch) {
1018
+ const res = await fetchImpl(`${CLERK_API}/v1/users/${encodeURIComponent(id2)}/metadata`, {
1019
+ method: "PATCH",
1020
+ headers: { authorization: `Bearer ${secretKey}`, "content-type": "application/json" },
1021
+ body: JSON.stringify({ public_metadata: { role } })
1022
+ });
1023
+ return res.ok;
1024
+ }
1025
+
975
1026
  // src/session.ts
976
1027
  function applicationSummary(app) {
977
1028
  return {