@shipfox/api-auth 15.0.0 → 17.0.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.
Files changed (101) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/README.md +41 -0
  4. package/dist/config.d.ts +1 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +4 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/core/administration.d.ts +24 -2
  9. package/dist/core/administration.d.ts.map +1 -1
  10. package/dist/core/administration.js +126 -3
  11. package/dist/core/administration.js.map +1 -1
  12. package/dist/core/auth.d.ts +31 -0
  13. package/dist/core/auth.d.ts.map +1 -1
  14. package/dist/core/auth.js +70 -2
  15. package/dist/core/auth.js.map +1 -1
  16. package/dist/core/errors.d.ts +15 -0
  17. package/dist/core/errors.d.ts.map +1 -1
  18. package/dist/core/errors.js +30 -0
  19. package/dist/core/errors.js.map +1 -1
  20. package/dist/core/jwt.d.ts +2 -0
  21. package/dist/core/jwt.d.ts.map +1 -1
  22. package/dist/core/jwt.js +22 -1
  23. package/dist/core/jwt.js.map +1 -1
  24. package/dist/db/admin-command.d.ts +12 -4
  25. package/dist/db/admin-command.d.ts.map +1 -1
  26. package/dist/db/admin-command.js +10 -1
  27. package/dist/db/admin-command.js.map +1 -1
  28. package/dist/db/admin-grants.d.ts.map +1 -1
  29. package/dist/db/admin-grants.js +4 -1
  30. package/dist/db/admin-grants.js.map +1 -1
  31. package/dist/db/admin-user-moderation.d.ts +3 -0
  32. package/dist/db/admin-user-moderation.d.ts.map +1 -1
  33. package/dist/db/admin-user-moderation.js +6 -2
  34. package/dist/db/admin-user-moderation.js.map +1 -1
  35. package/dist/db/impersonation.d.ts +55 -0
  36. package/dist/db/impersonation.d.ts.map +1 -0
  37. package/dist/db/impersonation.js +212 -0
  38. package/dist/db/impersonation.js.map +1 -0
  39. package/dist/db/schema/admin-command-results.d.ts +16 -0
  40. package/dist/db/schema/admin-command-results.d.ts.map +1 -1
  41. package/dist/db/schema/admin-command-results.js.map +1 -1
  42. package/dist/index.d.ts +5 -4
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +5 -5
  45. package/dist/index.js.map +1 -1
  46. package/dist/metrics/index.d.ts +1 -1
  47. package/dist/metrics/index.d.ts.map +1 -1
  48. package/dist/metrics/index.js +1 -1
  49. package/dist/metrics/index.js.map +1 -1
  50. package/dist/metrics/instance.d.ts +4 -2
  51. package/dist/metrics/instance.d.ts.map +1 -1
  52. package/dist/metrics/instance.js +8 -0
  53. package/dist/metrics/instance.js.map +1 -1
  54. package/dist/presentation/auth/bearer-token-auth.d.ts +2 -2
  55. package/dist/presentation/auth/bearer-token-auth.d.ts.map +1 -1
  56. package/dist/presentation/auth/bearer-token-auth.js +2 -2
  57. package/dist/presentation/auth/bearer-token-auth.js.map +1 -1
  58. package/dist/presentation/auth/jwt-auth.d.ts.map +1 -1
  59. package/dist/presentation/auth/jwt-auth.js +26 -2
  60. package/dist/presentation/auth/jwt-auth.js.map +1 -1
  61. package/dist/presentation/routes/administration.d.ts +2 -1
  62. package/dist/presentation/routes/administration.d.ts.map +1 -1
  63. package/dist/presentation/routes/administration.js +127 -19
  64. package/dist/presentation/routes/administration.js.map +1 -1
  65. package/dist/presentation/routes/rate-limit.d.ts +8 -0
  66. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  67. package/dist/presentation/routes/rate-limit.js +44 -0
  68. package/dist/presentation/routes/rate-limit.js.map +1 -1
  69. package/dist/presentation/routes/session/me.d.ts.map +1 -1
  70. package/dist/presentation/routes/session/me.js +2 -1
  71. package/dist/presentation/routes/session/me.js.map +1 -1
  72. package/dist/tsconfig.test.tsbuildinfo +1 -1
  73. package/package.json +5 -5
  74. package/src/config.ts +4 -0
  75. package/src/core/administration.ts +172 -1
  76. package/src/core/auth.test.ts +79 -0
  77. package/src/core/auth.ts +98 -1
  78. package/src/core/errors.ts +35 -0
  79. package/src/core/jwt.test.ts +92 -0
  80. package/src/core/jwt.ts +38 -9
  81. package/src/db/admin-command.ts +35 -6
  82. package/src/db/admin-grants.ts +5 -1
  83. package/src/db/admin-user-moderation.ts +8 -2
  84. package/src/db/impersonation.ts +323 -0
  85. package/src/db/schema/admin-command-results.ts +18 -0
  86. package/src/index.test.ts +1 -1
  87. package/src/index.ts +12 -3
  88. package/src/metrics/index.ts +2 -0
  89. package/src/metrics/instance.ts +13 -2
  90. package/src/presentation/auth/bearer-token-auth.ts +4 -4
  91. package/src/presentation/auth/jwt-auth.test.ts +191 -0
  92. package/src/presentation/auth/jwt-auth.ts +19 -1
  93. package/src/presentation/routes/administration.test.ts +828 -3
  94. package/src/presentation/routes/administration.ts +130 -11
  95. package/src/presentation/routes/rate-limit.ts +40 -0
  96. package/src/presentation/routes/session/login.test.ts +5 -0
  97. package/src/presentation/routes/session/me.test.ts +24 -0
  98. package/src/presentation/routes/session/me.ts +1 -0
  99. package/src/presentation/routes/session/refresh.test.ts +11 -0
  100. package/test/routes.ts +43 -16
  101. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/admin-command.ts"],"sourcesContent":["import type {\n AdministrationActionEvent,\n AdministrationActionEventMap,\n} from '@shipfox/api-common-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, eq, sql} from 'drizzle-orm';\nimport {AdminIdempotencyKeyReuseError} from '#core/errors.js';\nimport type {db} from './db.js';\nimport {\n type AdminCommandResultDb,\n adminCommandResults,\n type StoredAdminCommandResult,\n} from './schema/admin-command-results.js';\nimport {authOutbox} from './schema/outbox.js';\n\nexport type Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport interface AdminCommandTransactionParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nexport async function lockAdminCommand(\n tx: Tx,\n params: Pick<AdminCommandTransactionParams, 'actorId' | 'idempotencyKeyFingerprint'>,\n): Promise<void> {\n await tx.execute(\n sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`,\n );\n}\n\nexport async function lockAdminOwnerGrants(tx: Tx): Promise<void> {\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n}\n\nexport async function findAdminCommandResult(\n tx: Tx,\n params: Pick<\n AdminCommandTransactionParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {command: string},\n): Promise<AdminCommandResultDb | undefined> {\n const rows = await tx\n .select()\n .from(adminCommandResults)\n .where(\n and(\n eq(adminCommandResults.actorId, params.actorId),\n eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint),\n ),\n )\n .limit(1);\n const result = rows[0];\n if (!result) return undefined;\n if (\n result.command !== params.command ||\n result.requestFingerprint !== params.requestFingerprint\n ) {\n throw new AdminIdempotencyKeyReuseError();\n }\n return result;\n}\n\nexport async function storeAdminCommandResult(\n tx: Tx,\n params: AdminCommandTransactionParams,\n result: StoredAdminCommandResult,\n): Promise<void> {\n await tx.insert(adminCommandResults).values({\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n command: params.event.command,\n requestFingerprint: params.requestFingerprint,\n result,\n });\n}\n\nexport async function writeAdminAction(tx: Tx, event: AdministrationActionEvent): Promise<void> {\n await writeOutboxEvent<AdministrationActionEventMap>(tx, authOutbox, {\n type: 'administration.action.performed',\n payload: event,\n });\n}\n"],"names":["writeOutboxEvent","and","eq","sql","AdminIdempotencyKeyReuseError","adminCommandResults","authOutbox","lockAdminCommand","tx","params","execute","actorId","idempotencyKeyFingerprint","lockAdminOwnerGrants","findAdminCommandResult","rows","select","from","where","limit","result","undefined","command","requestFingerprint","storeAdminCommandResult","insert","values","event","writeAdminAction","type","payload"],"mappings":"AAIA,SAAQA,gBAAgB,QAAO,uBAAuB;AACtD,SAAQC,GAAG,EAAEC,EAAE,EAAEC,GAAG,QAAO,cAAc;AACzC,SAAQC,6BAA6B,QAAO,kBAAkB;AAE9D,SAEEC,mBAAmB,QAEd,oCAAoC;AAC3C,SAAQC,UAAU,QAAO,qBAAqB;AAW9C,OAAO,eAAeC,iBACpBC,EAAM,EACNC,MAAoF;IAEpF,MAAMD,GAAGE,OAAO,CACdP,GAAG,CAAC,sCAAsC,EAAE,CAAC,mBAAmB,EAAEM,OAAOE,OAAO,CAAC,CAAC,EAAEF,OAAOG,yBAAyB,EAAE,CAAC,EAAE,CAAC;AAE9H;AAEA,OAAO,eAAeC,qBAAqBL,EAAM;IAC/C,MAAMA,GAAGE,OAAO,CAACP,GAAG,CAAC,iEAAiE,CAAC;AACzF;AAEA,OAAO,eAAeW,uBACpBN,EAAM,EACNC,MAGqB;IAErB,MAAMM,OAAO,MAAMP,GAChBQ,MAAM,GACNC,IAAI,CAACZ,qBACLa,KAAK,CACJjB,IACEC,GAAGG,oBAAoBM,OAAO,EAAEF,OAAOE,OAAO,GAC9CT,GAAGG,oBAAoBO,yBAAyB,EAAEH,OAAOG,yBAAyB,IAGrFO,KAAK,CAAC;IACT,MAAMC,SAASL,IAAI,CAAC,EAAE;IACtB,IAAI,CAACK,QAAQ,OAAOC;IACpB,IACED,OAAOE,OAAO,KAAKb,OAAOa,OAAO,IACjCF,OAAOG,kBAAkB,KAAKd,OAAOc,kBAAkB,EACvD;QACA,MAAM,IAAInB;IACZ;IACA,OAAOgB;AACT;AAEA,OAAO,eAAeI,wBACpBhB,EAAM,EACNC,MAAqC,EACrCW,MAAgC;IAEhC,MAAMZ,GAAGiB,MAAM,CAACpB,qBAAqBqB,MAAM,CAAC;QAC1Cf,SAASF,OAAOE,OAAO;QACvBC,2BAA2BH,OAAOG,yBAAyB;QAC3DU,SAASb,OAAOkB,KAAK,CAACL,OAAO;QAC7BC,oBAAoBd,OAAOc,kBAAkB;QAC7CH;IACF;AACF;AAEA,OAAO,eAAeQ,iBAAiBpB,EAAM,EAAEmB,KAAgC;IAC7E,MAAM3B,iBAA+CQ,IAAIF,YAAY;QACnEuB,MAAM;QACNC,SAASH;IACX;AACF"}
1
+ {"version":3,"sources":["../../src/db/admin-command.ts"],"sourcesContent":["import type {\n AdministrationActionEvent,\n AdministrationActionEventMap,\n} from '@shipfox/api-common-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport {and, eq, sql} from 'drizzle-orm';\nimport {AdminIdempotencyKeyReuseError} from '#core/errors.js';\nimport type {db} from './db.js';\nimport {\n type AdminCommandResultDb,\n adminCommandResults,\n type StoredAdminCommandResult,\n} from './schema/admin-command-results.js';\nimport {authOutbox} from './schema/outbox.js';\n\nexport type Tx = Parameters<Parameters<ReturnType<typeof db>['transaction']>[0]>[0];\n\nexport interface AdminCommandTransactionParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nexport type AdminCommandResultLookup = Pick<\n AdminCommandTransactionParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n> & {command: string};\n\nexport type AdminCommandResultKey = Pick<\n AdminCommandTransactionParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n>;\n\nexport async function lockAdminCommand(\n tx: Tx,\n params: Pick<AdminCommandTransactionParams, 'actorId' | 'idempotencyKeyFingerprint'>,\n): Promise<void> {\n await tx.execute(\n sql`select pg_advisory_xact_lock(hashtext(${`auth_admin_command:${params.actorId}:${params.idempotencyKeyFingerprint}`}))`,\n );\n}\n\nexport async function lockAdminOwnerGrants(tx: Tx): Promise<void> {\n await tx.execute(sql`select pg_advisory_xact_lock(hashtext('auth_admin_owner_grants'))`);\n}\n\nexport async function findAdminCommandResult(\n tx: Tx,\n params: AdminCommandResultLookup,\n): Promise<AdminCommandResultDb | undefined> {\n const rows = await tx\n .select()\n .from(adminCommandResults)\n .where(\n and(\n eq(adminCommandResults.actorId, params.actorId),\n eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint),\n ),\n )\n .limit(1);\n const result = rows[0];\n if (!result) return undefined;\n if (\n result.command !== params.command ||\n result.requestFingerprint !== params.requestFingerprint\n ) {\n throw new AdminIdempotencyKeyReuseError();\n }\n return result;\n}\n\nexport async function storeAdminCommandResult(\n tx: Tx,\n params: AdminCommandResultLookup,\n result: StoredAdminCommandResult,\n): Promise<void> {\n await tx.insert(adminCommandResults).values({\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n command: params.command,\n requestFingerprint: params.requestFingerprint,\n result,\n });\n}\n\n/**\n * Replaces the stored result of an already-committed command, used by\n * impersonation replays to append the newly issued token's fingerprint while\n * keeping the original `expires_at`.\n */\nexport async function updateAdminCommandResult(\n tx: Tx,\n params: AdminCommandResultKey,\n result: StoredAdminCommandResult,\n): Promise<void> {\n await tx\n .update(adminCommandResults)\n .set({result})\n .where(\n and(\n eq(adminCommandResults.actorId, params.actorId),\n eq(adminCommandResults.idempotencyKeyFingerprint, params.idempotencyKeyFingerprint),\n eq(adminCommandResults.requestFingerprint, params.requestFingerprint),\n ),\n );\n}\n\nexport async function writeAdminAction(tx: Tx, event: AdministrationActionEvent): Promise<void> {\n await writeOutboxEvent<AdministrationActionEventMap>(tx, authOutbox, {\n type: 'administration.action.performed',\n payload: event,\n });\n}\n"],"names":["writeOutboxEvent","and","eq","sql","AdminIdempotencyKeyReuseError","adminCommandResults","authOutbox","lockAdminCommand","tx","params","execute","actorId","idempotencyKeyFingerprint","lockAdminOwnerGrants","findAdminCommandResult","rows","select","from","where","limit","result","undefined","command","requestFingerprint","storeAdminCommandResult","insert","values","updateAdminCommandResult","update","set","writeAdminAction","event","type","payload"],"mappings":"AAIA,SAAQA,gBAAgB,QAAO,uBAAuB;AACtD,SAAQC,GAAG,EAAEC,EAAE,EAAEC,GAAG,QAAO,cAAc;AACzC,SAAQC,6BAA6B,QAAO,kBAAkB;AAE9D,SAEEC,mBAAmB,QAEd,oCAAoC;AAC3C,SAAQC,UAAU,QAAO,qBAAqB;AAqB9C,OAAO,eAAeC,iBACpBC,EAAM,EACNC,MAAoF;IAEpF,MAAMD,GAAGE,OAAO,CACdP,GAAG,CAAC,sCAAsC,EAAE,CAAC,mBAAmB,EAAEM,OAAOE,OAAO,CAAC,CAAC,EAAEF,OAAOG,yBAAyB,EAAE,CAAC,EAAE,CAAC;AAE9H;AAEA,OAAO,eAAeC,qBAAqBL,EAAM;IAC/C,MAAMA,GAAGE,OAAO,CAACP,GAAG,CAAC,iEAAiE,CAAC;AACzF;AAEA,OAAO,eAAeW,uBACpBN,EAAM,EACNC,MAAgC;IAEhC,MAAMM,OAAO,MAAMP,GAChBQ,MAAM,GACNC,IAAI,CAACZ,qBACLa,KAAK,CACJjB,IACEC,GAAGG,oBAAoBM,OAAO,EAAEF,OAAOE,OAAO,GAC9CT,GAAGG,oBAAoBO,yBAAyB,EAAEH,OAAOG,yBAAyB,IAGrFO,KAAK,CAAC;IACT,MAAMC,SAASL,IAAI,CAAC,EAAE;IACtB,IAAI,CAACK,QAAQ,OAAOC;IACpB,IACED,OAAOE,OAAO,KAAKb,OAAOa,OAAO,IACjCF,OAAOG,kBAAkB,KAAKd,OAAOc,kBAAkB,EACvD;QACA,MAAM,IAAInB;IACZ;IACA,OAAOgB;AACT;AAEA,OAAO,eAAeI,wBACpBhB,EAAM,EACNC,MAAgC,EAChCW,MAAgC;IAEhC,MAAMZ,GAAGiB,MAAM,CAACpB,qBAAqBqB,MAAM,CAAC;QAC1Cf,SAASF,OAAOE,OAAO;QACvBC,2BAA2BH,OAAOG,yBAAyB;QAC3DU,SAASb,OAAOa,OAAO;QACvBC,oBAAoBd,OAAOc,kBAAkB;QAC7CH;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAeO,yBACpBnB,EAAM,EACNC,MAA6B,EAC7BW,MAAgC;IAEhC,MAAMZ,GACHoB,MAAM,CAACvB,qBACPwB,GAAG,CAAC;QAACT;IAAM,GACXF,KAAK,CACJjB,IACEC,GAAGG,oBAAoBM,OAAO,EAAEF,OAAOE,OAAO,GAC9CT,GAAGG,oBAAoBO,yBAAyB,EAAEH,OAAOG,yBAAyB,GAClFV,GAAGG,oBAAoBkB,kBAAkB,EAAEd,OAAOc,kBAAkB;AAG5E;AAEA,OAAO,eAAeO,iBAAiBtB,EAAM,EAAEuB,KAAgC;IAC7E,MAAM/B,iBAA+CQ,IAAIF,YAAY;QACnE0B,MAAM;QACNC,SAASF;IACX;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"admin-grants.d.ts","sourceRoot":"","sources":["../../src/db/admin-grants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,4CAA4C,CAAC;AAQ1F,OAAO,EAKL,KAAK,EAAE,EAER,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAK3B,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC;AAmB1D,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ1F;AAED,MAAM,WAAW,+BAA+B;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,yBAAyB,CAAC,MAAM,CAAC,CAAC;CACzC;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,GAAG,OAAO,CAAC;IACV,IAAI,EAAE,+BAA+B,EAAE,CAAC;IACxC,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACtC,CAAC,CA+BD;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAc9F;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,GAAE,uBAA8B,GACvC,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CA2BjG;AAED,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAiDD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACnD,OAAO,CAAC,UAAU,CAAC,CAiCrB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GACpE,OAAO,CAAC,UAAU,CAAC,CA4CrB;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,yBAAyB,GAAG;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACpD,OAAO,CAAC,UAAU,CAAC,CAqCrB"}
1
+ {"version":3,"file":"admin-grants.d.ts","sourceRoot":"","sources":["../../src/db/admin-grants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAEL,KAAK,iBAAiB,EAEvB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,4CAA4C,CAAC;AAQ1F,OAAO,EAKL,KAAK,EAAE,EAER,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAK3B,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,CAAC;AAmB1D,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CAQ1F;AAED,MAAM,WAAW,+BAA+B;IAC9C,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,yBAAyB,CAAC,MAAM,CAAC,CAAC;CACzC;AAED,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,GAAG,OAAO,CAAC;IACV,IAAI,EAAE,+BAA+B,EAAE,CAAC;IACxC,UAAU,EAAE,iBAAiB,GAAG,IAAI,CAAC;CACtC,CAAC,CA+BD;AAED,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAc9F;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,GAAE,uBAA8B,GACvC,OAAO,CAAC,OAAO,CAAC,CAGlB;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CA2BjG;AAED,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAqDD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACnD,OAAO,CAAC,UAAU,CAAC,CAiCrB;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,yBAAyB,GAAG;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAC,GACpE,OAAO,CAAC,UAAU,CAAC,CA4CrB;AAED,wBAAsB,yBAAyB,CAC7C,MAAM,EAAE,yBAAyB,GAAG;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACpD,OAAO,CAAC,UAAU,CAAC,CAqCrB"}
@@ -117,7 +117,10 @@ async function findCommandResult(tx, params) {
117
117
  return fromStoredAdminGrant(result.result.grant);
118
118
  }
119
119
  async function storeCommandResult(tx, params, grant) {
120
- await storeAdminCommandResult(tx, params, {
120
+ await storeAdminCommandResult(tx, {
121
+ ...params,
122
+ command: params.event.command
123
+ }, {
121
124
  grant: toStoredAdminGrant(grant)
122
125
  });
123
126
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/admin-grants.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport type {AdministrationActionEvent} from '@shipfox/api-common-dto';\nimport {\n paginateTimestampIdRows,\n type TimestampIdCursor,\n timestampIdCursorWhere,\n} from '@shipfox/node-drizzle';\nimport {and, desc, eq, isNull} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {AdministratorGrantSummary} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {\n findAdminCommandResult,\n lockAdminCommand,\n lockAdminOwnerGrants,\n storeAdminCommandResult,\n type Tx,\n writeAdminAction,\n} from './admin-command.js';\nimport {db} from './db.js';\nimport type {StoredAdminGrant} from './schema/admin-command-results.js';\nimport {adminGrants, toAdminGrant} from './schema/admin-grants.js';\nimport {users} from './schema/users.js';\n\ntype AdminOwnerQueryExecutor = ReturnType<typeof db> | Tx;\n\nfunction activeAdminOwnerWhere() {\n return and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n );\n}\n\nasync function listActiveAdminOwners(executor: AdminOwnerQueryExecutor) {\n return await executor\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(activeAdminOwnerWhere())\n .limit(2);\n}\n\nexport interface CreateAdminGrantParams {\n userId: string;\n role: AdminRole;\n}\n\nexport async function createAdminGrant(params: CreateAdminGrantParams): Promise<AdminGrant> {\n const rows = await db()\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toAdminGrant(row);\n}\n\nexport interface AdministratorGrantSummaryRecord {\n id: string;\n role: AdminRole;\n createdAt: Date;\n revokedAt: Date | null;\n user: AdministratorGrantSummary['user'];\n}\n\nexport async function listAdminGrantSummaries(params: {\n limit: number;\n cursor?: TimestampIdCursor;\n}): Promise<{\n rows: AdministratorGrantSummaryRecord[];\n nextCursor: TimestampIdCursor | null;\n}> {\n const cursorCondition = timestampIdCursorWhere({\n timestampColumn: adminGrants.createdAt,\n idColumn: adminGrants.id,\n cursor: params.cursor,\n });\n const conditions = cursorCondition ? [cursorCondition] : [];\n const rows = await db()\n .select({\n id: adminGrants.id,\n role: adminGrants.role,\n createdAt: adminGrants.createdAt,\n revokedAt: adminGrants.revokedAt,\n user: {\n id: users.id,\n email: users.email,\n name: users.name,\n status: users.status,\n },\n })\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(and(...conditions))\n .orderBy(desc(adminGrants.createdAt), desc(adminGrants.id))\n .limit(params.limit + 1);\n\n const page = paginateTimestampIdRows({rows, limit: params.limit, timestampKey: 'createdAt'});\n return {\n rows: page.pageRows,\n nextCursor: page.nextCursor,\n };\n}\n\nexport async function findCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {\n const rows = await db()\n .select({role: adminGrants.role})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n\n return highestAdminRole(rows.map(({role}) => role));\n}\n\n/**\n * An owner is active only while both the grant and its user account remain\n * active. A suspended owner's grant is retained, but it intentionally does\n * not prevent deployment-bound bootstrap recovery.\n */\nexport async function hasActiveAdminOwner(\n executor: AdminOwnerQueryExecutor = db(),\n): Promise<boolean> {\n const rows = await listActiveAdminOwners(executor);\n return rows.length > 0;\n}\n\nexport async function revokeAdminGrant(params: {grantId: string}): Promise<AdminGrant | undefined> {\n return await db().transaction(async (tx) => {\n // All owner grant changes share one lock so concurrent revocations cannot\n // both observe the same final owner and leave the instance ownerless.\n await lockAdminOwnerGrants(tx);\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) return undefined;\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await listActiveAdminOwners(tx);\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const row = updated[0];\n return row ? toAdminGrant(row) : undefined;\n });\n}\n\ninterface AuditedAdminCommandParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nfunction toStoredAdminGrant(grant: AdminGrant): StoredAdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt?.toISOString() ?? null,\n createdAt: grant.createdAt.toISOString(),\n updatedAt: grant.updatedAt.toISOString(),\n };\n}\n\nfunction fromStoredAdminGrant(grant: StoredAdminGrant): AdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt ? new Date(grant.revokedAt) : null,\n createdAt: new Date(grant.createdAt),\n updatedAt: new Date(grant.updatedAt),\n };\n}\n\nasync function findCommandResult(\n tx: Tx,\n params: Pick<\n AuditedAdminCommandParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {\n command: string;\n },\n): Promise<AdminGrant | undefined> {\n const result = await findAdminCommandResult(tx, params);\n if (!result) return undefined;\n if (!('grant' in result.result)) {\n throw new Error('Administrator command result has an unexpected shape');\n }\n return fromStoredAdminGrant(result.result.grant);\n}\n\nasync function storeCommandResult(\n tx: Tx,\n params: AuditedAdminCommandParams,\n grant: AdminGrant,\n): Promise<void> {\n await storeAdminCommandResult(tx, params, {grant: toStoredAdminGrant(grant)});\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AuditedAdminCommandParams & {userId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n if (await hasActiveAdminOwner(tx)) throw new AdminBootstrapClosedError();\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: 'admin-owner'})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Bootstrap grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function grantAdminRoleWithAudit(\n params: AuditedAdminCommandParams & {userId: string; role: AdminRole},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const activeRows = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n eq(adminGrants.role, params.role),\n isNull(adminGrants.revokedAt),\n ),\n )\n .limit(1);\n if (activeRows.length > 0) throw new AdminGrantAlreadyExistsError();\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Administrator grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function revokeAdminGrantWithAudit(\n params: AuditedAdminCommandParams & {grantId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) throw new AdminGrantNotFoundError();\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await listActiveAdminOwners(tx);\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const updatedRow = updated[0];\n if (!updatedRow) throw new AdminGrantNotFoundError();\n\n const revoked = toAdminGrant(updatedRow);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, revoked);\n return revoked;\n });\n}\n"],"names":["paginateTimestampIdRows","timestampIdCursorWhere","and","desc","eq","isNull","highestAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","LastAdminOwnerError","UserNotFoundError","findAdminCommandResult","lockAdminCommand","lockAdminOwnerGrants","storeAdminCommandResult","writeAdminAction","db","adminGrants","toAdminGrant","users","activeAdminOwnerWhere","role","revokedAt","status","listActiveAdminOwners","executor","select","id","from","innerJoin","userId","where","limit","createAdminGrant","params","rows","insert","values","returning","row","Error","listAdminGrantSummaries","cursorCondition","timestampColumn","createdAt","idColumn","cursor","conditions","user","email","name","orderBy","page","timestampKey","pageRows","nextCursor","findCurrentAdminRole","map","hasActiveAdminOwner","length","revokeAdminGrant","transaction","tx","grantId","grant","undefined","activeOwners","updated","update","set","Date","updatedAt","toStoredAdminGrant","toISOString","fromStoredAdminGrant","findCommandResult","result","storeCommandResult","bootstrapFirstAdminOwner","existing","command","event","userRows","grantAdminRoleWithAudit","activeRows","revokeAdminGrantWithAudit","updatedRow","revoked"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/db/admin-grants.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport type {AdministrationActionEvent} from '@shipfox/api-common-dto';\nimport {\n paginateTimestampIdRows,\n type TimestampIdCursor,\n timestampIdCursorWhere,\n} from '@shipfox/node-drizzle';\nimport {and, desc, eq, isNull} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport type {AdminGrant} from '#core/entities/admin-grant.js';\nimport type {AdministratorGrantSummary} from '#core/entities/administrator-read-model.js';\nimport {\n AdminBootstrapClosedError,\n AdminGrantAlreadyExistsError,\n AdminGrantNotFoundError,\n LastAdminOwnerError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {\n findAdminCommandResult,\n lockAdminCommand,\n lockAdminOwnerGrants,\n storeAdminCommandResult,\n type Tx,\n writeAdminAction,\n} from './admin-command.js';\nimport {db} from './db.js';\nimport type {StoredAdminGrant} from './schema/admin-command-results.js';\nimport {adminGrants, toAdminGrant} from './schema/admin-grants.js';\nimport {users} from './schema/users.js';\n\ntype AdminOwnerQueryExecutor = ReturnType<typeof db> | Tx;\n\nfunction activeAdminOwnerWhere() {\n return and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n );\n}\n\nasync function listActiveAdminOwners(executor: AdminOwnerQueryExecutor) {\n return await executor\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(activeAdminOwnerWhere())\n .limit(2);\n}\n\nexport interface CreateAdminGrantParams {\n userId: string;\n role: AdminRole;\n}\n\nexport async function createAdminGrant(params: CreateAdminGrantParams): Promise<AdminGrant> {\n const rows = await db()\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Insert returned no rows');\n return toAdminGrant(row);\n}\n\nexport interface AdministratorGrantSummaryRecord {\n id: string;\n role: AdminRole;\n createdAt: Date;\n revokedAt: Date | null;\n user: AdministratorGrantSummary['user'];\n}\n\nexport async function listAdminGrantSummaries(params: {\n limit: number;\n cursor?: TimestampIdCursor;\n}): Promise<{\n rows: AdministratorGrantSummaryRecord[];\n nextCursor: TimestampIdCursor | null;\n}> {\n const cursorCondition = timestampIdCursorWhere({\n timestampColumn: adminGrants.createdAt,\n idColumn: adminGrants.id,\n cursor: params.cursor,\n });\n const conditions = cursorCondition ? [cursorCondition] : [];\n const rows = await db()\n .select({\n id: adminGrants.id,\n role: adminGrants.role,\n createdAt: adminGrants.createdAt,\n revokedAt: adminGrants.revokedAt,\n user: {\n id: users.id,\n email: users.email,\n name: users.name,\n status: users.status,\n },\n })\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(and(...conditions))\n .orderBy(desc(adminGrants.createdAt), desc(adminGrants.id))\n .limit(params.limit + 1);\n\n const page = paginateTimestampIdRows({rows, limit: params.limit, timestampKey: 'createdAt'});\n return {\n rows: page.pageRows,\n nextCursor: page.nextCursor,\n };\n}\n\nexport async function findCurrentAdminRole(params: {userId: string}): Promise<AdminRole | null> {\n const rows = await db()\n .select({role: adminGrants.role})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n\n return highestAdminRole(rows.map(({role}) => role));\n}\n\n/**\n * An owner is active only while both the grant and its user account remain\n * active. A suspended owner's grant is retained, but it intentionally does\n * not prevent deployment-bound bootstrap recovery.\n */\nexport async function hasActiveAdminOwner(\n executor: AdminOwnerQueryExecutor = db(),\n): Promise<boolean> {\n const rows = await listActiveAdminOwners(executor);\n return rows.length > 0;\n}\n\nexport async function revokeAdminGrant(params: {grantId: string}): Promise<AdminGrant | undefined> {\n return await db().transaction(async (tx) => {\n // All owner grant changes share one lock so concurrent revocations cannot\n // both observe the same final owner and leave the instance ownerless.\n await lockAdminOwnerGrants(tx);\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) return undefined;\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await listActiveAdminOwners(tx);\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const row = updated[0];\n return row ? toAdminGrant(row) : undefined;\n });\n}\n\ninterface AuditedAdminCommandParams {\n actorId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nfunction toStoredAdminGrant(grant: AdminGrant): StoredAdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt?.toISOString() ?? null,\n createdAt: grant.createdAt.toISOString(),\n updatedAt: grant.updatedAt.toISOString(),\n };\n}\n\nfunction fromStoredAdminGrant(grant: StoredAdminGrant): AdminGrant {\n return {\n id: grant.id,\n userId: grant.userId,\n role: grant.role,\n revokedAt: grant.revokedAt ? new Date(grant.revokedAt) : null,\n createdAt: new Date(grant.createdAt),\n updatedAt: new Date(grant.updatedAt),\n };\n}\n\nasync function findCommandResult(\n tx: Tx,\n params: Pick<\n AuditedAdminCommandParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {\n command: string;\n },\n): Promise<AdminGrant | undefined> {\n const result = await findAdminCommandResult(tx, params);\n if (!result) return undefined;\n if (!('grant' in result.result)) {\n throw new Error('Administrator command result has an unexpected shape');\n }\n return fromStoredAdminGrant(result.result.grant);\n}\n\nasync function storeCommandResult(\n tx: Tx,\n params: AuditedAdminCommandParams,\n grant: AdminGrant,\n): Promise<void> {\n await storeAdminCommandResult(\n tx,\n {...params, command: params.event.command},\n {grant: toStoredAdminGrant(grant)},\n );\n}\n\nexport async function bootstrapFirstAdminOwner(\n params: AuditedAdminCommandParams & {userId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n if (await hasActiveAdminOwner(tx)) throw new AdminBootstrapClosedError();\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: 'admin-owner'})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Bootstrap grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function grantAdminRoleWithAudit(\n params: AuditedAdminCommandParams & {userId: string; role: AdminRole},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const userRows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, params.userId))\n .limit(1);\n const user = userRows[0];\n if (user?.status !== 'active') throw new UserNotFoundError(params.userId);\n\n const activeRows = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .where(\n and(\n eq(adminGrants.userId, params.userId),\n eq(adminGrants.role, params.role),\n isNull(adminGrants.revokedAt),\n ),\n )\n .limit(1);\n if (activeRows.length > 0) throw new AdminGrantAlreadyExistsError();\n\n const rows = await tx\n .insert(adminGrants)\n .values({userId: params.userId, role: params.role})\n .returning();\n const row = rows[0];\n if (!row) throw new Error('Administrator grant insert returned no rows');\n\n const grant = toAdminGrant(row);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, grant);\n return grant;\n });\n}\n\nexport async function revokeAdminGrantWithAudit(\n params: AuditedAdminCommandParams & {grantId: string},\n): Promise<AdminGrant> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n const rows = await tx\n .select()\n .from(adminGrants)\n .where(and(eq(adminGrants.id, params.grantId), isNull(adminGrants.revokedAt)))\n .limit(1);\n const grant = rows[0];\n if (!grant) throw new AdminGrantNotFoundError();\n\n if (grant.role === 'admin-owner') {\n const activeOwners = await listActiveAdminOwners(tx);\n if (activeOwners.length <= 1) throw new LastAdminOwnerError();\n }\n\n const updated = await tx\n .update(adminGrants)\n .set({revokedAt: new Date(), updatedAt: new Date()})\n .where(and(eq(adminGrants.id, grant.id), isNull(adminGrants.revokedAt)))\n .returning();\n const updatedRow = updated[0];\n if (!updatedRow) throw new AdminGrantNotFoundError();\n\n const revoked = toAdminGrant(updatedRow);\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, revoked);\n return revoked;\n });\n}\n"],"names":["paginateTimestampIdRows","timestampIdCursorWhere","and","desc","eq","isNull","highestAdminRole","AdminBootstrapClosedError","AdminGrantAlreadyExistsError","AdminGrantNotFoundError","LastAdminOwnerError","UserNotFoundError","findAdminCommandResult","lockAdminCommand","lockAdminOwnerGrants","storeAdminCommandResult","writeAdminAction","db","adminGrants","toAdminGrant","users","activeAdminOwnerWhere","role","revokedAt","status","listActiveAdminOwners","executor","select","id","from","innerJoin","userId","where","limit","createAdminGrant","params","rows","insert","values","returning","row","Error","listAdminGrantSummaries","cursorCondition","timestampColumn","createdAt","idColumn","cursor","conditions","user","email","name","orderBy","page","timestampKey","pageRows","nextCursor","findCurrentAdminRole","map","hasActiveAdminOwner","length","revokeAdminGrant","transaction","tx","grantId","grant","undefined","activeOwners","updated","update","set","Date","updatedAt","toStoredAdminGrant","toISOString","fromStoredAdminGrant","findCommandResult","result","storeCommandResult","command","event","bootstrapFirstAdminOwner","existing","userRows","grantAdminRoleWithAudit","activeRows","revokeAdminGrantWithAudit","updatedRow","revoked"],"mappings":"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"}
@@ -1,4 +1,6 @@
1
+ import type { AdminRole } from '@shipfox/api-auth-dto';
1
2
  import type { AdministrationActionEvent } from '@shipfox/api-common-dto';
3
+ import { type Tx } from './admin-command.js';
2
4
  interface UserModerationCommandParams {
3
5
  actorId: string;
4
6
  userId: string;
@@ -6,6 +8,7 @@ interface UserModerationCommandParams {
6
8
  requestFingerprint: string;
7
9
  event: AdministrationActionEvent;
8
10
  }
11
+ export declare function requireActiveAdminOperator(tx: Tx, actorId: string): Promise<AdminRole>;
9
12
  declare function executeUserModerationCommand(params: UserModerationCommandParams & {
10
13
  operation: 'suspend' | 'reactivate' | 'revoke-sessions';
11
14
  }): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"admin-user-moderation.d.ts","sourceRoot":"","sources":["../../src/db/admin-user-moderation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,yBAAyB,CAAC;AAwBvE,UAAU,2BAA2B;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAyGD,iBAAe,4BAA4B,CACzC,MAAM,EAAE,2BAA2B,GAAG;IACpC,SAAS,EAAE,SAAS,GAAG,YAAY,GAAG,iBAAiB,CAAC;CACzD;;QArFG,EAAE;QACF,KAAK;QACL,IAAI;QACJ,eAAe;QACf,MAAM;QACN,SAAS;QACT,SAAS;;;;GAsJd;AAED,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAC,CAAC;AAE5F,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAE/B"}
1
+ {"version":3,"file":"admin-user-moderation.d.ts","sourceRoot":"","sources":["../../src/db/admin-user-moderation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,yBAAyB,CAAC;AAKvE,OAAO,EAKL,KAAK,EAAE,EAER,MAAM,oBAAoB,CAAC;AAY5B,UAAU,2BAA2B;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,yBAAyB,CAAC;CAClC;AA2ED,wBAAsB,0BAA0B,CAAC,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CAiB5F;AAkBD,iBAAe,4BAA4B,CACzC,MAAM,EAAE,2BAA2B,GAAG;IACpC,SAAS,EAAE,SAAS,GAAG,YAAY,GAAG,iBAAiB,CAAC;CACzD;;QA1FG,EAAE;QACF,KAAK;QACL,IAAI;QACJ,eAAe;QACf,MAAM;QACN,SAAS;QACT,SAAS;;;;GA2Jd;AAED,MAAM,MAAM,oBAAoB,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,4BAA4B,CAAC,CAAC,CAAC;AAE5F,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAE/B;AAED,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,oBAAoB,CAAC,CAE/B"}
@@ -50,7 +50,10 @@ async function findCommandResult(tx, params) {
50
50
  return fromStoredResult(result.result.userModeration);
51
51
  }
52
52
  async function storeCommandResult(tx, params, result) {
53
- await storeAdminCommandResult(tx, params, {
53
+ await storeAdminCommandResult(tx, {
54
+ ...params,
55
+ command: params.event.command
56
+ }, {
54
57
  userModeration: result
55
58
  });
56
59
  }
@@ -63,7 +66,7 @@ async function readTargetUserForUpdate(tx, userId) {
63
66
  if (!user || user.status === 'deleted') throw new UserNotFoundError(userId);
64
67
  return user;
65
68
  }
66
- async function requireActiveAdminOperator(tx, actorId) {
69
+ export async function requireActiveAdminOperator(tx, actorId) {
67
70
  const actorRows = await tx.select({
68
71
  status: users.status
69
72
  }).from(users).where(eq(users.id, actorId)).limit(1);
@@ -75,6 +78,7 @@ async function requireActiveAdminOperator(tx, actorId) {
75
78
  if (actor?.status !== 'active' || !role || !hasMinimumAdminRole(role, 'admin-operator')) {
76
79
  throw new AdminRoleRequiredError('admin-operator');
77
80
  }
81
+ return role;
78
82
  }
79
83
  async function revokeActiveSessions(tx, userId) {
80
84
  const revoked = await tx.update(refreshTokens).set({
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/admin-user-moderation.ts"],"sourcesContent":["import type {AdministrationActionEvent} from '@shipfox/api-common-dto';\nimport {and, eq, gt, isNull, sql} from 'drizzle-orm';\nimport {hasMinimumAdminRole, highestAdminRole} from '#core/admin-role-model.js';\nimport type {AdministratorUserSummary} from '#core/entities/administrator-read-model.js';\nimport {AdminRoleRequiredError, LastAdminOwnerError, UserNotFoundError} from '#core/errors.js';\nimport {\n findAdminCommandResult,\n lockAdminCommand,\n lockAdminOwnerGrants,\n storeAdminCommandResult,\n type Tx,\n writeAdminAction,\n} from './admin-command.js';\nimport {findAdministratorUserSummary} from './admin-user-summary.js';\nimport {db} from './db.js';\nimport {lockUserSessionMutations} from './refresh-tokens.js';\nimport type {\n StoredAdministratorUserSummary,\n StoredAdminUserModerationResult,\n} from './schema/admin-command-results.js';\nimport {adminGrants} from './schema/admin-grants.js';\nimport {refreshTokens} from './schema/refresh-tokens.js';\nimport {users} from './schema/users.js';\n\ninterface UserModerationCommandParams {\n actorId: string;\n userId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nfunction toStoredUserSummary(user: AdministratorUserSummary): StoredAdministratorUserSummary {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n emailVerifiedAt: user.emailVerifiedAt?.toISOString() ?? null,\n status: user.status,\n createdAt: user.createdAt.toISOString(),\n adminRole: user.adminRole,\n };\n}\n\nasync function findUserSummary(tx: Tx, userId: string): Promise<StoredAdministratorUserSummary> {\n const user = await findAdministratorUserSummary(tx, {id: userId});\n if (!user) throw new Error('User summary query returned no rows');\n return toStoredUserSummary(user);\n}\n\nfunction fromStoredResult(result: StoredAdminUserModerationResult) {\n return {\n user: {\n id: result.user.id,\n email: result.user.email,\n name: result.user.name,\n emailVerifiedAt: result.user.emailVerifiedAt ? new Date(result.user.emailVerifiedAt) : null,\n status: result.user.status,\n createdAt: new Date(result.user.createdAt),\n adminRole: result.user.adminRole,\n },\n correlationId: result.correlationId,\n sessionsRevoked: result.sessionsRevoked,\n };\n}\n\nasync function findCommandResult(\n tx: Tx,\n params: Pick<\n UserModerationCommandParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {command: string},\n) {\n const result = await findAdminCommandResult(tx, params);\n if (!result) return undefined;\n if (!('userModeration' in result.result)) {\n throw new Error('Administrator command result has an unexpected shape');\n }\n return fromStoredResult(result.result.userModeration);\n}\n\nasync function storeCommandResult(\n tx: Tx,\n params: UserModerationCommandParams,\n result: StoredAdminUserModerationResult,\n): Promise<void> {\n await storeAdminCommandResult(tx, params, {userModeration: result});\n}\n\nasync function readTargetUserForUpdate(tx: Tx, userId: string) {\n const rows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, userId))\n .limit(1)\n .for('update');\n const user = rows[0];\n if (!user || user.status === 'deleted') throw new UserNotFoundError(userId);\n return user;\n}\n\nasync function requireActiveAdminOperator(tx: Tx, actorId: string): Promise<void> {\n const actorRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, actorId))\n .limit(1);\n const actor = actorRows[0];\n const grants = await tx\n .select({role: adminGrants.role})\n .from(adminGrants)\n .where(and(eq(adminGrants.userId, actorId), isNull(adminGrants.revokedAt)));\n const role = highestAdminRole(grants.map(({role}) => role));\n\n if (actor?.status !== 'active' || !role || !hasMinimumAdminRole(role, 'admin-operator')) {\n throw new AdminRoleRequiredError('admin-operator');\n }\n}\n\nasync function revokeActiveSessions(tx: Tx, userId: string): Promise<number> {\n const revoked = await tx\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.userId, userId),\n isNull(refreshTokens.revokedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .returning({sessionId: refreshTokens.sessionId});\n\n return new Set(revoked.map(({sessionId}) => sessionId)).size;\n}\n\nasync function executeUserModerationCommand(\n params: UserModerationCommandParams & {\n operation: 'suspend' | 'reactivate' | 'revoke-sessions';\n },\n) {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n if (params.operation === 'suspend') await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n await lockUserSessionMutations(tx, params.userId);\n const user = await readTargetUserForUpdate(tx, params.userId);\n await requireActiveAdminOperator(tx, params.actorId);\n let sessionsRevoked = 0;\n\n if (params.operation === 'suspend') {\n if (user.status === 'active') {\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n )\n .limit(2);\n const targetIsActiveOwner = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .where(\n and(\n eq(adminGrants.userId, user.id),\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n ),\n )\n .limit(1);\n if (targetIsActiveOwner.length > 0 && activeOwners.length <= 1) {\n throw new LastAdminOwnerError();\n }\n\n await tx\n .update(users)\n .set({status: 'suspended', updatedAt: sql`now()`})\n .where(eq(users.id, user.id));\n }\n sessionsRevoked = await revokeActiveSessions(tx, user.id);\n } else if (params.operation === 'reactivate' && user.status === 'suspended') {\n await tx\n .update(users)\n .set({status: 'active', updatedAt: sql`now()`})\n .where(eq(users.id, user.id));\n } else if (params.operation === 'revoke-sessions') {\n sessionsRevoked = await revokeActiveSessions(tx, user.id);\n }\n\n const summary = await findUserSummary(tx, user.id);\n const result: StoredAdminUserModerationResult = {\n user: summary,\n correlationId: params.event.correlationId,\n sessionsRevoked,\n };\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, result);\n return fromStoredResult(result);\n });\n}\n\nexport type UserModerationResult = Awaited<ReturnType<typeof executeUserModerationCommand>>;\n\nexport async function suspendUserWithAudit(\n params: UserModerationCommandParams,\n): Promise<UserModerationResult> {\n return await executeUserModerationCommand({...params, operation: 'suspend'});\n}\n\nexport async function reactivateUserWithAudit(\n params: UserModerationCommandParams,\n): Promise<UserModerationResult> {\n return await executeUserModerationCommand({...params, operation: 'reactivate'});\n}\n\nexport async function revokeUserSessionsWithAudit(\n params: UserModerationCommandParams,\n): Promise<UserModerationResult> {\n return await executeUserModerationCommand({...params, operation: 'revoke-sessions'});\n}\n"],"names":["and","eq","gt","isNull","sql","hasMinimumAdminRole","highestAdminRole","AdminRoleRequiredError","LastAdminOwnerError","UserNotFoundError","findAdminCommandResult","lockAdminCommand","lockAdminOwnerGrants","storeAdminCommandResult","writeAdminAction","findAdministratorUserSummary","db","lockUserSessionMutations","adminGrants","refreshTokens","users","toStoredUserSummary","user","id","email","name","emailVerifiedAt","toISOString","status","createdAt","adminRole","findUserSummary","tx","userId","Error","fromStoredResult","result","Date","correlationId","sessionsRevoked","findCommandResult","params","undefined","userModeration","storeCommandResult","readTargetUserForUpdate","rows","select","from","where","limit","for","requireActiveAdminOperator","actorId","actorRows","actor","grants","role","revokedAt","map","revokeActiveSessions","revoked","update","set","updatedAt","expiresAt","returning","sessionId","Set","size","executeUserModerationCommand","transaction","operation","existing","command","event","activeOwners","innerJoin","targetIsActiveOwner","length","summary","suspendUserWithAudit","reactivateUserWithAudit","revokeUserSessionsWithAudit"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/db/admin-user-moderation.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport type {AdministrationActionEvent} from '@shipfox/api-common-dto';\nimport {and, eq, gt, isNull, sql} from 'drizzle-orm';\nimport {hasMinimumAdminRole, highestAdminRole} from '#core/admin-role-model.js';\nimport type {AdministratorUserSummary} from '#core/entities/administrator-read-model.js';\nimport {AdminRoleRequiredError, LastAdminOwnerError, UserNotFoundError} from '#core/errors.js';\nimport {\n findAdminCommandResult,\n lockAdminCommand,\n lockAdminOwnerGrants,\n storeAdminCommandResult,\n type Tx,\n writeAdminAction,\n} from './admin-command.js';\nimport {findAdministratorUserSummary} from './admin-user-summary.js';\nimport {db} from './db.js';\nimport {lockUserSessionMutations} from './refresh-tokens.js';\nimport type {\n StoredAdministratorUserSummary,\n StoredAdminUserModerationResult,\n} from './schema/admin-command-results.js';\nimport {adminGrants} from './schema/admin-grants.js';\nimport {refreshTokens} from './schema/refresh-tokens.js';\nimport {users} from './schema/users.js';\n\ninterface UserModerationCommandParams {\n actorId: string;\n userId: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n event: AdministrationActionEvent;\n}\n\nfunction toStoredUserSummary(user: AdministratorUserSummary): StoredAdministratorUserSummary {\n return {\n id: user.id,\n email: user.email,\n name: user.name,\n emailVerifiedAt: user.emailVerifiedAt?.toISOString() ?? null,\n status: user.status,\n createdAt: user.createdAt.toISOString(),\n adminRole: user.adminRole,\n };\n}\n\nasync function findUserSummary(tx: Tx, userId: string): Promise<StoredAdministratorUserSummary> {\n const user = await findAdministratorUserSummary(tx, {id: userId});\n if (!user) throw new Error('User summary query returned no rows');\n return toStoredUserSummary(user);\n}\n\nfunction fromStoredResult(result: StoredAdminUserModerationResult) {\n return {\n user: {\n id: result.user.id,\n email: result.user.email,\n name: result.user.name,\n emailVerifiedAt: result.user.emailVerifiedAt ? new Date(result.user.emailVerifiedAt) : null,\n status: result.user.status,\n createdAt: new Date(result.user.createdAt),\n adminRole: result.user.adminRole,\n },\n correlationId: result.correlationId,\n sessionsRevoked: result.sessionsRevoked,\n };\n}\n\nasync function findCommandResult(\n tx: Tx,\n params: Pick<\n UserModerationCommandParams,\n 'actorId' | 'idempotencyKeyFingerprint' | 'requestFingerprint'\n > & {command: string},\n) {\n const result = await findAdminCommandResult(tx, params);\n if (!result) return undefined;\n if (!('userModeration' in result.result)) {\n throw new Error('Administrator command result has an unexpected shape');\n }\n return fromStoredResult(result.result.userModeration);\n}\n\nasync function storeCommandResult(\n tx: Tx,\n params: UserModerationCommandParams,\n result: StoredAdminUserModerationResult,\n): Promise<void> {\n await storeAdminCommandResult(\n tx,\n {...params, command: params.event.command},\n {userModeration: result},\n );\n}\n\nasync function readTargetUserForUpdate(tx: Tx, userId: string) {\n const rows = await tx\n .select({id: users.id, status: users.status})\n .from(users)\n .where(eq(users.id, userId))\n .limit(1)\n .for('update');\n const user = rows[0];\n if (!user || user.status === 'deleted') throw new UserNotFoundError(userId);\n return user;\n}\n\nexport async function requireActiveAdminOperator(tx: Tx, actorId: string): Promise<AdminRole> {\n const actorRows = await tx\n .select({status: users.status})\n .from(users)\n .where(eq(users.id, actorId))\n .limit(1);\n const actor = actorRows[0];\n const grants = await tx\n .select({role: adminGrants.role})\n .from(adminGrants)\n .where(and(eq(adminGrants.userId, actorId), isNull(adminGrants.revokedAt)));\n const role = highestAdminRole(grants.map(({role}) => role));\n\n if (actor?.status !== 'active' || !role || !hasMinimumAdminRole(role, 'admin-operator')) {\n throw new AdminRoleRequiredError('admin-operator');\n }\n return role;\n}\n\nasync function revokeActiveSessions(tx: Tx, userId: string): Promise<number> {\n const revoked = await tx\n .update(refreshTokens)\n .set({revokedAt: sql`now()`, updatedAt: sql`now()`})\n .where(\n and(\n eq(refreshTokens.userId, userId),\n isNull(refreshTokens.revokedAt),\n gt(refreshTokens.expiresAt, sql`now()`),\n ),\n )\n .returning({sessionId: refreshTokens.sessionId});\n\n return new Set(revoked.map(({sessionId}) => sessionId)).size;\n}\n\nasync function executeUserModerationCommand(\n params: UserModerationCommandParams & {\n operation: 'suspend' | 'reactivate' | 'revoke-sessions';\n },\n) {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n if (params.operation === 'suspend') await lockAdminOwnerGrants(tx);\n\n const existing = await findCommandResult(tx, {\n ...params,\n command: params.event.command,\n });\n if (existing) return existing;\n\n await lockUserSessionMutations(tx, params.userId);\n const user = await readTargetUserForUpdate(tx, params.userId);\n await requireActiveAdminOperator(tx, params.actorId);\n let sessionsRevoked = 0;\n\n if (params.operation === 'suspend') {\n if (user.status === 'active') {\n const activeOwners = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n )\n .limit(2);\n const targetIsActiveOwner = await tx\n .select({id: adminGrants.id})\n .from(adminGrants)\n .where(\n and(\n eq(adminGrants.userId, user.id),\n eq(adminGrants.role, 'admin-owner'),\n isNull(adminGrants.revokedAt),\n ),\n )\n .limit(1);\n if (targetIsActiveOwner.length > 0 && activeOwners.length <= 1) {\n throw new LastAdminOwnerError();\n }\n\n await tx\n .update(users)\n .set({status: 'suspended', updatedAt: sql`now()`})\n .where(eq(users.id, user.id));\n }\n sessionsRevoked = await revokeActiveSessions(tx, user.id);\n } else if (params.operation === 'reactivate' && user.status === 'suspended') {\n await tx\n .update(users)\n .set({status: 'active', updatedAt: sql`now()`})\n .where(eq(users.id, user.id));\n } else if (params.operation === 'revoke-sessions') {\n sessionsRevoked = await revokeActiveSessions(tx, user.id);\n }\n\n const summary = await findUserSummary(tx, user.id);\n const result: StoredAdminUserModerationResult = {\n user: summary,\n correlationId: params.event.correlationId,\n sessionsRevoked,\n };\n await writeAdminAction(tx, params.event);\n await storeCommandResult(tx, params, result);\n return fromStoredResult(result);\n });\n}\n\nexport type UserModerationResult = Awaited<ReturnType<typeof executeUserModerationCommand>>;\n\nexport async function suspendUserWithAudit(\n params: UserModerationCommandParams,\n): Promise<UserModerationResult> {\n return await executeUserModerationCommand({...params, operation: 'suspend'});\n}\n\nexport async function reactivateUserWithAudit(\n params: UserModerationCommandParams,\n): Promise<UserModerationResult> {\n return await executeUserModerationCommand({...params, operation: 'reactivate'});\n}\n\nexport async function revokeUserSessionsWithAudit(\n params: UserModerationCommandParams,\n): Promise<UserModerationResult> {\n return await executeUserModerationCommand({...params, operation: 'revoke-sessions'});\n}\n"],"names":["and","eq","gt","isNull","sql","hasMinimumAdminRole","highestAdminRole","AdminRoleRequiredError","LastAdminOwnerError","UserNotFoundError","findAdminCommandResult","lockAdminCommand","lockAdminOwnerGrants","storeAdminCommandResult","writeAdminAction","findAdministratorUserSummary","db","lockUserSessionMutations","adminGrants","refreshTokens","users","toStoredUserSummary","user","id","email","name","emailVerifiedAt","toISOString","status","createdAt","adminRole","findUserSummary","tx","userId","Error","fromStoredResult","result","Date","correlationId","sessionsRevoked","findCommandResult","params","undefined","userModeration","storeCommandResult","command","event","readTargetUserForUpdate","rows","select","from","where","limit","for","requireActiveAdminOperator","actorId","actorRows","actor","grants","role","revokedAt","map","revokeActiveSessions","revoked","update","set","updatedAt","expiresAt","returning","sessionId","Set","size","executeUserModerationCommand","transaction","operation","existing","activeOwners","innerJoin","targetIsActiveOwner","length","summary","suspendUserWithAudit","reactivateUserWithAudit","revokeUserSessionsWithAudit"],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { AdminRole } from '@shipfox/api-auth-dto';
2
+ import type { WorkspacesInterModuleClient } from '@shipfox/api-workspaces-dto/inter-module';
3
+ import { type CreateImpersonatedSessionTokenResult } from '#core/auth.js';
4
+ export declare const IMPERSONATE_COMMAND = "auth.user.impersonate";
5
+ export interface ImpersonationCommandParams {
6
+ actorId: string;
7
+ targetUserId: string;
8
+ reason: string;
9
+ idempotencyKeyFingerprint: string;
10
+ requestFingerprint: string;
11
+ correlationId: string;
12
+ workspaces: WorkspacesInterModuleClient;
13
+ }
14
+ export interface ImpersonationResult {
15
+ token: string;
16
+ expiresAt: Date;
17
+ user: CreateImpersonatedSessionTokenResult['user'];
18
+ impersonatorId: string;
19
+ correlationId: string;
20
+ }
21
+ /**
22
+ * True when the current invocation's mint committed: a `succeeded` event
23
+ * carrying this invocation's `correlationId` is durable under the idempotency
24
+ * key. The event is written atomically with the result row, and the
25
+ * correlationId is unique per request, so a result row committed by an earlier
26
+ * mint or replay under the same key can never be mistaken for this
27
+ * invocation's commit. The command entry reconciles against this before
28
+ * publishing a `failed` event for an unexpected error: if the event exists,
29
+ * the mint transaction committed (the driver may still have raised on the
30
+ * COMMIT acknowledgement), so a failure event would contradict the durable
31
+ * `succeeded` trail.
32
+ */
33
+ export declare function impersonationSucceededEventExists(params: {
34
+ actorId: string;
35
+ idempotencyKeyFingerprint: string;
36
+ correlationId: string;
37
+ }): Promise<boolean>;
38
+ export declare function impersonateUserWithAudit(params: ImpersonationCommandParams): Promise<ImpersonationResult>;
39
+ /**
40
+ * Publishes a `failed` administration event from its own committed
41
+ * transaction. The main command transaction rolls back on failure and the role
42
+ * check runs before it opens, so an event written inside it would disappear;
43
+ * without this a denied attempt leaves no trace on the one route where failed
44
+ * attempts matter most. The strict event schema requires an actor role, so a
45
+ * role-less actor's denial (nothing but the role gate itself) is not recorded.
46
+ */
47
+ export declare function publishImpersonationFailure(params: {
48
+ actorId: string;
49
+ targetUserId: string;
50
+ reason: string;
51
+ actorRole: AdminRole | null;
52
+ idempotencyKeyFingerprint: string;
53
+ correlationId: string;
54
+ }): Promise<void>;
55
+ //# sourceMappingURL=impersonation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"impersonation.d.ts","sourceRoot":"","sources":["../../src/db/impersonation.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAOrD,OAAO,KAAK,EAAC,2BAA2B,EAAC,MAAM,0CAA0C,CAAC;AAI1F,OAAO,EACL,KAAK,oCAAoC,EAE1C,MAAM,eAAe,CAAC;AAwBvB,eAAO,MAAM,mBAAmB,0BAA0B,CAAC;AAG3D,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,yBAAyB,EAAE,MAAM,CAAC;IAClC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,2BAA2B,CAAC;CACzC;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,IAAI,CAAC;IAChB,IAAI,EAAE,oCAAoC,CAAC,MAAM,CAAC,CAAC;IACnD,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;CACvB;AAmGD;;;;;;;;;;;GAWG;AACH,wBAAsB,iCAAiC,CAAC,MAAM,EAAE;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,yBAAyB,EAAE,MAAM,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,OAAO,CAAC,CAgBnB;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA6F9B;AAED;;;;;;;GAOG;AACH,wBAAsB,2BAA2B,CAAC,MAAM,EAAE;IACxD,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC;IAC5B,yBAAyB,EAAE,MAAM,CAAC;IAClC,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBhB"}
@@ -0,0 +1,212 @@
1
+ import { ADMINISTRATION_ACTION_PERFORMED, createAdministrationActionEvent } from '@shipfox/api-common-dto';
2
+ import { hashOpaqueToken } from '@shipfox/node-tokens';
3
+ import { and, eq, isNull, sql } from 'drizzle-orm';
4
+ import { highestAdminRole } from '#core/admin-role-model.js';
5
+ import { createImpersonatedSessionToken } from '#core/auth.js';
6
+ import { CannotImpersonateAdministratorError, CannotImpersonateSelfError, ImpersonationExpiredError, ImpersonationTargetNotActiveError, UserNotFoundError } from '#core/errors.js';
7
+ import { findAdminCommandResult, lockAdminCommand, lockAdminOwnerGrants, storeAdminCommandResult, updateAdminCommandResult, writeAdminAction } from './admin-command.js';
8
+ import { requireActiveAdminOperator } from './admin-user-moderation.js';
9
+ import { db } from './db.js';
10
+ import { adminGrants } from './schema/admin-grants.js';
11
+ import { authOutbox } from './schema/outbox.js';
12
+ import { users } from './schema/users.js';
13
+ export const IMPERSONATE_COMMAND = 'auth.user.impersonate';
14
+ const IMPERSONATE_REQUIRED_ROLE = 'admin-operator';
15
+ function impersonationEvent(params, result) {
16
+ return createAdministrationActionEvent({
17
+ actorId: params.actorId,
18
+ actorRole: params.actorRole,
19
+ requiredRole: IMPERSONATE_REQUIRED_ROLE,
20
+ command: IMPERSONATE_COMMAND,
21
+ targetType: 'user',
22
+ targetId: params.targetUserId,
23
+ reason: params.reason,
24
+ result,
25
+ correlationId: params.correlationId,
26
+ idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,
27
+ occurredAt: new Date().toISOString()
28
+ });
29
+ }
30
+ function isSameUuid(a, b) {
31
+ return a.toLowerCase() === b.toLowerCase();
32
+ }
33
+ /** Rule 5: the target exists, is active, and has a verified email. */ async function requireEligibleImpersonationTarget(tx, targetUserId) {
34
+ const rows = await tx.select({
35
+ emailVerifiedAt: users.emailVerifiedAt,
36
+ status: users.status
37
+ }).from(users).where(eq(users.id, targetUserId)).limit(1);
38
+ const target = rows[0];
39
+ if (!target || target.status === 'deleted') throw new UserNotFoundError(targetUserId);
40
+ if (target.emailVerifiedAt === null) throw new ImpersonationTargetNotActiveError();
41
+ if (target.status !== 'active') throw new ImpersonationTargetNotActiveError();
42
+ }
43
+ /** Rule 6: the target holds no active administrator grant of any role. */ async function requireNonAdministratorTarget(tx, targetUserId) {
44
+ const grants = await tx.select({
45
+ role: adminGrants.role
46
+ }).from(adminGrants).innerJoin(users, eq(adminGrants.userId, users.id)).where(and(eq(adminGrants.userId, targetUserId), isNull(adminGrants.revokedAt), eq(users.status, 'active')));
47
+ if (highestAdminRole(grants.map(({ role })=>role)) !== null) {
48
+ throw new CannotImpersonateAdministratorError();
49
+ }
50
+ }
51
+ /**
52
+ * The in-transaction authorization and eligibility ladder (rules 2, 4, 5, and
53
+ * 6). Rule 1 (`AUTH_IMPERSONATION_ENABLED`) is a configuration read checked at
54
+ * the command entry and inside the mint primitive, and rule 3 (the actor's own
55
+ * session is not impersonated) is the positional `/admin` route guard; all run
56
+ * on every invocation, replay included. Returns the actor's current role read
57
+ * in this transaction, so the audit event records the role that actually
58
+ * authorized the mint rather than a pre-transaction snapshot.
59
+ */ async function runImpersonationLadder(tx, params) {
60
+ const actorRole = await requireActiveAdminOperator(tx, params.actorId);
61
+ if (isSameUuid(params.targetUserId, params.actorId)) throw new CannotImpersonateSelfError();
62
+ await requireEligibleImpersonationTarget(tx, params.targetUserId);
63
+ await requireNonAdministratorTarget(tx, params.targetUserId);
64
+ return actorRole;
65
+ }
66
+ function toImpersonationResult(params, minted, expiresAt) {
67
+ return {
68
+ token: minted.token,
69
+ expiresAt,
70
+ user: minted.user,
71
+ impersonatorId: params.actorId,
72
+ correlationId: params.correlationId
73
+ };
74
+ }
75
+ /**
76
+ * True when the current invocation's mint committed: a `succeeded` event
77
+ * carrying this invocation's `correlationId` is durable under the idempotency
78
+ * key. The event is written atomically with the result row, and the
79
+ * correlationId is unique per request, so a result row committed by an earlier
80
+ * mint or replay under the same key can never be mistaken for this
81
+ * invocation's commit. The command entry reconciles against this before
82
+ * publishing a `failed` event for an unexpected error: if the event exists,
83
+ * the mint transaction committed (the driver may still have raised on the
84
+ * COMMIT acknowledgement), so a failure event would contradict the durable
85
+ * `succeeded` trail.
86
+ */ export async function impersonationSucceededEventExists(params) {
87
+ const rows = await db().select({
88
+ id: authOutbox.id
89
+ }).from(authOutbox).where(and(eq(authOutbox.eventType, ADMINISTRATION_ACTION_PERFORMED), sql`${authOutbox.payload}->>'command' = ${IMPERSONATE_COMMAND}`, sql`${authOutbox.payload}->>'actorId' = ${params.actorId}`, sql`${authOutbox.payload}->>'idempotencyKeyFingerprint' = ${params.idempotencyKeyFingerprint}`, sql`${authOutbox.payload}->>'correlationId' = ${params.correlationId}`, sql`${authOutbox.payload}->>'result' = 'succeeded'`)).limit(1);
90
+ return rows.length > 0;
91
+ }
92
+ export async function impersonateUserWithAudit(params) {
93
+ return await db().transaction(async (tx)=>{
94
+ await lockAdminCommand(tx, params);
95
+ // Every audited administrator grant mutation (bootstrap, grant, revoke,
96
+ // and suspension) takes this advisory lock, so serializing the ladder and
97
+ // the mint against it closes the race where a concurrent grant mutation
98
+ // changes actor or target eligibility after the ladder read but before
99
+ // the token is signed.
100
+ await lockAdminOwnerGrants(tx);
101
+ const existing = await findAdminCommandResult(tx, {
102
+ actorId: params.actorId,
103
+ idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,
104
+ requestFingerprint: params.requestFingerprint,
105
+ command: IMPERSONATE_COMMAND
106
+ });
107
+ if (existing) {
108
+ if (!('impersonation' in existing.result)) {
109
+ throw new Error('Administrator command result has an unexpected shape');
110
+ }
111
+ const stored = existing.result.impersonation;
112
+ // A replay hands back a usable bearer token, so it is an issuance, not a
113
+ // read: it re-runs the full ladder and re-signs instead of returning the
114
+ // stored result, and a replay after expiry is a terminal failure.
115
+ const storedExpiresAt = new Date(stored.expiresAt);
116
+ if (storedExpiresAt.getTime() <= Date.now()) throw new ImpersonationExpiredError();
117
+ const actorRole = await runImpersonationLadder(tx, {
118
+ actorId: params.actorId,
119
+ targetUserId: stored.targetUserId
120
+ });
121
+ // A replay never extends the window: the re-signed token's TTL is the
122
+ // remaining time to the canonical `expiresAt`, so a sub-second remainder
123
+ // is treated as already-expired instead of flooring up to a new token.
124
+ const remainingSeconds = Math.floor((storedExpiresAt.getTime() - Date.now()) / 1000);
125
+ if (remainingSeconds <= 0) throw new ImpersonationExpiredError();
126
+ const minted = await createImpersonatedSessionToken({
127
+ targetUserId: stored.targetUserId,
128
+ impersonatorId: params.actorId,
129
+ workspaces: params.workspaces,
130
+ expiresIn: `${remainingSeconds}s`
131
+ });
132
+ const fingerprint = hashOpaqueToken(minted.token);
133
+ await updateAdminCommandResult(tx, {
134
+ actorId: params.actorId,
135
+ idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,
136
+ requestFingerprint: params.requestFingerprint
137
+ }, {
138
+ impersonation: {
139
+ ...stored,
140
+ tokenFingerprints: [
141
+ ...stored.tokenFingerprints,
142
+ fingerprint
143
+ ]
144
+ }
145
+ });
146
+ // Replays publish their own event with the same idempotency-key
147
+ // fingerprint: the second event under one fingerprint is the replay
148
+ // marker. It commits atomically with the updated command result.
149
+ await writeAdminAction(tx, impersonationEvent({
150
+ ...params,
151
+ actorRole
152
+ }, 'succeeded'));
153
+ return toImpersonationResult(params, minted, storedExpiresAt);
154
+ }
155
+ const actorRole = await runImpersonationLadder(tx, {
156
+ actorId: params.actorId,
157
+ targetUserId: params.targetUserId
158
+ });
159
+ const minted = await createImpersonatedSessionToken({
160
+ targetUserId: params.targetUserId,
161
+ impersonatorId: params.actorId,
162
+ workspaces: params.workspaces
163
+ });
164
+ const stored = {
165
+ targetUserId: params.targetUserId,
166
+ expiresAt: minted.expiresAt.toISOString(),
167
+ tokenFingerprints: [
168
+ hashOpaqueToken(minted.token)
169
+ ]
170
+ };
171
+ await storeAdminCommandResult(tx, {
172
+ actorId: params.actorId,
173
+ idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,
174
+ requestFingerprint: params.requestFingerprint,
175
+ command: IMPERSONATE_COMMAND
176
+ }, {
177
+ impersonation: stored
178
+ });
179
+ await writeAdminAction(tx, impersonationEvent({
180
+ ...params,
181
+ actorRole
182
+ }, 'succeeded'));
183
+ return toImpersonationResult(params, minted, minted.expiresAt);
184
+ });
185
+ }
186
+ /**
187
+ * Publishes a `failed` administration event from its own committed
188
+ * transaction. The main command transaction rolls back on failure and the role
189
+ * check runs before it opens, so an event written inside it would disappear;
190
+ * without this a denied attempt leaves no trace on the one route where failed
191
+ * attempts matter most. The strict event schema requires an actor role, so a
192
+ * role-less actor's denial (nothing but the role gate itself) is not recorded.
193
+ */ export async function publishImpersonationFailure(params) {
194
+ if (!params.actorRole) return;
195
+ const event = impersonationEvent({
196
+ actorId: params.actorId,
197
+ targetUserId: params.targetUserId,
198
+ reason: params.reason,
199
+ actorRole: params.actorRole,
200
+ idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,
201
+ correlationId: params.correlationId
202
+ }, 'failed');
203
+ try {
204
+ await db().transaction(async (tx)=>{
205
+ await writeAdminAction(tx, event);
206
+ });
207
+ } catch {
208
+ // The failure event must never mask the original command error.
209
+ }
210
+ }
211
+
212
+ //# sourceMappingURL=impersonation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/db/impersonation.ts"],"sourcesContent":["import type {AdminRole} from '@shipfox/api-auth-dto';\nimport {\n ADMINISTRATION_ACTION_PERFORMED,\n type AdministrationActionEvent,\n type AdministrationActionResult,\n createAdministrationActionEvent,\n} from '@shipfox/api-common-dto';\nimport type {WorkspacesInterModuleClient} from '@shipfox/api-workspaces-dto/inter-module';\nimport {hashOpaqueToken} from '@shipfox/node-tokens';\nimport {and, eq, isNull, sql} from 'drizzle-orm';\nimport {highestAdminRole} from '#core/admin-role-model.js';\nimport {\n type CreateImpersonatedSessionTokenResult,\n createImpersonatedSessionToken,\n} from '#core/auth.js';\nimport {\n CannotImpersonateAdministratorError,\n CannotImpersonateSelfError,\n ImpersonationExpiredError,\n ImpersonationTargetNotActiveError,\n UserNotFoundError,\n} from '#core/errors.js';\nimport {\n findAdminCommandResult,\n lockAdminCommand,\n lockAdminOwnerGrants,\n storeAdminCommandResult,\n type Tx,\n updateAdminCommandResult,\n writeAdminAction,\n} from './admin-command.js';\nimport {requireActiveAdminOperator} from './admin-user-moderation.js';\nimport {db} from './db.js';\nimport type {StoredImpersonationResult} from './schema/admin-command-results.js';\nimport {adminGrants} from './schema/admin-grants.js';\nimport {authOutbox} from './schema/outbox.js';\nimport {users} from './schema/users.js';\n\nexport const IMPERSONATE_COMMAND = 'auth.user.impersonate';\nconst IMPERSONATE_REQUIRED_ROLE: AdminRole = 'admin-operator';\n\nexport interface ImpersonationCommandParams {\n actorId: string;\n targetUserId: string;\n reason: string;\n idempotencyKeyFingerprint: string;\n requestFingerprint: string;\n correlationId: string;\n workspaces: WorkspacesInterModuleClient;\n}\n\nexport interface ImpersonationResult {\n token: string;\n expiresAt: Date;\n user: CreateImpersonatedSessionTokenResult['user'];\n impersonatorId: string;\n correlationId: string;\n}\n\ninterface ImpersonationEventFields {\n actorId: string;\n targetUserId: string;\n reason: string;\n actorRole: AdminRole;\n idempotencyKeyFingerprint: string;\n correlationId: string;\n}\n\nfunction impersonationEvent(\n params: ImpersonationEventFields,\n result: AdministrationActionResult,\n): AdministrationActionEvent {\n return createAdministrationActionEvent({\n actorId: params.actorId,\n actorRole: params.actorRole,\n requiredRole: IMPERSONATE_REQUIRED_ROLE,\n command: IMPERSONATE_COMMAND,\n targetType: 'user',\n targetId: params.targetUserId,\n reason: params.reason,\n result,\n correlationId: params.correlationId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n occurredAt: new Date().toISOString(),\n });\n}\n\nfunction isSameUuid(a: string, b: string): boolean {\n return a.toLowerCase() === b.toLowerCase();\n}\n\n/** Rule 5: the target exists, is active, and has a verified email. */\nasync function requireEligibleImpersonationTarget(tx: Tx, targetUserId: string): Promise<void> {\n const rows = await tx\n .select({emailVerifiedAt: users.emailVerifiedAt, status: users.status})\n .from(users)\n .where(eq(users.id, targetUserId))\n .limit(1);\n const target = rows[0];\n if (!target || target.status === 'deleted') throw new UserNotFoundError(targetUserId);\n if (target.emailVerifiedAt === null) throw new ImpersonationTargetNotActiveError();\n if (target.status !== 'active') throw new ImpersonationTargetNotActiveError();\n}\n\n/** Rule 6: the target holds no active administrator grant of any role. */\nasync function requireNonAdministratorTarget(tx: Tx, targetUserId: string): Promise<void> {\n const grants = await tx\n .select({role: adminGrants.role})\n .from(adminGrants)\n .innerJoin(users, eq(adminGrants.userId, users.id))\n .where(\n and(\n eq(adminGrants.userId, targetUserId),\n isNull(adminGrants.revokedAt),\n eq(users.status, 'active'),\n ),\n );\n if (highestAdminRole(grants.map(({role}) => role)) !== null) {\n throw new CannotImpersonateAdministratorError();\n }\n}\n\n/**\n * The in-transaction authorization and eligibility ladder (rules 2, 4, 5, and\n * 6). Rule 1 (`AUTH_IMPERSONATION_ENABLED`) is a configuration read checked at\n * the command entry and inside the mint primitive, and rule 3 (the actor's own\n * session is not impersonated) is the positional `/admin` route guard; all run\n * on every invocation, replay included. Returns the actor's current role read\n * in this transaction, so the audit event records the role that actually\n * authorized the mint rather than a pre-transaction snapshot.\n */\nasync function runImpersonationLadder(\n tx: Tx,\n params: {actorId: string; targetUserId: string},\n): Promise<AdminRole> {\n const actorRole = await requireActiveAdminOperator(tx, params.actorId);\n if (isSameUuid(params.targetUserId, params.actorId)) throw new CannotImpersonateSelfError();\n await requireEligibleImpersonationTarget(tx, params.targetUserId);\n await requireNonAdministratorTarget(tx, params.targetUserId);\n return actorRole;\n}\n\nfunction toImpersonationResult(\n params: ImpersonationCommandParams,\n minted: CreateImpersonatedSessionTokenResult,\n expiresAt: Date,\n): ImpersonationResult {\n return {\n token: minted.token,\n expiresAt,\n user: minted.user,\n impersonatorId: params.actorId,\n correlationId: params.correlationId,\n };\n}\n\n/**\n * True when the current invocation's mint committed: a `succeeded` event\n * carrying this invocation's `correlationId` is durable under the idempotency\n * key. The event is written atomically with the result row, and the\n * correlationId is unique per request, so a result row committed by an earlier\n * mint or replay under the same key can never be mistaken for this\n * invocation's commit. The command entry reconciles against this before\n * publishing a `failed` event for an unexpected error: if the event exists,\n * the mint transaction committed (the driver may still have raised on the\n * COMMIT acknowledgement), so a failure event would contradict the durable\n * `succeeded` trail.\n */\nexport async function impersonationSucceededEventExists(params: {\n actorId: string;\n idempotencyKeyFingerprint: string;\n correlationId: string;\n}): Promise<boolean> {\n const rows = await db()\n .select({id: authOutbox.id})\n .from(authOutbox)\n .where(\n and(\n eq(authOutbox.eventType, ADMINISTRATION_ACTION_PERFORMED),\n sql`${authOutbox.payload}->>'command' = ${IMPERSONATE_COMMAND}`,\n sql`${authOutbox.payload}->>'actorId' = ${params.actorId}`,\n sql`${authOutbox.payload}->>'idempotencyKeyFingerprint' = ${params.idempotencyKeyFingerprint}`,\n sql`${authOutbox.payload}->>'correlationId' = ${params.correlationId}`,\n sql`${authOutbox.payload}->>'result' = 'succeeded'`,\n ),\n )\n .limit(1);\n return rows.length > 0;\n}\n\nexport async function impersonateUserWithAudit(\n params: ImpersonationCommandParams,\n): Promise<ImpersonationResult> {\n return await db().transaction(async (tx) => {\n await lockAdminCommand(tx, params);\n // Every audited administrator grant mutation (bootstrap, grant, revoke,\n // and suspension) takes this advisory lock, so serializing the ladder and\n // the mint against it closes the race where a concurrent grant mutation\n // changes actor or target eligibility after the ladder read but before\n // the token is signed.\n await lockAdminOwnerGrants(tx);\n\n const existing = await findAdminCommandResult(tx, {\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n requestFingerprint: params.requestFingerprint,\n command: IMPERSONATE_COMMAND,\n });\n\n if (existing) {\n if (!('impersonation' in existing.result)) {\n throw new Error('Administrator command result has an unexpected shape');\n }\n const stored = existing.result.impersonation as StoredImpersonationResult;\n\n // A replay hands back a usable bearer token, so it is an issuance, not a\n // read: it re-runs the full ladder and re-signs instead of returning the\n // stored result, and a replay after expiry is a terminal failure.\n const storedExpiresAt = new Date(stored.expiresAt);\n if (storedExpiresAt.getTime() <= Date.now()) throw new ImpersonationExpiredError();\n const actorRole = await runImpersonationLadder(tx, {\n actorId: params.actorId,\n targetUserId: stored.targetUserId,\n });\n\n // A replay never extends the window: the re-signed token's TTL is the\n // remaining time to the canonical `expiresAt`, so a sub-second remainder\n // is treated as already-expired instead of flooring up to a new token.\n const remainingSeconds = Math.floor((storedExpiresAt.getTime() - Date.now()) / 1000);\n if (remainingSeconds <= 0) throw new ImpersonationExpiredError();\n const minted = await createImpersonatedSessionToken({\n targetUserId: stored.targetUserId,\n impersonatorId: params.actorId,\n workspaces: params.workspaces,\n expiresIn: `${remainingSeconds}s`,\n });\n const fingerprint = hashOpaqueToken(minted.token);\n await updateAdminCommandResult(\n tx,\n {\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n requestFingerprint: params.requestFingerprint,\n },\n {\n impersonation: {\n ...stored,\n tokenFingerprints: [...stored.tokenFingerprints, fingerprint],\n },\n },\n );\n // Replays publish their own event with the same idempotency-key\n // fingerprint: the second event under one fingerprint is the replay\n // marker. It commits atomically with the updated command result.\n await writeAdminAction(tx, impersonationEvent({...params, actorRole}, 'succeeded'));\n return toImpersonationResult(params, minted, storedExpiresAt);\n }\n\n const actorRole = await runImpersonationLadder(tx, {\n actorId: params.actorId,\n targetUserId: params.targetUserId,\n });\n const minted = await createImpersonatedSessionToken({\n targetUserId: params.targetUserId,\n impersonatorId: params.actorId,\n workspaces: params.workspaces,\n });\n const stored: StoredImpersonationResult = {\n targetUserId: params.targetUserId,\n expiresAt: minted.expiresAt.toISOString(),\n tokenFingerprints: [hashOpaqueToken(minted.token)],\n };\n await storeAdminCommandResult(\n tx,\n {\n actorId: params.actorId,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n requestFingerprint: params.requestFingerprint,\n command: IMPERSONATE_COMMAND,\n },\n {impersonation: stored},\n );\n await writeAdminAction(tx, impersonationEvent({...params, actorRole}, 'succeeded'));\n return toImpersonationResult(params, minted, minted.expiresAt);\n });\n}\n\n/**\n * Publishes a `failed` administration event from its own committed\n * transaction. The main command transaction rolls back on failure and the role\n * check runs before it opens, so an event written inside it would disappear;\n * without this a denied attempt leaves no trace on the one route where failed\n * attempts matter most. The strict event schema requires an actor role, so a\n * role-less actor's denial (nothing but the role gate itself) is not recorded.\n */\nexport async function publishImpersonationFailure(params: {\n actorId: string;\n targetUserId: string;\n reason: string;\n actorRole: AdminRole | null;\n idempotencyKeyFingerprint: string;\n correlationId: string;\n}): Promise<void> {\n if (!params.actorRole) return;\n const event = impersonationEvent(\n {\n actorId: params.actorId,\n targetUserId: params.targetUserId,\n reason: params.reason,\n actorRole: params.actorRole,\n idempotencyKeyFingerprint: params.idempotencyKeyFingerprint,\n correlationId: params.correlationId,\n },\n 'failed',\n );\n try {\n await db().transaction(async (tx) => {\n await writeAdminAction(tx, event);\n });\n } catch {\n // The failure event must never mask the original command error.\n }\n}\n"],"names":["ADMINISTRATION_ACTION_PERFORMED","createAdministrationActionEvent","hashOpaqueToken","and","eq","isNull","sql","highestAdminRole","createImpersonatedSessionToken","CannotImpersonateAdministratorError","CannotImpersonateSelfError","ImpersonationExpiredError","ImpersonationTargetNotActiveError","UserNotFoundError","findAdminCommandResult","lockAdminCommand","lockAdminOwnerGrants","storeAdminCommandResult","updateAdminCommandResult","writeAdminAction","requireActiveAdminOperator","db","adminGrants","authOutbox","users","IMPERSONATE_COMMAND","IMPERSONATE_REQUIRED_ROLE","impersonationEvent","params","result","actorId","actorRole","requiredRole","command","targetType","targetId","targetUserId","reason","correlationId","idempotencyKeyFingerprint","occurredAt","Date","toISOString","isSameUuid","a","b","toLowerCase","requireEligibleImpersonationTarget","tx","rows","select","emailVerifiedAt","status","from","where","id","limit","target","requireNonAdministratorTarget","grants","role","innerJoin","userId","revokedAt","map","runImpersonationLadder","toImpersonationResult","minted","expiresAt","token","user","impersonatorId","impersonationSucceededEventExists","eventType","payload","length","impersonateUserWithAudit","transaction","existing","requestFingerprint","Error","stored","impersonation","storedExpiresAt","getTime","now","remainingSeconds","Math","floor","workspaces","expiresIn","fingerprint","tokenFingerprints","publishImpersonationFailure","event"],"mappings":"AACA,SACEA,+BAA+B,EAG/BC,+BAA+B,QAC1B,0BAA0B;AAEjC,SAAQC,eAAe,QAAO,uBAAuB;AACrD,SAAQC,GAAG,EAAEC,EAAE,EAAEC,MAAM,EAAEC,GAAG,QAAO,cAAc;AACjD,SAAQC,gBAAgB,QAAO,4BAA4B;AAC3D,SAEEC,8BAA8B,QACzB,gBAAgB;AACvB,SACEC,mCAAmC,EACnCC,0BAA0B,EAC1BC,yBAAyB,EACzBC,iCAAiC,EACjCC,iBAAiB,QACZ,kBAAkB;AACzB,SACEC,sBAAsB,EACtBC,gBAAgB,EAChBC,oBAAoB,EACpBC,uBAAuB,EAEvBC,wBAAwB,EACxBC,gBAAgB,QACX,qBAAqB;AAC5B,SAAQC,0BAA0B,QAAO,6BAA6B;AACtE,SAAQC,EAAE,QAAO,UAAU;AAE3B,SAAQC,WAAW,QAAO,2BAA2B;AACrD,SAAQC,UAAU,QAAO,qBAAqB;AAC9C,SAAQC,KAAK,QAAO,oBAAoB;AAExC,OAAO,MAAMC,sBAAsB,wBAAwB;AAC3D,MAAMC,4BAAuC;AA6B7C,SAASC,mBACPC,MAAgC,EAChCC,MAAkC;IAElC,OAAO5B,gCAAgC;QACrC6B,SAASF,OAAOE,OAAO;QACvBC,WAAWH,OAAOG,SAAS;QAC3BC,cAAcN;QACdO,SAASR;QACTS,YAAY;QACZC,UAAUP,OAAOQ,YAAY;QAC7BC,QAAQT,OAAOS,MAAM;QACrBR;QACAS,eAAeV,OAAOU,aAAa;QACnCC,2BAA2BX,OAAOW,yBAAyB;QAC3DC,YAAY,IAAIC,OAAOC,WAAW;IACpC;AACF;AAEA,SAASC,WAAWC,CAAS,EAAEC,CAAS;IACtC,OAAOD,EAAEE,WAAW,OAAOD,EAAEC,WAAW;AAC1C;AAEA,oEAAoE,GACpE,eAAeC,mCAAmCC,EAAM,EAAEZ,YAAoB;IAC5E,MAAMa,OAAO,MAAMD,GAChBE,MAAM,CAAC;QAACC,iBAAiB3B,MAAM2B,eAAe;QAAEC,QAAQ5B,MAAM4B,MAAM;IAAA,GACpEC,IAAI,CAAC7B,OACL8B,KAAK,CAAClD,GAAGoB,MAAM+B,EAAE,EAAEnB,eACnBoB,KAAK,CAAC;IACT,MAAMC,SAASR,IAAI,CAAC,EAAE;IACtB,IAAI,CAACQ,UAAUA,OAAOL,MAAM,KAAK,WAAW,MAAM,IAAIvC,kBAAkBuB;IACxE,IAAIqB,OAAON,eAAe,KAAK,MAAM,MAAM,IAAIvC;IAC/C,IAAI6C,OAAOL,MAAM,KAAK,UAAU,MAAM,IAAIxC;AAC5C;AAEA,wEAAwE,GACxE,eAAe8C,8BAA8BV,EAAM,EAAEZ,YAAoB;IACvE,MAAMuB,SAAS,MAAMX,GAClBE,MAAM,CAAC;QAACU,MAAMtC,YAAYsC,IAAI;IAAA,GAC9BP,IAAI,CAAC/B,aACLuC,SAAS,CAACrC,OAAOpB,GAAGkB,YAAYwC,MAAM,EAAEtC,MAAM+B,EAAE,GAChDD,KAAK,CACJnD,IACEC,GAAGkB,YAAYwC,MAAM,EAAE1B,eACvB/B,OAAOiB,YAAYyC,SAAS,GAC5B3D,GAAGoB,MAAM4B,MAAM,EAAE;IAGvB,IAAI7C,iBAAiBoD,OAAOK,GAAG,CAAC,CAAC,EAACJ,IAAI,EAAC,GAAKA,WAAW,MAAM;QAC3D,MAAM,IAAInD;IACZ;AACF;AAEA;;;;;;;;CAQC,GACD,eAAewD,uBACbjB,EAAM,EACNpB,MAA+C;IAE/C,MAAMG,YAAY,MAAMX,2BAA2B4B,IAAIpB,OAAOE,OAAO;IACrE,IAAIa,WAAWf,OAAOQ,YAAY,EAAER,OAAOE,OAAO,GAAG,MAAM,IAAIpB;IAC/D,MAAMqC,mCAAmCC,IAAIpB,OAAOQ,YAAY;IAChE,MAAMsB,8BAA8BV,IAAIpB,OAAOQ,YAAY;IAC3D,OAAOL;AACT;AAEA,SAASmC,sBACPtC,MAAkC,EAClCuC,MAA4C,EAC5CC,SAAe;IAEf,OAAO;QACLC,OAAOF,OAAOE,KAAK;QACnBD;QACAE,MAAMH,OAAOG,IAAI;QACjBC,gBAAgB3C,OAAOE,OAAO;QAC9BQ,eAAeV,OAAOU,aAAa;IACrC;AACF;AAEA;;;;;;;;;;;CAWC,GACD,OAAO,eAAekC,kCAAkC5C,MAIvD;IACC,MAAMqB,OAAO,MAAM5B,KAChB6B,MAAM,CAAC;QAACK,IAAIhC,WAAWgC,EAAE;IAAA,GACzBF,IAAI,CAAC9B,YACL+B,KAAK,CACJnD,IACEC,GAAGmB,WAAWkD,SAAS,EAAEzE,kCACzBM,GAAG,CAAC,EAAEiB,WAAWmD,OAAO,CAAC,eAAe,EAAEjD,oBAAoB,CAAC,EAC/DnB,GAAG,CAAC,EAAEiB,WAAWmD,OAAO,CAAC,eAAe,EAAE9C,OAAOE,OAAO,CAAC,CAAC,EAC1DxB,GAAG,CAAC,EAAEiB,WAAWmD,OAAO,CAAC,iCAAiC,EAAE9C,OAAOW,yBAAyB,CAAC,CAAC,EAC9FjC,GAAG,CAAC,EAAEiB,WAAWmD,OAAO,CAAC,qBAAqB,EAAE9C,OAAOU,aAAa,CAAC,CAAC,EACtEhC,GAAG,CAAC,EAAEiB,WAAWmD,OAAO,CAAC,yBAAyB,CAAC,GAGtDlB,KAAK,CAAC;IACT,OAAOP,KAAK0B,MAAM,GAAG;AACvB;AAEA,OAAO,eAAeC,yBACpBhD,MAAkC;IAElC,OAAO,MAAMP,KAAKwD,WAAW,CAAC,OAAO7B;QACnC,MAAMjC,iBAAiBiC,IAAIpB;QAC3B,wEAAwE;QACxE,0EAA0E;QAC1E,wEAAwE;QACxE,uEAAuE;QACvE,uBAAuB;QACvB,MAAMZ,qBAAqBgC;QAE3B,MAAM8B,WAAW,MAAMhE,uBAAuBkC,IAAI;YAChDlB,SAASF,OAAOE,OAAO;YACvBS,2BAA2BX,OAAOW,yBAAyB;YAC3DwC,oBAAoBnD,OAAOmD,kBAAkB;YAC7C9C,SAASR;QACX;QAEA,IAAIqD,UAAU;YACZ,IAAI,CAAE,CAAA,mBAAmBA,SAASjD,MAAM,AAAD,GAAI;gBACzC,MAAM,IAAImD,MAAM;YAClB;YACA,MAAMC,SAASH,SAASjD,MAAM,CAACqD,aAAa;YAE5C,yEAAyE;YACzE,yEAAyE;YACzE,kEAAkE;YAClE,MAAMC,kBAAkB,IAAI1C,KAAKwC,OAAOb,SAAS;YACjD,IAAIe,gBAAgBC,OAAO,MAAM3C,KAAK4C,GAAG,IAAI,MAAM,IAAI1E;YACvD,MAAMoB,YAAY,MAAMkC,uBAAuBjB,IAAI;gBACjDlB,SAASF,OAAOE,OAAO;gBACvBM,cAAc6C,OAAO7C,YAAY;YACnC;YAEA,sEAAsE;YACtE,yEAAyE;YACzE,uEAAuE;YACvE,MAAMkD,mBAAmBC,KAAKC,KAAK,CAAC,AAACL,CAAAA,gBAAgBC,OAAO,KAAK3C,KAAK4C,GAAG,EAAC,IAAK;YAC/E,IAAIC,oBAAoB,GAAG,MAAM,IAAI3E;YACrC,MAAMwD,SAAS,MAAM3D,+BAA+B;gBAClD4B,cAAc6C,OAAO7C,YAAY;gBACjCmC,gBAAgB3C,OAAOE,OAAO;gBAC9B2D,YAAY7D,OAAO6D,UAAU;gBAC7BC,WAAW,GAAGJ,iBAAiB,CAAC,CAAC;YACnC;YACA,MAAMK,cAAczF,gBAAgBiE,OAAOE,KAAK;YAChD,MAAMnD,yBACJ8B,IACA;gBACElB,SAASF,OAAOE,OAAO;gBACvBS,2BAA2BX,OAAOW,yBAAyB;gBAC3DwC,oBAAoBnD,OAAOmD,kBAAkB;YAC/C,GACA;gBACEG,eAAe;oBACb,GAAGD,MAAM;oBACTW,mBAAmB;2BAAIX,OAAOW,iBAAiB;wBAAED;qBAAY;gBAC/D;YACF;YAEF,gEAAgE;YAChE,oEAAoE;YACpE,iEAAiE;YACjE,MAAMxE,iBAAiB6B,IAAIrB,mBAAmB;gBAAC,GAAGC,MAAM;gBAAEG;YAAS,GAAG;YACtE,OAAOmC,sBAAsBtC,QAAQuC,QAAQgB;QAC/C;QAEA,MAAMpD,YAAY,MAAMkC,uBAAuBjB,IAAI;YACjDlB,SAASF,OAAOE,OAAO;YACvBM,cAAcR,OAAOQ,YAAY;QACnC;QACA,MAAM+B,SAAS,MAAM3D,+BAA+B;YAClD4B,cAAcR,OAAOQ,YAAY;YACjCmC,gBAAgB3C,OAAOE,OAAO;YAC9B2D,YAAY7D,OAAO6D,UAAU;QAC/B;QACA,MAAMR,SAAoC;YACxC7C,cAAcR,OAAOQ,YAAY;YACjCgC,WAAWD,OAAOC,SAAS,CAAC1B,WAAW;YACvCkD,mBAAmB;gBAAC1F,gBAAgBiE,OAAOE,KAAK;aAAE;QACpD;QACA,MAAMpD,wBACJ+B,IACA;YACElB,SAASF,OAAOE,OAAO;YACvBS,2BAA2BX,OAAOW,yBAAyB;YAC3DwC,oBAAoBnD,OAAOmD,kBAAkB;YAC7C9C,SAASR;QACX,GACA;YAACyD,eAAeD;QAAM;QAExB,MAAM9D,iBAAiB6B,IAAIrB,mBAAmB;YAAC,GAAGC,MAAM;YAAEG;QAAS,GAAG;QACtE,OAAOmC,sBAAsBtC,QAAQuC,QAAQA,OAAOC,SAAS;IAC/D;AACF;AAEA;;;;;;;CAOC,GACD,OAAO,eAAeyB,4BAA4BjE,MAOjD;IACC,IAAI,CAACA,OAAOG,SAAS,EAAE;IACvB,MAAM+D,QAAQnE,mBACZ;QACEG,SAASF,OAAOE,OAAO;QACvBM,cAAcR,OAAOQ,YAAY;QACjCC,QAAQT,OAAOS,MAAM;QACrBN,WAAWH,OAAOG,SAAS;QAC3BQ,2BAA2BX,OAAOW,yBAAyB;QAC3DD,eAAeV,OAAOU,aAAa;IACrC,GACA;IAEF,IAAI;QACF,MAAMjB,KAAKwD,WAAW,CAAC,OAAO7B;YAC5B,MAAM7B,iBAAiB6B,IAAI8C;QAC7B;IACF,EAAE,OAAM;IACN,gEAAgE;IAClE;AACF"}