@aphexcms/cms-core 9.8.1 → 9.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/organizations.d.ts +2 -1
- package/dist/api/organizations.d.ts.map +1 -1
- package/dist/api/organizations.js +2 -1
- package/dist/api/schemas/jobs.d.ts +15 -0
- package/dist/api/schemas/jobs.d.ts.map +1 -0
- package/dist/api/schemas/jobs.js +17 -0
- package/dist/api/schemas/user.d.ts.map +1 -1
- package/dist/api/schemas/user.js +6 -2
- package/dist/auth/account-deletion.d.ts +35 -0
- package/dist/auth/account-deletion.d.ts.map +1 -0
- package/dist/auth/account-deletion.js +65 -0
- package/dist/auth/bootstrap.d.ts +104 -0
- package/dist/auth/bootstrap.d.ts.map +1 -0
- package/dist/auth/bootstrap.js +164 -0
- package/dist/auth/instance-state.d.ts +13 -0
- package/dist/auth/instance-state.d.ts.map +1 -0
- package/dist/auth/instance-state.js +30 -0
- package/dist/auth/invitation-status.d.ts +25 -0
- package/dist/auth/invitation-status.d.ts.map +1 -0
- package/dist/auth/invitation-status.js +41 -0
- package/dist/auth/sign-up-policy.d.ts +62 -0
- package/dist/auth/sign-up-policy.d.ts.map +1 -0
- package/dist/auth/sign-up-policy.js +120 -0
- package/dist/components/admin/ActivityView.svelte +263 -13
- package/dist/components/admin/ActivityView.svelte.d.ts +10 -1
- package/dist/components/admin/ActivityView.svelte.d.ts.map +1 -1
- package/dist/db/interfaces/events.d.ts +29 -2
- package/dist/db/interfaces/events.d.ts.map +1 -1
- package/dist/db/interfaces/index.d.ts +1 -0
- package/dist/db/interfaces/index.d.ts.map +1 -1
- package/dist/db/interfaces/index.js +1 -0
- package/dist/db/interfaces/instance.d.ts +29 -0
- package/dist/db/interfaces/instance.d.ts.map +1 -1
- package/dist/db/interfaces/instance.js +8 -1
- package/dist/events/built-in-consumers.d.ts +20 -0
- package/dist/events/built-in-consumers.d.ts.map +1 -0
- package/dist/events/built-in-consumers.js +49 -0
- package/dist/events/catalog.d.ts +16 -0
- package/dist/events/catalog.d.ts.map +1 -1
- package/dist/events/catalog.js +17 -0
- package/dist/events/consumer.d.ts +14 -0
- package/dist/events/consumer.d.ts.map +1 -1
- package/dist/events/consumer.js +1 -0
- package/dist/events/emit.d.ts +23 -0
- package/dist/events/emit.d.ts.map +1 -1
- package/dist/events/emit.js +32 -1
- package/dist/events/index.d.ts +2 -0
- package/dist/events/index.d.ts.map +1 -1
- package/dist/events/index.js +2 -0
- package/dist/field-validation/utils.d.ts +27 -4
- package/dist/field-validation/utils.d.ts.map +1 -1
- package/dist/field-validation/utils.js +78 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/jobs/run-batch.d.ts +7 -1
- package/dist/jobs/run-batch.d.ts.map +1 -1
- package/dist/jobs/run-batch.js +3 -1
- package/dist/lib/api/organizations.d.ts +2 -1
- package/dist/lib/api/organizations.d.ts.map +1 -1
- package/dist/lib/api/organizations.js +2 -1
- package/dist/lib/api/organizations.js.map +1 -1
- package/dist/lib/api/schemas/api-keys.d.ts +4 -4
- package/dist/lib/api/schemas/jobs.d.ts +15 -0
- package/dist/lib/api/schemas/jobs.d.ts.map +1 -0
- package/dist/lib/api/schemas/jobs.js +18 -0
- package/dist/lib/api/schemas/jobs.js.map +1 -0
- package/dist/lib/api/schemas/user.d.ts.map +1 -1
- package/dist/lib/api/schemas/user.js +6 -2
- package/dist/lib/api/schemas/user.js.map +1 -1
- package/dist/lib/auth/account-deletion.d.ts +35 -0
- package/dist/lib/auth/account-deletion.d.ts.map +1 -0
- package/dist/lib/auth/account-deletion.js +66 -0
- package/dist/lib/auth/account-deletion.js.map +1 -0
- package/dist/lib/auth/bootstrap.d.ts +104 -0
- package/dist/lib/auth/bootstrap.d.ts.map +1 -0
- package/dist/lib/auth/bootstrap.js +165 -0
- package/dist/lib/auth/bootstrap.js.map +1 -0
- package/dist/lib/auth/instance-state.d.ts +13 -0
- package/dist/lib/auth/instance-state.d.ts.map +1 -0
- package/dist/lib/auth/instance-state.js +31 -0
- package/dist/lib/auth/instance-state.js.map +1 -0
- package/dist/lib/auth/invitation-status.d.ts +25 -0
- package/dist/lib/auth/invitation-status.d.ts.map +1 -0
- package/dist/lib/auth/invitation-status.js +42 -0
- package/dist/lib/auth/invitation-status.js.map +1 -0
- package/dist/lib/auth/sign-up-policy.d.ts +62 -0
- package/dist/lib/auth/sign-up-policy.d.ts.map +1 -0
- package/dist/lib/auth/sign-up-policy.js +121 -0
- package/dist/lib/auth/sign-up-policy.js.map +1 -0
- package/dist/lib/db/interfaces/events.d.ts +29 -2
- package/dist/lib/db/interfaces/events.d.ts.map +1 -1
- package/dist/lib/db/interfaces/index.d.ts +1 -0
- package/dist/lib/db/interfaces/index.d.ts.map +1 -1
- package/dist/lib/db/interfaces/index.js +1 -0
- package/dist/lib/db/interfaces/index.js.map +1 -1
- package/dist/lib/db/interfaces/instance.d.ts +29 -0
- package/dist/lib/db/interfaces/instance.d.ts.map +1 -1
- package/dist/lib/db/interfaces/instance.js +8 -1
- package/dist/lib/db/interfaces/instance.js.map +1 -1
- package/dist/lib/events/built-in-consumers.d.ts +20 -0
- package/dist/lib/events/built-in-consumers.d.ts.map +1 -0
- package/dist/lib/events/built-in-consumers.js +50 -0
- package/dist/lib/events/built-in-consumers.js.map +1 -0
- package/dist/lib/events/catalog.d.ts +16 -0
- package/dist/lib/events/catalog.d.ts.map +1 -1
- package/dist/lib/events/catalog.js +17 -0
- package/dist/lib/events/catalog.js.map +1 -1
- package/dist/lib/events/consumer.d.ts +14 -0
- package/dist/lib/events/consumer.d.ts.map +1 -1
- package/dist/lib/events/consumer.js +1 -0
- package/dist/lib/events/consumer.js.map +1 -1
- package/dist/lib/events/emit.d.ts +23 -0
- package/dist/lib/events/emit.d.ts.map +1 -1
- package/dist/lib/events/emit.js +32 -1
- package/dist/lib/events/emit.js.map +1 -1
- package/dist/lib/events/index.d.ts +2 -0
- package/dist/lib/events/index.d.ts.map +1 -1
- package/dist/lib/events/index.js +2 -0
- package/dist/lib/events/index.js.map +1 -1
- package/dist/lib/field-validation/utils.d.ts +27 -4
- package/dist/lib/field-validation/utils.d.ts.map +1 -1
- package/dist/lib/field-validation/utils.js +78 -10
- package/dist/lib/field-validation/utils.js.map +1 -1
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +3 -0
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/jobs/run-batch.d.ts +7 -1
- package/dist/lib/jobs/run-batch.d.ts.map +1 -1
- package/dist/lib/jobs/run-batch.js +3 -1
- package/dist/lib/jobs/run-batch.js.map +1 -1
- package/dist/lib/local-api/collection-api.d.ts +42 -1
- package/dist/lib/local-api/collection-api.d.ts.map +1 -1
- package/dist/lib/local-api/collection-api.js +68 -8
- package/dist/lib/local-api/collection-api.js.map +1 -1
- package/dist/lib/local-api/index.d.ts +1 -1
- package/dist/lib/local-api/index.d.ts.map +1 -1
- package/dist/lib/local-api/index.js +1 -1
- package/dist/lib/local-api/index.js.map +1 -1
- package/dist/lib/plugins/resolver.d.ts.map +1 -1
- package/dist/lib/plugins/resolver.js +7 -1
- package/dist/lib/plugins/resolver.js.map +1 -1
- package/dist/lib/server/api/rate-limit.d.ts +38 -0
- package/dist/lib/server/api/rate-limit.d.ts.map +1 -0
- package/dist/lib/server/api/rate-limit.js +71 -0
- package/dist/lib/server/api/rate-limit.js.map +1 -0
- package/dist/lib/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents-by-id.js +10 -1
- package/dist/lib/server/api/routes/documents-by-id.js.map +1 -1
- package/dist/lib/server/api/routes/documents-publish.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents-publish.js +10 -0
- package/dist/lib/server/api/routes/documents-publish.js.map +1 -1
- package/dist/lib/server/api/routes/documents.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents.js +10 -0
- package/dist/lib/server/api/routes/documents.js.map +1 -1
- package/dist/lib/server/api/routes/jobs.d.ts.map +1 -1
- package/dist/lib/server/api/routes/jobs.js +174 -20
- package/dist/lib/server/api/routes/jobs.js.map +1 -1
- package/dist/lib/server/api/routes/organizations-by-id.d.ts.map +1 -1
- package/dist/lib/server/api/routes/organizations-by-id.js +61 -3
- package/dist/lib/server/api/routes/organizations-by-id.js.map +1 -1
- package/dist/lib/server/api/routes/organizations-invitations.d.ts.map +1 -1
- package/dist/lib/server/api/routes/organizations-invitations.js +31 -2
- package/dist/lib/server/api/routes/organizations-invitations.js.map +1 -1
- package/dist/lib/server/api/routes/resolve-organization-names.d.ts +17 -0
- package/dist/lib/server/api/routes/resolve-organization-names.d.ts.map +1 -0
- package/dist/lib/server/api/routes/resolve-organization-names.js +26 -0
- package/dist/lib/server/api/routes/resolve-organization-names.js.map +1 -0
- package/dist/lib/server/api/routes/user.d.ts +0 -8
- package/dist/lib/server/api/routes/user.d.ts.map +1 -1
- package/dist/lib/server/api/routes/user.js +144 -0
- package/dist/lib/server/api/routes/user.js.map +1 -1
- package/dist/lib/server/index.d.ts +6 -1
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js +9 -1
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/types/capabilities.d.ts +14 -0
- package/dist/lib/types/capabilities.d.ts.map +1 -1
- package/dist/lib/types/capabilities.js +37 -18
- package/dist/lib/types/capabilities.js.map +1 -1
- package/dist/lib/types/events.d.ts +27 -4
- package/dist/lib/types/events.d.ts.map +1 -1
- package/dist/lib/utils/fetch-remote-file.d.ts.map +1 -1
- package/dist/lib/utils/fetch-remote-file.js +32 -3
- package/dist/lib/utils/fetch-remote-file.js.map +1 -1
- package/dist/local-api/collection-api.d.ts +42 -1
- package/dist/local-api/collection-api.d.ts.map +1 -1
- package/dist/local-api/collection-api.js +68 -8
- package/dist/local-api/index.d.ts +1 -1
- package/dist/local-api/index.d.ts.map +1 -1
- package/dist/local-api/index.js +1 -1
- package/dist/plugins/resolver.d.ts.map +1 -1
- package/dist/plugins/resolver.js +7 -1
- package/dist/server/api/rate-limit.d.ts +38 -0
- package/dist/server/api/rate-limit.d.ts.map +1 -0
- package/dist/server/api/rate-limit.js +70 -0
- package/dist/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/server/api/routes/documents-by-id.js +10 -1
- package/dist/server/api/routes/documents-publish.d.ts.map +1 -1
- package/dist/server/api/routes/documents-publish.js +10 -0
- package/dist/server/api/routes/documents.d.ts.map +1 -1
- package/dist/server/api/routes/documents.js +10 -0
- package/dist/server/api/routes/jobs.d.ts.map +1 -1
- package/dist/server/api/routes/jobs.js +174 -20
- package/dist/server/api/routes/organizations-by-id.d.ts.map +1 -1
- package/dist/server/api/routes/organizations-by-id.js +61 -3
- package/dist/server/api/routes/organizations-invitations.d.ts.map +1 -1
- package/dist/server/api/routes/organizations-invitations.js +31 -2
- package/dist/server/api/routes/resolve-organization-names.d.ts +17 -0
- package/dist/server/api/routes/resolve-organization-names.d.ts.map +1 -0
- package/dist/server/api/routes/resolve-organization-names.js +25 -0
- package/dist/server/api/routes/user.d.ts +0 -8
- package/dist/server/api/routes/user.d.ts.map +1 -1
- package/dist/server/api/routes/user.js +144 -0
- package/dist/server/index.d.ts +6 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +9 -1
- package/dist/types/capabilities.d.ts +14 -0
- package/dist/types/capabilities.d.ts.map +1 -1
- package/dist/types/capabilities.js +37 -18
- package/dist/types/events.d.ts +27 -4
- package/dist/types/events.d.ts.map +1 -1
- package/dist/utils/fetch-remote-file.d.ts.map +1 -1
- package/dist/utils/fetch-remote-file.js +32 -3
- package/package.json +6 -6
|
@@ -65,7 +65,8 @@ export declare class OrganizationsApi {
|
|
|
65
65
|
success: boolean;
|
|
66
66
|
}>>;
|
|
67
67
|
/**
|
|
68
|
-
* Delete an organization
|
|
68
|
+
* Delete an organization, its media, and every membership in it. Owners only —
|
|
69
|
+
* enforced by the route, not here.
|
|
69
70
|
*/
|
|
70
71
|
static remove(id: string): Promise<ApiResponse<{
|
|
71
72
|
success: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"organizations.d.ts","sourceRoot":"","sources":["../../src/lib/api/organizations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAChG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EACX,yBAAyB,EACzB,yBAAyB,EACzB,yBAAyB,EACzB,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,uBAAuB,EACvB,MAAM,yBAAyB,CAAC;AAIjC,MAAM,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAC/D,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AACnD,MAAM,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAC3D,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AACnD,MAAM,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAE3D,MAAM,WAAW,oBAAqB,SAAQ,YAAY;IACzD,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,EAAE,IAAI,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,gBAAgB;IAC5B;;OAEG;WACU,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAIjE;;OAEG;WACU,MAAM,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IAIxF;;OAEG;WACU,MAAM,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAIhG;;OAEG;WACU,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IAIpE;;OAEG;WACU,SAAS,IAAI,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IAY5D;;OAEG;WACU,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAIrE;;OAEG;WACU,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAI9F;;OAEG;WACU,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAIhG;;OAEG;WACU,gBAAgB,CAC5B,IAAI,EAAE,uBAAuB,GAC3B,OAAO,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAI3C;;OAEG;WACU,MAAM,CAClB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,yBAAyB,GAC7B,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IAIrC;;OAEG;WACU,gBAAgB,CAC5B,IAAI,EAAE,uBAAuB,GAC3B,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAI7C
|
|
1
|
+
{"version":3,"file":"organizations.d.ts","sourceRoot":"","sources":["../../src/lib/api/organizations.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAChG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,KAAK,EACX,yBAAyB,EACzB,yBAAyB,EACzB,yBAAyB,EACzB,mBAAmB,EACnB,uBAAuB,EACvB,mBAAmB,EACnB,uBAAuB,EACvB,MAAM,yBAAyB,CAAC;AAIjC,MAAM,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAC/D,MAAM,MAAM,sBAAsB,GAAG,yBAAyB,CAAC;AAC/D,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AACnD,MAAM,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAC3D,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AACnD,MAAM,MAAM,oBAAoB,GAAG,uBAAuB,CAAC;AAE3D,MAAM,WAAW,oBAAqB,SAAQ,YAAY;IACzD,IAAI,EAAE,gBAAgB,CAAC;IACvB,QAAQ,EAAE,IAAI,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;CAClB;AAED,qBAAa,gBAAgB;IAC5B;;OAEG;WACU,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC,oBAAoB,EAAE,CAAC,CAAC;IAIjE;;OAEG;WACU,MAAM,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IAIxF;;OAEG;WACU,MAAM,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAIhG;;OAEG;WACU,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IAIpE;;OAEG;WACU,SAAS,IAAI,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IAY5D;;OAEG;WACU,UAAU,IAAI,OAAO,CAAC,WAAW,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAIrE;;OAEG;WACU,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAI9F;;OAEG;WACU,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAIhG;;OAEG;WACU,gBAAgB,CAC5B,IAAI,EAAE,uBAAuB,GAC3B,OAAO,CAAC,WAAW,CAAC,kBAAkB,CAAC,CAAC;IAI3C;;OAEG;WACU,MAAM,CAClB,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,yBAAyB,GAC7B,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC;IAIrC;;OAEG;WACU,gBAAgB,CAC5B,IAAI,EAAE,uBAAuB,GAC3B,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAI7C;;;OAGG;WACU,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAG3E;AAGD,eAAO,MAAM,aAAa;;;;;;;;;;;;;CAazB,CAAC"}
|
|
@@ -76,7 +76,8 @@ export class OrganizationsApi {
|
|
|
76
76
|
return apiClient.delete('/organizations/invitations', data);
|
|
77
77
|
}
|
|
78
78
|
/**
|
|
79
|
-
* Delete an organization
|
|
79
|
+
* Delete an organization, its media, and every membership in it. Owners only —
|
|
80
|
+
* enforced by the route, not here.
|
|
80
81
|
*/
|
|
81
82
|
static async remove(id) {
|
|
82
83
|
return apiClient.delete(`/organizations/${id}`);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Body for both operator actions on a job.
|
|
4
|
+
*
|
|
5
|
+
* `organizationId` exists solely for the instance-wide view: a super admin looking at every
|
|
6
|
+
* tenant's queue needs to act on a job that isn't in their *active* org, and the id alone
|
|
7
|
+
* doesn't say where it lives. Omit it and the action targets the caller's active organization
|
|
8
|
+
* — which is the only thing a non-super-admin may ever do (the route rejects a mismatch with
|
|
9
|
+
* 403 rather than trusting the body).
|
|
10
|
+
*/
|
|
11
|
+
export declare const jobActionRequestSchema: z.ZodObject<{
|
|
12
|
+
organizationId: z.ZodOptional<z.ZodString>;
|
|
13
|
+
}, z.core.$strip>;
|
|
14
|
+
export type JobActionRequest = z.infer<typeof jobActionRequestSchema>;
|
|
15
|
+
//# sourceMappingURL=jobs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../../src/lib/api/schemas/jobs.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB;;iBAEjC,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Request contracts for the job queue's operator actions (POST /api/jobs/:id/retry|cancel).
|
|
2
|
+
// Single source of truth: the route validates with these, the client infers its input types
|
|
3
|
+
// from them. The read endpoints (GET /jobs, /events, /jobs/health) keep their query schemas
|
|
4
|
+
// in the route handler — per convention, GET-only shapes don't earn a shared module.
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
/**
|
|
7
|
+
* Body for both operator actions on a job.
|
|
8
|
+
*
|
|
9
|
+
* `organizationId` exists solely for the instance-wide view: a super admin looking at every
|
|
10
|
+
* tenant's queue needs to act on a job that isn't in their *active* org, and the id alone
|
|
11
|
+
* doesn't say where it lives. Omit it and the action targets the caller's active organization
|
|
12
|
+
* — which is the only thing a non-super-admin may ever do (the route rejects a mismatch with
|
|
13
|
+
* 403 rather than trusting the body).
|
|
14
|
+
*/
|
|
15
|
+
export const jobActionRequestSchema = z.object({
|
|
16
|
+
organizationId: z.string().min(1).optional()
|
|
17
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../../src/lib/api/schemas/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,iBAAiB;;;
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../../src/lib/api/schemas/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,iBAAiB;;;iBAW3B,CAAC;AAIJ,eAAO,MAAM,4BAA4B;;kBAI/B,CAAC;AAIX,eAAO,MAAM,2BAA2B;;;iBAGtC,CAAC;AAIH,eAAO,MAAM,oBAAoB;;;iBAG/B,CAAC;AAIH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAClE,MAAM,MAAM,4BAA4B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC;AACxF,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AACtF,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
|
package/dist/api/schemas/user.js
CHANGED
|
@@ -2,8 +2,12 @@ import { z } from 'zod';
|
|
|
2
2
|
// ---------- PATCH /user ----------
|
|
3
3
|
export const updateUserRequest = z
|
|
4
4
|
.object({
|
|
5
|
-
name
|
|
6
|
-
|
|
5
|
+
// Bounded so a display name can't be pasted in at essay length — it renders
|
|
6
|
+
// in sidebars, member lists, and audit trails that all assume it's short.
|
|
7
|
+
name: z.string().min(1).max(80).optional(),
|
|
8
|
+
// Generous, but finite: this holds a `/media/<id>/<filename>` path or an
|
|
9
|
+
// external provider's avatar URL, never free text.
|
|
10
|
+
image: z.string().min(1).max(2048).nullable().optional()
|
|
7
11
|
})
|
|
8
12
|
.refine((v) => v.name !== undefined || v.image !== undefined, {
|
|
9
13
|
message: 'At least one field (name, image) is required'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../db/interfaces/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when an account may not be deleted yet. Providers translate it into their own
|
|
4
|
+
* transport error; the message is written to be shown to the user as-is.
|
|
5
|
+
*/
|
|
6
|
+
export declare class AccountDeletionBlockedError extends Error {
|
|
7
|
+
readonly code = "ACCOUNT_DELETION_BLOCKED";
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Throws unless this user can leave without stranding an organization.
|
|
12
|
+
*
|
|
13
|
+
* The rule is narrow on purpose: an organization whose only owner deletes their account has
|
|
14
|
+
* nobody left who can add members, change roles, or delete it — the org becomes unreachable
|
|
15
|
+
* to everyone including its remaining members, and no in-product path can recover it. Being
|
|
16
|
+
* the sole *member* is fine, because deleting the organization first is a thing the user can
|
|
17
|
+
* actually do; being the sole *owner* of an org with other people in it is the trap.
|
|
18
|
+
*
|
|
19
|
+
* Enforced server-side rather than in the confirmation dialog: the endpoint is reachable
|
|
20
|
+
* without the UI, and this is the check that decides whether an organization survives.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertAccountDeletable(db: DatabaseAdapter, userId: string): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Detach a deleted user from every organization.
|
|
25
|
+
*
|
|
26
|
+
* `cms_organization_members.user_id` has no foreign key to the profile table — memberships
|
|
27
|
+
* reference the auth layer's user, which cms-core doesn't own — so nothing cascades and a
|
|
28
|
+
* deleted account would otherwise keep appearing in member lists indefinitely, still holding
|
|
29
|
+
* whatever role it had.
|
|
30
|
+
*
|
|
31
|
+
* Call inside the same transaction as the profile delete, after the erasure events are
|
|
32
|
+
* emitted (those read the memberships to know which organizations to fan out to).
|
|
33
|
+
*/
|
|
34
|
+
export declare function detachUserFromOrganizations(tx: DatabaseAdapter, userId: string, organizationIds: readonly string[]): Promise<void>;
|
|
35
|
+
//# sourceMappingURL=account-deletion.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-deletion.d.ts","sourceRoot":"","sources":["../../src/lib/auth/account-deletion.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;;GAGG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IACrD,QAAQ,CAAC,IAAI,8BAA8B;gBAC/B,OAAO,EAAE,MAAM;CAI3B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,sBAAsB,CAAC,EAAE,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB/F;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,2BAA2B,CAChD,EAAE,EAAE,eAAe,EACnB,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,SAAS,MAAM,EAAE,GAChC,OAAO,CAAC,IAAI,CAAC,CAUf"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// What has to be true before an account may be deleted, and what has to happen after.
|
|
2
|
+
//
|
|
3
|
+
// Lives in cms-core rather than in `@aphexcms/auth` for the same reason the sign-up gate
|
|
4
|
+
// does: "can this account go?" is a CMS question about organizations and ownership, and it
|
|
5
|
+
// must mean the same thing whichever provider authenticated the user. A provider supplies
|
|
6
|
+
// the lifecycle hook; it doesn't get to re-derive the rule.
|
|
7
|
+
import { cmsLogger } from '../utils/logger.js';
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when an account may not be deleted yet. Providers translate it into their own
|
|
10
|
+
* transport error; the message is written to be shown to the user as-is.
|
|
11
|
+
*/
|
|
12
|
+
export class AccountDeletionBlockedError extends Error {
|
|
13
|
+
code = 'ACCOUNT_DELETION_BLOCKED';
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'AccountDeletionBlockedError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Throws unless this user can leave without stranding an organization.
|
|
21
|
+
*
|
|
22
|
+
* The rule is narrow on purpose: an organization whose only owner deletes their account has
|
|
23
|
+
* nobody left who can add members, change roles, or delete it — the org becomes unreachable
|
|
24
|
+
* to everyone including its remaining members, and no in-product path can recover it. Being
|
|
25
|
+
* the sole *member* is fine, because deleting the organization first is a thing the user can
|
|
26
|
+
* actually do; being the sole *owner* of an org with other people in it is the trap.
|
|
27
|
+
*
|
|
28
|
+
* Enforced server-side rather than in the confirmation dialog: the endpoint is reachable
|
|
29
|
+
* without the UI, and this is the check that decides whether an organization survives.
|
|
30
|
+
*/
|
|
31
|
+
export async function assertAccountDeletable(db, userId) {
|
|
32
|
+
const memberships = await db.findUserOrganizations(userId);
|
|
33
|
+
const stranded = [];
|
|
34
|
+
for (const { organization } of memberships) {
|
|
35
|
+
const members = await db.findOrganizationMembers(organization.id);
|
|
36
|
+
const owners = members.filter((m) => m.role === 'owner');
|
|
37
|
+
// Sole owner, and somebody else is still here.
|
|
38
|
+
if (owners.length === 1 && owners[0]?.userId === userId && members.length > 1) {
|
|
39
|
+
stranded.push(organization.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (stranded.length > 0) {
|
|
43
|
+
throw new AccountDeletionBlockedError(`You are the only owner of ${stranded.join(', ')}. Transfer ownership or remove the other ` +
|
|
44
|
+
`members before deleting your account.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Detach a deleted user from every organization.
|
|
49
|
+
*
|
|
50
|
+
* `cms_organization_members.user_id` has no foreign key to the profile table — memberships
|
|
51
|
+
* reference the auth layer's user, which cms-core doesn't own — so nothing cascades and a
|
|
52
|
+
* deleted account would otherwise keep appearing in member lists indefinitely, still holding
|
|
53
|
+
* whatever role it had.
|
|
54
|
+
*
|
|
55
|
+
* Call inside the same transaction as the profile delete, after the erasure events are
|
|
56
|
+
* emitted (those read the memberships to know which organizations to fan out to).
|
|
57
|
+
*/
|
|
58
|
+
export async function detachUserFromOrganizations(tx, userId, organizationIds) {
|
|
59
|
+
for (const organizationId of organizationIds) {
|
|
60
|
+
const removed = await tx.removeMember(organizationId, userId);
|
|
61
|
+
if (!removed) {
|
|
62
|
+
cmsLogger.warn('[AccountDeletion]', `No membership row for ${userId} in ${organizationId} — nothing to detach.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../db/interfaces/index.js';
|
|
2
|
+
/** Instance-level role a bootstrap policy may grant. */
|
|
3
|
+
export type InstanceRole = 'super_admin' | 'admin' | 'editor' | 'viewer';
|
|
4
|
+
export interface BootstrapContext {
|
|
5
|
+
user: {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
emailVerified: boolean;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* True only when the system is *provably* empty.
|
|
12
|
+
*
|
|
13
|
+
* Never inferred from a missing capability: an adapter that can't answer the
|
|
14
|
+
* question yields `false`, so an unanswerable check can't be mistaken for
|
|
15
|
+
* "nobody is here yet, promote this person".
|
|
16
|
+
*/
|
|
17
|
+
isFirstUser: boolean;
|
|
18
|
+
/** The request that triggered profile creation, when there is one. */
|
|
19
|
+
request?: Request;
|
|
20
|
+
db: DatabaseAdapter;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Decides what instance role a brand-new user profile gets.
|
|
24
|
+
*
|
|
25
|
+
* Bootstrapping is a policy decision, not a library one — how you claim a fresh
|
|
26
|
+
* instance depends on how you deploy it. Return `null` for "no promotion"; the
|
|
27
|
+
* caller falls back to the ordinary `editor` role.
|
|
28
|
+
*/
|
|
29
|
+
export interface BootstrapPolicy {
|
|
30
|
+
(ctx: BootstrapContext): Promise<InstanceRole | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Optional one-off startup step, run before any request is served.
|
|
33
|
+
*
|
|
34
|
+
* Lets a recipe own its own setup instead of making the app wire it: only
|
|
35
|
+
* `claimCode()` needs one (to generate and log its code), and an app that
|
|
36
|
+
* swaps recipes shouldn't have to remember to delete a hook. Callers invoke
|
|
37
|
+
* it as `policy.prepare?.(db)` — recipes without setup simply omit it.
|
|
38
|
+
*/
|
|
39
|
+
prepare?: (db: DatabaseAdapter) => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Ensure an unclaimed instance has a pending claim code, and log it.
|
|
43
|
+
*
|
|
44
|
+
* Only the **hash** is persisted: a leaked database dump or backup shouldn't be
|
|
45
|
+
* enough to claim the instance. Called at startup by `claimCode()` consumers.
|
|
46
|
+
*/
|
|
47
|
+
export declare function ensureClaimCode(db: DatabaseAdapter): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* True when nobody has claimed this instance yet and a code is waiting to be
|
|
50
|
+
* used. Drives the sign-up form's claim-code field: without this the code has
|
|
51
|
+
* nowhere to go but a hand-set cookie, which is not a flow anyone can follow.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately narrow. It reveals only that an instance is unclaimed — never the
|
|
54
|
+
* code or its hash — so it is safe to hand to an unauthenticated page. That fact
|
|
55
|
+
* is already obvious to anyone who can reach a CMS with no users in it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isInstanceUnclaimed(db: DatabaseAdapter): Promise<boolean>;
|
|
58
|
+
/**
|
|
59
|
+
* The default. Keeps the familiar first-run wizard, but promotion requires a
|
|
60
|
+
* code printed to the server log at startup — so arriving first isn't enough,
|
|
61
|
+
* you also have to control the deployment. Same shape as Jupyter's `?token=`
|
|
62
|
+
* and GitLab's generated root password.
|
|
63
|
+
*
|
|
64
|
+
* Clearing the hash here is not what makes the code single-use — two concurrent
|
|
65
|
+
* claims can both read it before either clears it. Mutual exclusion comes from
|
|
66
|
+
* `tryClaimBootstrap`, which `createUserProfileWithBootstrap` takes before
|
|
67
|
+
* granting any instance role: the loser is demoted to an ordinary profile even
|
|
68
|
+
* though this returned `super_admin`. Clearing the hash still matters, just for
|
|
69
|
+
* the sequential case — it stops the code being reused later.
|
|
70
|
+
*
|
|
71
|
+
* One residual: a claim can be spent by a request whose profile insert then
|
|
72
|
+
* fails, which costs a code rather than granting anything. Recover by clearing
|
|
73
|
+
* the key from instance settings and restarting for a fresh one.
|
|
74
|
+
*/
|
|
75
|
+
export declare function claimCode(options?: {
|
|
76
|
+
readCode?: (request?: Request) => string | undefined;
|
|
77
|
+
}): BootstrapPolicy;
|
|
78
|
+
/**
|
|
79
|
+
* First user whose address is on the allowlist becomes super admin — Discourse's
|
|
80
|
+
* `DISCOURSE_DEVELOPER_EMAILS`. Good when you know the owner's address at deploy
|
|
81
|
+
* time and would rather not read logs.
|
|
82
|
+
*
|
|
83
|
+
* This recipe is only as strong as the address is trustworthy, so pair it with
|
|
84
|
+
* `requireEmailVerification`. That's enforced at the auth layer — better-auth
|
|
85
|
+
* refuses to complete sign-in for an unconfirmed address, so an unverified user
|
|
86
|
+
* never reaches profile creation at all. Re-checking it here would be dead code
|
|
87
|
+
* when verification is on, and would brick a fresh install when it's off.
|
|
88
|
+
*/
|
|
89
|
+
export declare function allowlistEmail(emails: string | readonly string[] | undefined): BootstrapPolicy;
|
|
90
|
+
/**
|
|
91
|
+
* Whoever signs up first becomes super admin, with nothing else required.
|
|
92
|
+
*
|
|
93
|
+
* This is what WordPress, Ghost, Strapi and Payload do, and it's fine when you
|
|
94
|
+
* install immediately after deploying. It is **not** fine for an instance that
|
|
95
|
+
* sits reachable before anyone signs in: the first stranger to find the URL owns
|
|
96
|
+
* it. Opt in deliberately.
|
|
97
|
+
*/
|
|
98
|
+
export declare function openFirstUser(): BootstrapPolicy;
|
|
99
|
+
/**
|
|
100
|
+
* Never promote anyone. Provision the first administrator out of band — a seed
|
|
101
|
+
* script, a migration, or a direct row — the way Directus and Keycloak do.
|
|
102
|
+
*/
|
|
103
|
+
export declare function never(): BootstrapPolicy;
|
|
104
|
+
//# sourceMappingURL=bootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/lib/auth/bootstrap.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,wDAAwD;AACxD,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAKzE,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,OAAO,CAAA;KAAE,CAAC;IAC5D;;;;;;OAMG;IACH,WAAW,EAAE,OAAO,CAAC;IACrB,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,EAAE,EAAE,eAAe,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe;IAC/B,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IACtD;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,eAAe,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACjD;AAcD;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,EAAE,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBxE;AAED;;;;;;;;GAQG;AACH,wBAAsB,mBAAmB,CAAC,EAAE,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAK/E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CACxB,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,OAAO,KAAK,MAAM,GAAG,SAAS,CAAA;CAAO,GACpE,eAAe,CAqDjB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,eAAe,CAsB9F;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,eAAe,CAE/C;AAED;;;GAGG;AACH,wBAAgB,KAAK,IAAI,eAAe,CAEvC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { cmsLogger } from '../utils/logger.js';
|
|
3
|
+
import { isInstanceEmpty } from './instance-state.js';
|
|
4
|
+
/** Where the claim-code hash lives inside the instance-settings blob. */
|
|
5
|
+
const CLAIM_CODE_KEY = 'bootstrapClaimCodeHash';
|
|
6
|
+
function hashCode(code) {
|
|
7
|
+
return createHash('sha256').update(code).digest('hex');
|
|
8
|
+
}
|
|
9
|
+
/** Constant-time compare of two hex digests of equal length. */
|
|
10
|
+
function digestsMatch(a, b) {
|
|
11
|
+
const left = Buffer.from(a, 'hex');
|
|
12
|
+
const right = Buffer.from(b, 'hex');
|
|
13
|
+
if (left.length !== right.length || left.length === 0)
|
|
14
|
+
return false;
|
|
15
|
+
return timingSafeEqual(left, right);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Ensure an unclaimed instance has a pending claim code, and log it.
|
|
19
|
+
*
|
|
20
|
+
* Only the **hash** is persisted: a leaked database dump or backup shouldn't be
|
|
21
|
+
* enough to claim the instance. Called at startup by `claimCode()` consumers.
|
|
22
|
+
*/
|
|
23
|
+
export async function ensureClaimCode(db) {
|
|
24
|
+
if (!(await isInstanceEmpty(db)))
|
|
25
|
+
return;
|
|
26
|
+
const settings = await db.getInstanceSettings();
|
|
27
|
+
if (typeof settings[CLAIM_CODE_KEY] === 'string')
|
|
28
|
+
return;
|
|
29
|
+
const code = randomBytes(24).toString('base64url');
|
|
30
|
+
await db.updateInstanceSettings({ [CLAIM_CODE_KEY]: hashCode(code) });
|
|
31
|
+
cmsLogger.info('[Bootstrap]', `\n\n This instance has no administrator yet.\n` +
|
|
32
|
+
` Sign up at /admin and enter this claim code to become the super admin:\n\n` +
|
|
33
|
+
` ${code}\n\n` +
|
|
34
|
+
` It is single-use and is not stored in recoverable form. If you lose it,\n` +
|
|
35
|
+
` clear "${CLAIM_CODE_KEY}" from instance settings and restart to get a new one.\n`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* True when nobody has claimed this instance yet and a code is waiting to be
|
|
39
|
+
* used. Drives the sign-up form's claim-code field: without this the code has
|
|
40
|
+
* nowhere to go but a hand-set cookie, which is not a flow anyone can follow.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately narrow. It reveals only that an instance is unclaimed — never the
|
|
43
|
+
* code or its hash — so it is safe to hand to an unauthenticated page. That fact
|
|
44
|
+
* is already obvious to anyone who can reach a CMS with no users in it.
|
|
45
|
+
*/
|
|
46
|
+
export async function isInstanceUnclaimed(db) {
|
|
47
|
+
if (!(await isInstanceEmpty(db)))
|
|
48
|
+
return false;
|
|
49
|
+
const settings = await db.getInstanceSettings();
|
|
50
|
+
return typeof settings[CLAIM_CODE_KEY] === 'string';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The default. Keeps the familiar first-run wizard, but promotion requires a
|
|
54
|
+
* code printed to the server log at startup — so arriving first isn't enough,
|
|
55
|
+
* you also have to control the deployment. Same shape as Jupyter's `?token=`
|
|
56
|
+
* and GitLab's generated root password.
|
|
57
|
+
*
|
|
58
|
+
* Clearing the hash here is not what makes the code single-use — two concurrent
|
|
59
|
+
* claims can both read it before either clears it. Mutual exclusion comes from
|
|
60
|
+
* `tryClaimBootstrap`, which `createUserProfileWithBootstrap` takes before
|
|
61
|
+
* granting any instance role: the loser is demoted to an ordinary profile even
|
|
62
|
+
* though this returned `super_admin`. Clearing the hash still matters, just for
|
|
63
|
+
* the sequential case — it stops the code being reused later.
|
|
64
|
+
*
|
|
65
|
+
* One residual: a claim can be spent by a request whose profile insert then
|
|
66
|
+
* fails, which costs a code rather than granting anything. Recover by clearing
|
|
67
|
+
* the key from instance settings and restarting for a fresh one.
|
|
68
|
+
*/
|
|
69
|
+
export function claimCode(options = {}) {
|
|
70
|
+
/**
|
|
71
|
+
* Header first, then an `aphex_bootstrap_code` cookie. The profile is created
|
|
72
|
+
* on the first authenticated request after sign-up, which may be a redirect
|
|
73
|
+
* the signup form doesn't control, so a header alone would be unreachable from
|
|
74
|
+
* a plain HTML flow. Override `readCode` to accept it somewhere else.
|
|
75
|
+
*
|
|
76
|
+
* Deliberately *not* a `?claim=` query parameter, which this used to accept.
|
|
77
|
+
* A URL carrying the code lands in browser history, server and proxy access
|
|
78
|
+
* logs, and any `Referer` sent to a third party — persisting a credential in
|
|
79
|
+
* several places nobody thinks to clear, to save one hop that the cookie
|
|
80
|
+
* already covers.
|
|
81
|
+
*/
|
|
82
|
+
const readCode = options.readCode ??
|
|
83
|
+
((request) => {
|
|
84
|
+
if (!request)
|
|
85
|
+
return undefined;
|
|
86
|
+
const header = request.headers.get('x-aphex-bootstrap-code');
|
|
87
|
+
if (header)
|
|
88
|
+
return header;
|
|
89
|
+
const cookie = request.headers.get('cookie');
|
|
90
|
+
const raw = cookie?.match(/(?:^|;\s*)aphex_bootstrap_code=([^;]+)/)?.[1];
|
|
91
|
+
// The sign-up form percent-encodes the value. A generated base64url code
|
|
92
|
+
// has nothing to decode, but a custom one may.
|
|
93
|
+
return raw === undefined ? undefined : decodeURIComponent(raw);
|
|
94
|
+
});
|
|
95
|
+
const policy = async ({ isFirstUser, request, db }) => {
|
|
96
|
+
if (!isFirstUser)
|
|
97
|
+
return null;
|
|
98
|
+
const supplied = readCode(request)?.trim();
|
|
99
|
+
if (!supplied)
|
|
100
|
+
return null;
|
|
101
|
+
const settings = await db.getInstanceSettings();
|
|
102
|
+
const expected = settings[CLAIM_CODE_KEY];
|
|
103
|
+
if (typeof expected !== 'string')
|
|
104
|
+
return null;
|
|
105
|
+
if (!digestsMatch(hashCode(supplied), expected)) {
|
|
106
|
+
cmsLogger.warn('[Bootstrap]', 'Rejected claim attempt — code did not match');
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
// Single-use: clearing the hash is what makes a second claimant lose.
|
|
110
|
+
await db.updateInstanceSettings({ [CLAIM_CODE_KEY]: null });
|
|
111
|
+
cmsLogger.info('[Bootstrap]', 'Instance claimed — super admin created');
|
|
112
|
+
return 'super_admin';
|
|
113
|
+
};
|
|
114
|
+
// The code only exists because this recipe was chosen, so this recipe is what
|
|
115
|
+
// generates it. Swapping to another recipe takes the startup step with it.
|
|
116
|
+
policy.prepare = ensureClaimCode;
|
|
117
|
+
return policy;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* First user whose address is on the allowlist becomes super admin — Discourse's
|
|
121
|
+
* `DISCOURSE_DEVELOPER_EMAILS`. Good when you know the owner's address at deploy
|
|
122
|
+
* time and would rather not read logs.
|
|
123
|
+
*
|
|
124
|
+
* This recipe is only as strong as the address is trustworthy, so pair it with
|
|
125
|
+
* `requireEmailVerification`. That's enforced at the auth layer — better-auth
|
|
126
|
+
* refuses to complete sign-in for an unconfirmed address, so an unverified user
|
|
127
|
+
* never reaches profile creation at all. Re-checking it here would be dead code
|
|
128
|
+
* when verification is on, and would brick a fresh install when it's off.
|
|
129
|
+
*/
|
|
130
|
+
export function allowlistEmail(emails) {
|
|
131
|
+
const allowed = new Set((typeof emails === 'string' ? emails.split(',') : (emails ?? []))
|
|
132
|
+
.map((email) => email.trim().toLowerCase())
|
|
133
|
+
.filter(Boolean));
|
|
134
|
+
return async ({ isFirstUser, user }) => {
|
|
135
|
+
if (!isFirstUser || allowed.size === 0)
|
|
136
|
+
return null;
|
|
137
|
+
if (!allowed.has(user.email.trim().toLowerCase()))
|
|
138
|
+
return null;
|
|
139
|
+
if (!user.emailVerified) {
|
|
140
|
+
// Advice, not enforcement — the decision belongs to the auth layer.
|
|
141
|
+
cmsLogger.warn('[Bootstrap]', `Promoting ${user.email} on an unverified address. Set ` +
|
|
142
|
+
`AUTH_REQUIRE_EMAIL_VERIFICATION=true so the address has to be proven.`);
|
|
143
|
+
}
|
|
144
|
+
return 'super_admin';
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Whoever signs up first becomes super admin, with nothing else required.
|
|
149
|
+
*
|
|
150
|
+
* This is what WordPress, Ghost, Strapi and Payload do, and it's fine when you
|
|
151
|
+
* install immediately after deploying. It is **not** fine for an instance that
|
|
152
|
+
* sits reachable before anyone signs in: the first stranger to find the URL owns
|
|
153
|
+
* it. Opt in deliberately.
|
|
154
|
+
*/
|
|
155
|
+
export function openFirstUser() {
|
|
156
|
+
return async ({ isFirstUser }) => (isFirstUser ? 'super_admin' : null);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Never promote anyone. Provision the first administrator out of band — a seed
|
|
160
|
+
* script, a migration, or a direct row — the way Directus and Keycloak do.
|
|
161
|
+
*/
|
|
162
|
+
export function never() {
|
|
163
|
+
return async () => null;
|
|
164
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DatabaseAdapter } from '../db/interfaces/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* True only when the instance is *provably* empty — the adapter can answer the
|
|
4
|
+
* question, and the answer is "no user profiles exist".
|
|
5
|
+
*
|
|
6
|
+
* Returns `false` when the adapter doesn't implement `hasAnyUserProfiles()`,
|
|
7
|
+
* which is the safe direction: bootstrap promotion is skipped and the invite
|
|
8
|
+
* gate stays shut rather than swinging open.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isInstanceEmpty(db: DatabaseAdapter): Promise<boolean>;
|
|
11
|
+
/** Whether the adapter can answer `isInstanceEmpty` at all — for warning about a skipped bootstrap. */
|
|
12
|
+
export declare function canDetermineInstanceEmptiness(db: DatabaseAdapter): boolean;
|
|
13
|
+
//# sourceMappingURL=instance-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-state.d.ts","sourceRoot":"","sources":["../../src/lib/auth/instance-state.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,EAAE,EAAE,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,CAI3E;AAED,uGAAuG;AACvG,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,eAAe,GAAG,OAAO,CAE1E"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The single definition of "has this instance been claimed yet?".
|
|
2
|
+
//
|
|
3
|
+
// Two features hang off this one fact — the bootstrap policy (who gets promoted
|
|
4
|
+
// to super admin) and the invite gate (who may sign up at all) — and they must
|
|
5
|
+
// agree, or the disagreement is a bypass rather than a bug.
|
|
6
|
+
//
|
|
7
|
+
// It also has one non-obvious rule that every caller has to get right, which is
|
|
8
|
+
// why it lives in one place instead of six: **proof of emptiness, never absence
|
|
9
|
+
// of proof.** `hasAnyUserProfiles` is optional on the adapter interface, and an
|
|
10
|
+
// adapter that cannot answer must NOT be read as "nobody is here yet". Getting
|
|
11
|
+
// that backwards is exactly how a missing implementation once promoted every
|
|
12
|
+
// single sign-up to super admin.
|
|
13
|
+
/**
|
|
14
|
+
* True only when the instance is *provably* empty — the adapter can answer the
|
|
15
|
+
* question, and the answer is "no user profiles exist".
|
|
16
|
+
*
|
|
17
|
+
* Returns `false` when the adapter doesn't implement `hasAnyUserProfiles()`,
|
|
18
|
+
* which is the safe direction: bootstrap promotion is skipped and the invite
|
|
19
|
+
* gate stays shut rather than swinging open.
|
|
20
|
+
*/
|
|
21
|
+
export async function isInstanceEmpty(db) {
|
|
22
|
+
const countUsers = db.hasAnyUserProfiles?.bind(db);
|
|
23
|
+
if (!countUsers)
|
|
24
|
+
return false;
|
|
25
|
+
return !(await countUsers());
|
|
26
|
+
}
|
|
27
|
+
/** Whether the adapter can answer `isInstanceEmpty` at all — for warning about a skipped bootstrap. */
|
|
28
|
+
export function canDetermineInstanceEmptiness(db) {
|
|
29
|
+
return typeof db.hasAnyUserProfiles === 'function';
|
|
30
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Invitation } from '../types/organization.js';
|
|
2
|
+
/** The minimum shape these predicates need — so callers can pass rows from any layer. */
|
|
3
|
+
export type InvitationState = Pick<Invitation, 'expiresAt' | 'acceptedAt'>;
|
|
4
|
+
/** Default lifetime for a new invitation. */
|
|
5
|
+
export declare const INVITATION_TTL_MS: number;
|
|
6
|
+
/** When a freshly-created invitation should expire. */
|
|
7
|
+
export declare function invitationExpiryFrom(now?: Date): Date;
|
|
8
|
+
export declare function isAccepted(invitation: InvitationState): boolean;
|
|
9
|
+
export declare function isExpired(invitation: InvitationState, now?: Date): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Redeemable right now: not yet accepted and not yet expired.
|
|
12
|
+
*
|
|
13
|
+
* The only question worth asking in most places — whether a sign-up may proceed,
|
|
14
|
+
* whether to show it in the members list, whether a re-invite is redundant.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isPendingInvitation(invitation: InvitationState, now?: Date): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Lapsed without being used, so it is safe to clear and replace.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately distinct from `!isPendingInvitation(...)`: an *accepted*
|
|
21
|
+
* invitation is also "not pending", but deleting it would erase the record that
|
|
22
|
+
* someone joined by invitation.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isStaleInvitation(invitation: InvitationState, now?: Date): boolean;
|
|
25
|
+
//# sourceMappingURL=invitation-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invitation-status.d.ts","sourceRoot":"","sources":["../../src/lib/auth/invitation-status.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAExD,yFAAyF;AACzF,MAAM,MAAM,eAAe,GAAG,IAAI,CAAC,UAAU,EAAE,WAAW,GAAG,YAAY,CAAC,CAAC;AAE3E,6CAA6C;AAC7C,eAAO,MAAM,iBAAiB,QAA0B,CAAC;AAEzD,uDAAuD;AACvD,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,IAAiB,GAAG,IAAI,CAEjE;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,eAAe,GAAG,OAAO,CAE/D;AAED,wBAAgB,SAAS,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAEtF;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAEhG;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,eAAe,EAAE,GAAG,GAAE,IAAiB,GAAG,OAAO,CAE9F"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The single definition of what an invitation's state means.
|
|
2
|
+
//
|
|
3
|
+
// This exists because three call sites independently decided what "pending"
|
|
4
|
+
// meant and two of them drifted: the sign-up gate required unaccepted *and*
|
|
5
|
+
// unexpired, the members list did the same, but the re-invite check tested only
|
|
6
|
+
// `acceptedAt === null`. An expired invitation therefore blocked re-inviting
|
|
7
|
+
// while being unusable and invisible — a permanent deadlock for that address.
|
|
8
|
+
//
|
|
9
|
+
// Anything that asks a question about invitation state imports from here. If a
|
|
10
|
+
// rule changes (grace periods, revocation, resend windows), it changes once.
|
|
11
|
+
/** Default lifetime for a new invitation. */
|
|
12
|
+
export const INVITATION_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
13
|
+
/** When a freshly-created invitation should expire. */
|
|
14
|
+
export function invitationExpiryFrom(now = new Date()) {
|
|
15
|
+
return new Date(now.getTime() + INVITATION_TTL_MS);
|
|
16
|
+
}
|
|
17
|
+
export function isAccepted(invitation) {
|
|
18
|
+
return invitation.acceptedAt !== null;
|
|
19
|
+
}
|
|
20
|
+
export function isExpired(invitation, now = new Date()) {
|
|
21
|
+
return new Date(invitation.expiresAt).getTime() <= now.getTime();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Redeemable right now: not yet accepted and not yet expired.
|
|
25
|
+
*
|
|
26
|
+
* The only question worth asking in most places — whether a sign-up may proceed,
|
|
27
|
+
* whether to show it in the members list, whether a re-invite is redundant.
|
|
28
|
+
*/
|
|
29
|
+
export function isPendingInvitation(invitation, now = new Date()) {
|
|
30
|
+
return !isAccepted(invitation) && !isExpired(invitation, now);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Lapsed without being used, so it is safe to clear and replace.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately distinct from `!isPendingInvitation(...)`: an *accepted*
|
|
36
|
+
* invitation is also "not pending", but deleting it would erase the record that
|
|
37
|
+
* someone joined by invitation.
|
|
38
|
+
*/
|
|
39
|
+
export function isStaleInvitation(invitation, now = new Date()) {
|
|
40
|
+
return !isAccepted(invitation) && isExpired(invitation, now);
|
|
41
|
+
}
|