@aphexcms/cms-core 9.8.0 → 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/agent-change-sets.js +1 -1
- package/dist/lib/server/api/routes/agent-change-sets.js.map +1 -1
- package/dist/lib/server/api/routes/document-versions.js +1 -1
- package/dist/lib/server/api/routes/document-versions.js.map +1 -1
- package/dist/lib/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/lib/server/api/routes/documents-by-id.js +11 -2
- 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 +11 -1
- 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/services/references-service.d.ts +1 -1
- package/dist/lib/services/references-service.d.ts.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/agent-change-sets.js +1 -1
- package/dist/server/api/routes/document-versions.js +1 -1
- package/dist/server/api/routes/documents-by-id.d.ts.map +1 -1
- package/dist/server/api/routes/documents-by-id.js +11 -2
- package/dist/server/api/routes/documents-publish.d.ts.map +1 -1
- package/dist/server/api/routes/documents-publish.js +11 -1
- 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/services/references-service.d.ts +1 -1
- package/dist/services/references-service.d.ts.map +1 -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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { BootstrapPolicy, InstanceRole } from './bootstrap.js';
|
|
2
|
+
import type { DatabaseAdapter } from '../db/interfaces/index.js';
|
|
3
|
+
import type { UserProfile } from '../types/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Thrown when sign-up is refused by policy.
|
|
6
|
+
*
|
|
7
|
+
* Providers translate this into whatever their transport expects — Better Auth
|
|
8
|
+
* raises an `APIError`, a REST handler might return 403. The message is written
|
|
9
|
+
* to be safe to show a user.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SignUpBlockedError extends Error {
|
|
12
|
+
readonly code = "SIGN_UP_BLOCKED";
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
export interface SignUpGateOptions {
|
|
16
|
+
db: DatabaseAdapter;
|
|
17
|
+
/** Address attempting to register. Matched case-insensitively. */
|
|
18
|
+
email: string | undefined;
|
|
19
|
+
/** When false the gate is disabled entirely and this is a no-op. */
|
|
20
|
+
inviteOnly: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Throws `SignUpBlockedError` unless this address may create an account.
|
|
24
|
+
*
|
|
25
|
+
* Two ways through: the instance is provably empty (nobody exists yet to have
|
|
26
|
+
* sent an invitation, so gating the first sign-up would lock the door with the
|
|
27
|
+
* keys inside), or the address holds a pending invitation.
|
|
28
|
+
*
|
|
29
|
+
* Narrowing that first exception is the bootstrap policy's job — see
|
|
30
|
+
* `resolveBootstrapRole` — not this gate's.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertSignUpAllowed({ db, email, inviteOnly }: SignUpGateOptions): Promise<void>;
|
|
33
|
+
export interface BootstrapRoleOptions {
|
|
34
|
+
db: DatabaseAdapter;
|
|
35
|
+
user: {
|
|
36
|
+
id: string;
|
|
37
|
+
email: string;
|
|
38
|
+
emailVerified: boolean;
|
|
39
|
+
};
|
|
40
|
+
/** The request that triggered profile creation, when there is one. */
|
|
41
|
+
request?: Request;
|
|
42
|
+
bootstrap: BootstrapPolicy;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Runs the bootstrap policy for a brand-new user, returning the instance role to
|
|
46
|
+
* grant or `null` for none.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately not wrapped in a transaction. SQLite allows a single writer, and
|
|
49
|
+
* holding that lock across the policy's own round-trips collides with the auth
|
|
50
|
+
* provider's concurrent user/session inserts (SQLITE_BUSY). The atomicity was
|
|
51
|
+
* only partial anyway — see `claimCode`: a claim is single-use, not mutually
|
|
52
|
+
* exclusive.
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveBootstrapRole({ db, user, request, bootstrap }: BootstrapRoleOptions): Promise<InstanceRole | null>;
|
|
55
|
+
/**
|
|
56
|
+
* The one place a CMS user profile is created for a newly-authenticated user.
|
|
57
|
+
*
|
|
58
|
+
* Every `AuthProvider` should route new profiles through here so bootstrap
|
|
59
|
+
* promotion happens identically regardless of who did the authenticating.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createUserProfileWithBootstrap(options: BootstrapRoleOptions): Promise<UserProfile>;
|
|
62
|
+
//# sourceMappingURL=sign-up-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sign-up-policy.d.ts","sourceRoot":"","sources":["../../src/lib/auth/sign-up-policy.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAElD;;;;;;GAMG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,qBAAqB;gBACtB,OAAO,EAAE,MAAM;CAI3B;AAED,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,eAAe,CAAC;IACpB,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CAAC,EACzC,EAAE,EACF,KAAK,EACL,UAAU,EACV,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBnC;AAED,MAAM,WAAW,oBAAoB;IACpC,EAAE,EAAE,eAAe,CAAC;IACpB,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,OAAO,CAAA;KAAE,CAAC;IAC5D,sEAAsE;IACtE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CAAC,EAC1C,EAAE,EACF,IAAI,EACJ,OAAO,EACP,SAAS,EACT,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAerD;AAuBD;;;;;GAKG;AACH,wBAAsB,8BAA8B,CACnD,OAAO,EAAE,oBAAoB,GAC3B,OAAO,CAAC,WAAW,CAAC,CA6BtB"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Who may sign up, and what they get when they do.
|
|
2
|
+
//
|
|
3
|
+
// These are **CMS policy decisions, not auth-provider concerns**. Whether an
|
|
4
|
+
// instance is invite-only, and who becomes its first administrator, should mean
|
|
5
|
+
// the same thing whether the app authenticates with Better Auth, Keycloak,
|
|
6
|
+
// Supabase, or something hand-rolled — so they live behind the `AuthProvider`
|
|
7
|
+
// port rather than inside any one implementation of it.
|
|
8
|
+
//
|
|
9
|
+
// Before this existed, both rules were enforced inside `@aphexcms/auth`'s
|
|
10
|
+
// Better Auth wiring, which meant a third-party provider silently inherited
|
|
11
|
+
// neither: `inviteOnly: true` would be configured and quietly do nothing. An
|
|
12
|
+
// implementation is free to call these wherever its lifecycle allows, but it is
|
|
13
|
+
// no longer free to *re-derive* them.
|
|
14
|
+
import { cmsLogger } from '../utils/logger.js';
|
|
15
|
+
import { isInstanceEmpty, canDetermineInstanceEmptiness } from './instance-state.js';
|
|
16
|
+
import { isPendingInvitation } from './invitation-status.js';
|
|
17
|
+
/**
|
|
18
|
+
* Thrown when sign-up is refused by policy.
|
|
19
|
+
*
|
|
20
|
+
* Providers translate this into whatever their transport expects — Better Auth
|
|
21
|
+
* raises an `APIError`, a REST handler might return 403. The message is written
|
|
22
|
+
* to be safe to show a user.
|
|
23
|
+
*/
|
|
24
|
+
export class SignUpBlockedError extends Error {
|
|
25
|
+
code = 'SIGN_UP_BLOCKED';
|
|
26
|
+
constructor(message) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'SignUpBlockedError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Throws `SignUpBlockedError` unless this address may create an account.
|
|
33
|
+
*
|
|
34
|
+
* Two ways through: the instance is provably empty (nobody exists yet to have
|
|
35
|
+
* sent an invitation, so gating the first sign-up would lock the door with the
|
|
36
|
+
* keys inside), or the address holds a pending invitation.
|
|
37
|
+
*
|
|
38
|
+
* Narrowing that first exception is the bootstrap policy's job — see
|
|
39
|
+
* `resolveBootstrapRole` — not this gate's.
|
|
40
|
+
*/
|
|
41
|
+
export async function assertSignUpAllowed({ db, email, inviteOnly }) {
|
|
42
|
+
if (!inviteOnly)
|
|
43
|
+
return;
|
|
44
|
+
if (await isInstanceEmpty(db)) {
|
|
45
|
+
cmsLogger.info('[SignUp]', 'Allowing first sign-up on an empty instance');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Invitations are stored lower-cased and matched exactly, so a mixed-case
|
|
49
|
+
// sign-up would otherwise find nothing and be refused.
|
|
50
|
+
const address = email?.toLowerCase().trim();
|
|
51
|
+
const invitations = address ? await db.findInvitationsByEmail(address) : [];
|
|
52
|
+
if (!invitations.some((invitation) => isPendingInvitation(invitation))) {
|
|
53
|
+
cmsLogger.warn('[SignUp]', `Blocked sign-up for ${address} — no pending invitation`);
|
|
54
|
+
throw new SignUpBlockedError('Sign-up is by invitation only. Ask an administrator for an invite.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Runs the bootstrap policy for a brand-new user, returning the instance role to
|
|
59
|
+
* grant or `null` for none.
|
|
60
|
+
*
|
|
61
|
+
* Deliberately not wrapped in a transaction. SQLite allows a single writer, and
|
|
62
|
+
* holding that lock across the policy's own round-trips collides with the auth
|
|
63
|
+
* provider's concurrent user/session inserts (SQLITE_BUSY). The atomicity was
|
|
64
|
+
* only partial anyway — see `claimCode`: a claim is single-use, not mutually
|
|
65
|
+
* exclusive.
|
|
66
|
+
*/
|
|
67
|
+
export async function resolveBootstrapRole({ db, user, request, bootstrap }) {
|
|
68
|
+
// "Provably empty", not "couldn't prove otherwise". An adapter that can't
|
|
69
|
+
// answer this must not read as "no users exist" — that inversion once
|
|
70
|
+
// promoted every single sign-up to super admin.
|
|
71
|
+
const isFirstUser = await isInstanceEmpty(db);
|
|
72
|
+
if (!canDetermineInstanceEmptiness(db)) {
|
|
73
|
+
cmsLogger.warn('[SignUp]', 'Database adapter does not implement hasAnyUserProfiles() — skipping bootstrap. ' +
|
|
74
|
+
'Provision the first administrator out of band.');
|
|
75
|
+
}
|
|
76
|
+
return bootstrap({ user, isFirstUser, request, db });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Take the one-time bootstrap claim, or report that somebody else has it.
|
|
80
|
+
*
|
|
81
|
+
* An adapter without `tryClaimBootstrap` keeps the old, racy behaviour rather
|
|
82
|
+
* than never promoting anyone — the opposite of the rule `isInstanceEmpty`
|
|
83
|
+
* follows, and on purpose. There, failing closed skips a promotion; here it
|
|
84
|
+
* would leave a fresh install with no administrator and no way to get one.
|
|
85
|
+
*/
|
|
86
|
+
async function claimBootstrapPromotion(db) {
|
|
87
|
+
const claim = db.tryClaimBootstrap?.bind(db);
|
|
88
|
+
if (!claim) {
|
|
89
|
+
cmsLogger.warn('[SignUp]', 'Database adapter does not implement tryClaimBootstrap() — bootstrap promotion is not ' +
|
|
90
|
+
'race-safe. Concurrent first sign-ups could both be promoted.');
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
return claim();
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The one place a CMS user profile is created for a newly-authenticated user.
|
|
97
|
+
*
|
|
98
|
+
* Every `AuthProvider` should route new profiles through here so bootstrap
|
|
99
|
+
* promotion happens identically regardless of who did the authenticating.
|
|
100
|
+
*/
|
|
101
|
+
export async function createUserProfileWithBootstrap(options) {
|
|
102
|
+
const proposed = await resolveBootstrapRole(options);
|
|
103
|
+
// The policy decided *whether* this user qualifies; the claim decides whether
|
|
104
|
+
// they got there first. Both are needed: the policy's own `isFirstUser` check
|
|
105
|
+
// reads a snapshot taken before the profile insert, so two sign-ups racing on
|
|
106
|
+
// a fresh instance can both pass it and both be promoted. Every promotion in
|
|
107
|
+
// the instance's lifetime funnels through this one call, so claiming here
|
|
108
|
+
// covers every policy — including a custom one — without each having to know
|
|
109
|
+
// about the race.
|
|
110
|
+
const granted = proposed && (await claimBootstrapPromotion(options.db)) ? proposed : null;
|
|
111
|
+
if (proposed && !granted) {
|
|
112
|
+
cmsLogger.warn('[SignUp]', `Bootstrap promotion for ${options.user.id} lost the claim — another sign-up got there first. ` +
|
|
113
|
+
'Creating an ordinary profile instead.');
|
|
114
|
+
}
|
|
115
|
+
cmsLogger.info('[SignUp]', `Creating profile for ${options.user.id}${granted ? ` with role ${granted.toUpperCase()}` : ''}`);
|
|
116
|
+
return options.db.createUserProfile({
|
|
117
|
+
userId: options.user.id,
|
|
118
|
+
role: granted ?? 'editor'
|
|
119
|
+
});
|
|
120
|
+
}
|
|
@@ -7,25 +7,58 @@
|
|
|
7
7
|
import { onMount } from 'svelte';
|
|
8
8
|
import { Button } from '@aphexcms/ui/shadcn/button';
|
|
9
9
|
import { Badge } from '@aphexcms/ui/shadcn/badge';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
RefreshCw,
|
|
12
|
+
CalendarClock,
|
|
13
|
+
Radio,
|
|
14
|
+
Sparkles,
|
|
15
|
+
Undo2,
|
|
16
|
+
ChevronDown,
|
|
17
|
+
RotateCcw,
|
|
18
|
+
Ban,
|
|
19
|
+
TriangleAlert
|
|
20
|
+
} from '@lucide/svelte';
|
|
11
21
|
import { toast } from 'svelte-sonner';
|
|
12
22
|
import { apiClient } from '../../api/client';
|
|
13
|
-
import type { Job, DomainEvent, JobStatus } from '../../types/events';
|
|
23
|
+
import type { Job, DomainEvent, JobStatus, OutboxHealth } from '../../types/events';
|
|
14
24
|
import type {
|
|
15
25
|
AgentChangeSet,
|
|
16
26
|
AgentChangeSetWithOperations,
|
|
17
27
|
AgentChangeSetStatus
|
|
18
28
|
} from '../../types/agent-change-sets';
|
|
19
29
|
|
|
30
|
+
type Props = {
|
|
31
|
+
/**
|
|
32
|
+
* May the viewer retry/cancel jobs? `org.settings` — see `requireJobControl` in the
|
|
33
|
+
* route. Gating here only hides buttons; the server is what enforces it.
|
|
34
|
+
*/
|
|
35
|
+
canControlJobs?: boolean;
|
|
36
|
+
/** Super admins can widen jobs/events past their active organization (`?scope=all`). */
|
|
37
|
+
isSuperAdmin?: boolean;
|
|
38
|
+
};
|
|
39
|
+
let { canControlJobs = false, isSuperAdmin = false }: Props = $props();
|
|
40
|
+
|
|
20
41
|
// The server resolves `createdBy` (a raw user id) to a display name — see
|
|
21
42
|
// `resolve-created-by.ts`, shared by GET /api/events and GET /api/agent/change-sets.
|
|
22
43
|
type WithCreatedByName<T> = T & { createdByName: string | null };
|
|
44
|
+
// …and `organizationId` to an org name, but only in the instance-wide view, where rows
|
|
45
|
+
// from several tenants share a table. Absent (not null) in the org-scoped view.
|
|
46
|
+
type WithOrganizationName<T> = T & { organizationName?: string | null };
|
|
23
47
|
|
|
24
48
|
type Tab = 'jobs' | 'events' | 'agent';
|
|
25
49
|
let tab = $state<Tab>('jobs');
|
|
26
50
|
|
|
27
|
-
|
|
28
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Whose history is on screen. `all` is a super-admin-only widening: the queue is
|
|
53
|
+
* instance-wide (one worker claims across every org), so a failure in a tenant you
|
|
54
|
+
* aren't currently switched into is otherwise invisible.
|
|
55
|
+
*/
|
|
56
|
+
let scope = $state<'organization' | 'all'>('organization');
|
|
57
|
+
|
|
58
|
+
let jobs = $state<WithOrganizationName<Job>[]>([]);
|
|
59
|
+
let events = $state<WithOrganizationName<WithCreatedByName<DomainEvent>>[]>([]);
|
|
60
|
+
let health = $state<OutboxHealth | null>(null);
|
|
61
|
+
let actingJobId = $state<string | null>(null);
|
|
29
62
|
let changeSets = $state<WithCreatedByName<AgentChangeSetWithOperations>[]>([]);
|
|
30
63
|
let loading = $state(false);
|
|
31
64
|
let error = $state<string | null>(null);
|
|
@@ -89,24 +122,86 @@
|
|
|
89
122
|
return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString();
|
|
90
123
|
}
|
|
91
124
|
|
|
125
|
+
/** "3m", "2h", "4d" — coarse on purpose; this is a staleness signal, not a stopwatch. */
|
|
126
|
+
function age(d: string | Date): string {
|
|
127
|
+
const ms = Date.now() - new Date(d).getTime();
|
|
128
|
+
if (ms < 60_000) return `${Math.max(0, Math.round(ms / 1000))}s`;
|
|
129
|
+
if (ms < 3_600_000) return `${Math.round(ms / 60_000)}m`;
|
|
130
|
+
if (ms < 86_400_000) return `${Math.round(ms / 3_600_000)}h`;
|
|
131
|
+
return `${Math.round(ms / 86_400_000)}d`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A backlog older than this means the relay has stopped, not that it's busy. The
|
|
135
|
+
// self-hosted worker polls every 5s by default and platform cron runs at most once a
|
|
136
|
+
// minute, so a minute is the shortest gap that can't be explained by normal cadence.
|
|
137
|
+
const RELAY_STALE_MS = 60_000;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The backlog, pre-chewed into exactly what the banner renders — or null when there's
|
|
141
|
+
* nothing to say. Deriving the whole object (rather than a `stalled` flag beside the raw
|
|
142
|
+
* `health`) is what lets the markup read `oldestAge` without re-proving that
|
|
143
|
+
* `oldestPendingAt` is non-null: `pending > 0` implies a row exists, but only this
|
|
144
|
+
* narrowing tells the compiler so.
|
|
145
|
+
*/
|
|
146
|
+
const relayBacklog = $derived.by(() => {
|
|
147
|
+
const oldest = health?.oldestPendingAt;
|
|
148
|
+
if (!health || health.pending === 0 || !oldest) return null;
|
|
149
|
+
return {
|
|
150
|
+
pending: health.pending,
|
|
151
|
+
oldestAge: age(oldest),
|
|
152
|
+
stalled: Date.now() - new Date(oldest).getTime() > RELAY_STALE_MS
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A job whose lease has run out but which is still marked `leased` — the worker holding
|
|
158
|
+
* it died mid-run. It isn't lost (the next `claimDueJobs` reclaims expired leases), but
|
|
159
|
+
* it looks identical to healthy in-flight work without this.
|
|
160
|
+
*/
|
|
161
|
+
function leaseExpired(job: Job): boolean {
|
|
162
|
+
return (
|
|
163
|
+
job.status === 'leased' && !!job.leaseExpiresAt && new Date(job.leaseExpiresAt) < new Date()
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Requeue puts a dead letter back with a fresh attempt budget; cancel retires it. */
|
|
168
|
+
const canRetry = (job: Job) => job.status === 'failed' || job.status === 'cancelled';
|
|
169
|
+
const canCancel = (job: Job) => job.status === 'pending' || job.status === 'failed';
|
|
170
|
+
|
|
171
|
+
/** Only send `scope` when it's actually widened — keeps the default request unchanged. */
|
|
172
|
+
function scopeParam(): Record<string, string> {
|
|
173
|
+
return scope === 'all' ? { scope: 'all' } : {};
|
|
174
|
+
}
|
|
175
|
+
|
|
92
176
|
async function load() {
|
|
93
177
|
loading = true;
|
|
94
178
|
error = null;
|
|
95
179
|
try {
|
|
96
180
|
if (tab === 'jobs') {
|
|
97
|
-
const params: Record<string, string> = { limit: '100' };
|
|
181
|
+
const params: Record<string, string> = { limit: '100', ...scopeParam() };
|
|
98
182
|
if (statusFilter !== 'all') params.status = statusFilter;
|
|
99
|
-
|
|
183
|
+
// Health rides along with the list: it's the context for everything below it,
|
|
184
|
+
// and a separate refresh button for one number would be its own small trap.
|
|
185
|
+
const [res, healthRes] = await Promise.all([
|
|
186
|
+
apiClient.get<WithOrganizationName<Job>[]>('/jobs', params),
|
|
187
|
+
apiClient.get<OutboxHealth>('/jobs/health', scopeParam())
|
|
188
|
+
]);
|
|
100
189
|
if (res.success) {
|
|
101
190
|
jobs = res.data ?? [];
|
|
102
191
|
total = res.pagination?.total ?? jobs.length;
|
|
103
192
|
} else {
|
|
104
193
|
error = res.error ?? 'Failed to load jobs';
|
|
105
194
|
}
|
|
195
|
+
// A failed health read shouldn't blank the page — the list is still useful.
|
|
196
|
+
health = healthRes.success ? (healthRes.data ?? null) : null;
|
|
106
197
|
} else if (tab === 'events') {
|
|
107
|
-
const res = await apiClient.get<WithCreatedByName<DomainEvent
|
|
108
|
-
|
|
109
|
-
|
|
198
|
+
const res = await apiClient.get<WithOrganizationName<WithCreatedByName<DomainEvent>>[]>(
|
|
199
|
+
'/events',
|
|
200
|
+
{
|
|
201
|
+
limit: '100',
|
|
202
|
+
...scopeParam()
|
|
203
|
+
}
|
|
204
|
+
);
|
|
110
205
|
if (res.success) {
|
|
111
206
|
events = res.data ?? [];
|
|
112
207
|
total = res.pagination?.total ?? events.length;
|
|
@@ -138,6 +233,34 @@
|
|
|
138
233
|
load();
|
|
139
234
|
}
|
|
140
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Retry and cancel share everything but the verb and the past-tense confirmation, so they
|
|
238
|
+
* share a body. `organizationId` is always sent: in the instance-wide view the job may
|
|
239
|
+
* belong to a tenant the caller isn't switched into, and the server needs to be told which
|
|
240
|
+
* one (it rejects the mismatch unless you're a super admin).
|
|
241
|
+
*/
|
|
242
|
+
async function actOnJob(job: Job, action: 'retry' | 'cancel') {
|
|
243
|
+
if (actingJobId) return;
|
|
244
|
+
actingJobId = job.id;
|
|
245
|
+
try {
|
|
246
|
+
const res = await apiClient.post<Job>(`/jobs/${job.id}/${action}`, {
|
|
247
|
+
organizationId: job.organizationId
|
|
248
|
+
});
|
|
249
|
+
if (!res.success) {
|
|
250
|
+
toast.error(res.error ?? `Could not ${action} this job`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
toast.success(
|
|
254
|
+
action === 'retry' ? 'Job requeued — it runs on the next tick.' : 'Job cancelled.'
|
|
255
|
+
);
|
|
256
|
+
await load();
|
|
257
|
+
} catch (err) {
|
|
258
|
+
toast.error(err instanceof Error ? err.message : `Could not ${action} this job`);
|
|
259
|
+
} finally {
|
|
260
|
+
actingJobId = null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
141
264
|
async function toggleExpand(changeSet: AgentChangeSet) {
|
|
142
265
|
if (expandedChangeSetId === changeSet.id) {
|
|
143
266
|
expandedChangeSetId = null;
|
|
@@ -196,6 +319,13 @@
|
|
|
196
319
|
if (tab === 'jobs') load();
|
|
197
320
|
});
|
|
198
321
|
|
|
322
|
+
// Scope applies to jobs and events alike, so this one reloads whichever is showing.
|
|
323
|
+
$effect(() => {
|
|
324
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reactive dependency: re-run when scope changes
|
|
325
|
+
scope;
|
|
326
|
+
if (tab !== 'agent') load();
|
|
327
|
+
});
|
|
328
|
+
|
|
199
329
|
onMount(load);
|
|
200
330
|
</script>
|
|
201
331
|
|
|
@@ -207,9 +337,32 @@
|
|
|
207
337
|
Scheduled jobs, the domain-event log, and the AI assistant's audit trail.
|
|
208
338
|
</p>
|
|
209
339
|
</div>
|
|
210
|
-
<
|
|
211
|
-
|
|
212
|
-
|
|
340
|
+
<div class="flex items-center gap-2">
|
|
341
|
+
{#if isSuperAdmin && tab !== 'agent'}
|
|
342
|
+
<!-- The queue is instance-wide; this is the only view that can show it that way. -->
|
|
343
|
+
<div class="border-rule flex items-center rounded-md border p-0.5 text-xs">
|
|
344
|
+
<button
|
|
345
|
+
class="rounded px-2 py-1 transition-colors {scope === 'organization'
|
|
346
|
+
? 'bg-muted text-foreground'
|
|
347
|
+
: 'text-muted-foreground hover:text-foreground'}"
|
|
348
|
+
onclick={() => (scope = 'organization')}
|
|
349
|
+
>
|
|
350
|
+
This workspace
|
|
351
|
+
</button>
|
|
352
|
+
<button
|
|
353
|
+
class="rounded px-2 py-1 transition-colors {scope === 'all'
|
|
354
|
+
? 'bg-muted text-foreground'
|
|
355
|
+
: 'text-muted-foreground hover:text-foreground'}"
|
|
356
|
+
onclick={() => (scope = 'all')}
|
|
357
|
+
>
|
|
358
|
+
All workspaces
|
|
359
|
+
</button>
|
|
360
|
+
</div>
|
|
361
|
+
{/if}
|
|
362
|
+
<Button variant="outline" size="sm" onclick={load} disabled={loading} class="gap-1.5">
|
|
363
|
+
<RefreshCw class="h-3.5 w-3.5 {loading ? 'animate-spin' : ''}" /> Refresh
|
|
364
|
+
</Button>
|
|
365
|
+
</div>
|
|
213
366
|
</div>
|
|
214
367
|
|
|
215
368
|
<!-- Tabs -->
|
|
@@ -244,6 +397,43 @@
|
|
|
244
397
|
</div>
|
|
245
398
|
|
|
246
399
|
{#if tab === 'jobs'}
|
|
400
|
+
{#if relayBacklog}
|
|
401
|
+
<!--
|
|
402
|
+
The relay backlog. A count on its own is meaningless — a busy instance always has
|
|
403
|
+
rows in flight — so the age of the oldest is what decides the tone: past a minute,
|
|
404
|
+
nothing is draining the outbox and every consumer has silently stopped.
|
|
405
|
+
-->
|
|
406
|
+
<div
|
|
407
|
+
class="mb-3 flex items-start gap-2 rounded-md border p-3 text-sm {relayBacklog.stalled
|
|
408
|
+
? 'border-destructive/40 bg-destructive/10 text-destructive'
|
|
409
|
+
: 'border-rule text-muted-foreground'}"
|
|
410
|
+
>
|
|
411
|
+
{#if relayBacklog.stalled}
|
|
412
|
+
<TriangleAlert class="mt-0.5 h-4 w-4 shrink-0" />
|
|
413
|
+
{:else}
|
|
414
|
+
<Radio class="mt-0.5 h-4 w-4 shrink-0" />
|
|
415
|
+
{/if}
|
|
416
|
+
<div>
|
|
417
|
+
{#if relayBacklog.stalled}
|
|
418
|
+
<p class="font-medium">
|
|
419
|
+
The relay looks stopped — {relayBacklog.pending} event{relayBacklog.pending === 1
|
|
420
|
+
? ''
|
|
421
|
+
: 's'} waiting, oldest {relayBacklog.oldestAge} old.
|
|
422
|
+
</p>
|
|
423
|
+
<p class="mt-0.5 text-xs">
|
|
424
|
+
Nothing is reacting to events: scheduled publishes, erasure and plugin consumers are
|
|
425
|
+
all paused until a worker calls <code>POST /api/internal/workers/run</code> again.
|
|
426
|
+
</p>
|
|
427
|
+
{:else}
|
|
428
|
+
<p>
|
|
429
|
+
{relayBacklog.pending} event{relayBacklog.pending === 1 ? '' : 's'} waiting to fan out,
|
|
430
|
+
oldest {relayBacklog.oldestAge} old — the relay is keeping up.
|
|
431
|
+
</p>
|
|
432
|
+
{/if}
|
|
433
|
+
</div>
|
|
434
|
+
</div>
|
|
435
|
+
{/if}
|
|
436
|
+
|
|
247
437
|
<div class="mb-3 flex flex-wrap items-center gap-1.5">
|
|
248
438
|
{#each statuses as s (s)}
|
|
249
439
|
<button
|
|
@@ -276,19 +466,42 @@
|
|
|
276
466
|
<thead class="bg-muted/50 text-muted-foreground text-xs">
|
|
277
467
|
<tr>
|
|
278
468
|
<th class="px-3 py-2 text-left font-medium">Type</th>
|
|
469
|
+
{#if scope === 'all'}
|
|
470
|
+
<th class="px-3 py-2 text-left font-medium">Workspace</th>
|
|
471
|
+
{/if}
|
|
279
472
|
<th class="px-3 py-2 text-left font-medium">Status</th>
|
|
280
473
|
<th class="px-3 py-2 text-left font-medium">Run at</th>
|
|
281
474
|
<th class="px-3 py-2 text-left font-medium">Attempts</th>
|
|
282
475
|
<th class="px-3 py-2 text-left font-medium">Last error</th>
|
|
283
476
|
<th class="px-3 py-2 text-left font-medium">Created</th>
|
|
477
|
+
{#if canControlJobs}
|
|
478
|
+
<th class="px-3 py-2 text-right font-medium">Actions</th>
|
|
479
|
+
{/if}
|
|
284
480
|
</tr>
|
|
285
481
|
</thead>
|
|
286
482
|
<tbody>
|
|
287
483
|
{#each jobs as job (job.id)}
|
|
288
484
|
<tr class="border-rule border-t">
|
|
289
485
|
<td class="px-3 py-2 font-mono text-xs">{job.type}</td>
|
|
290
|
-
|
|
486
|
+
{#if scope === 'all'}
|
|
487
|
+
<td class="text-muted-foreground px-3 py-2 text-xs">
|
|
488
|
+
{job.organizationName ?? job.organizationId}
|
|
489
|
+
</td>
|
|
490
|
+
{/if}
|
|
491
|
+
<td class="px-3 py-2 whitespace-nowrap">
|
|
291
492
|
<Badge variant={statusVariant[job.status]} class="capitalize">{job.status}</Badge>
|
|
493
|
+
{#if leaseExpired(job)}
|
|
494
|
+
<!-- Still `leased` past its expiry: the worker that held it died. The next
|
|
495
|
+
claim reclaims it, so this is a diagnosis, not an action item. -->
|
|
496
|
+
<span
|
|
497
|
+
class="text-destructive ml-1.5 text-xs"
|
|
498
|
+
title="Lease expired {age(
|
|
499
|
+
job.leaseExpiresAt ?? new Date()
|
|
500
|
+
)} ago — the worker holding this job stopped. It will be reclaimed on the next tick."
|
|
501
|
+
>
|
|
502
|
+
stalled
|
|
503
|
+
</span>
|
|
504
|
+
{/if}
|
|
292
505
|
</td>
|
|
293
506
|
<td class="px-3 py-2 whitespace-nowrap">{fmt(job.runAt)}</td>
|
|
294
507
|
<td class="px-3 py-2">{job.attempts}/{job.maxAttempts}</td>
|
|
@@ -301,6 +514,35 @@
|
|
|
301
514
|
<td class="text-muted-foreground px-3 py-2 whitespace-nowrap"
|
|
302
515
|
>{fmt(job.createdAt)}</td
|
|
303
516
|
>
|
|
517
|
+
{#if canControlJobs}
|
|
518
|
+
<td class="px-3 py-2 text-right whitespace-nowrap">
|
|
519
|
+
{#if canRetry(job)}
|
|
520
|
+
<Button
|
|
521
|
+
variant="ghost"
|
|
522
|
+
size="sm"
|
|
523
|
+
class="h-7 gap-1 px-2 text-xs"
|
|
524
|
+
disabled={actingJobId !== null}
|
|
525
|
+
onclick={() => actOnJob(job, 'retry')}
|
|
526
|
+
>
|
|
527
|
+
<RotateCcw class="h-3 w-3" /> Retry
|
|
528
|
+
</Button>
|
|
529
|
+
{/if}
|
|
530
|
+
{#if canCancel(job)}
|
|
531
|
+
<Button
|
|
532
|
+
variant="ghost"
|
|
533
|
+
size="sm"
|
|
534
|
+
class="text-muted-foreground h-7 gap-1 px-2 text-xs"
|
|
535
|
+
disabled={actingJobId !== null}
|
|
536
|
+
onclick={() => actOnJob(job, 'cancel')}
|
|
537
|
+
>
|
|
538
|
+
<Ban class="h-3 w-3" /> Cancel
|
|
539
|
+
</Button>
|
|
540
|
+
{/if}
|
|
541
|
+
{#if !canRetry(job) && !canCancel(job)}
|
|
542
|
+
<span class="text-muted-foreground text-xs">—</span>
|
|
543
|
+
{/if}
|
|
544
|
+
</td>
|
|
545
|
+
{/if}
|
|
304
546
|
</tr>
|
|
305
547
|
{/each}
|
|
306
548
|
</tbody>
|
|
@@ -317,6 +559,9 @@
|
|
|
317
559
|
<thead class="bg-muted/50 text-muted-foreground text-xs">
|
|
318
560
|
<tr>
|
|
319
561
|
<th class="px-3 py-2 text-left font-medium">Type</th>
|
|
562
|
+
{#if scope === 'all'}
|
|
563
|
+
<th class="px-3 py-2 text-left font-medium">Workspace</th>
|
|
564
|
+
{/if}
|
|
320
565
|
<th class="px-3 py-2 text-left font-medium">Payload</th>
|
|
321
566
|
<th class="px-3 py-2 text-left font-medium">By</th>
|
|
322
567
|
<th class="px-3 py-2 text-left font-medium">When</th>
|
|
@@ -326,6 +571,11 @@
|
|
|
326
571
|
{#each events as event (event.id)}
|
|
327
572
|
<tr class="border-rule border-t">
|
|
328
573
|
<td class="px-3 py-2 font-mono text-xs">{event.type}</td>
|
|
574
|
+
{#if scope === 'all'}
|
|
575
|
+
<td class="text-muted-foreground px-3 py-2 text-xs">
|
|
576
|
+
{event.organizationName ?? event.organizationId}
|
|
577
|
+
</td>
|
|
578
|
+
{/if}
|
|
329
579
|
<td
|
|
330
580
|
class="text-muted-foreground max-w-[320px] truncate px-3 py-2 font-mono text-xs"
|
|
331
581
|
title={JSON.stringify(event.payload)}
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
type Props = {
|
|
2
|
+
/**
|
|
3
|
+
* May the viewer retry/cancel jobs? `org.settings` — see `requireJobControl` in the
|
|
4
|
+
* route. Gating here only hides buttons; the server is what enforces it.
|
|
5
|
+
*/
|
|
6
|
+
canControlJobs?: boolean;
|
|
7
|
+
/** Super admins can widen jobs/events past their active organization (`?scope=all`). */
|
|
8
|
+
isSuperAdmin?: boolean;
|
|
9
|
+
};
|
|
10
|
+
declare const ActivityView: import("svelte").Component<Props, {}, "">;
|
|
2
11
|
type ActivityView = ReturnType<typeof ActivityView>;
|
|
3
12
|
export default ActivityView;
|
|
4
13
|
//# sourceMappingURL=ActivityView.svelte.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActivityView.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/ActivityView.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ActivityView.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/components/admin/ActivityView.svelte.ts"],"names":[],"mappings":"AAgCC,KAAK,KAAK,GAAG;IACZ;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,wFAAwF;IACxF,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AA8kBH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DomainEvent, AppendEventInput, Job, ScheduleJobInput, ClaimJobsOptions, ListEventsOptions, ListJobsOptions, OutboxRow, ListUnprocessedOutboxOptions, Page } from '../../types/events.js';
|
|
1
|
+
import type { DomainEvent, AppendEventInput, Job, ScheduleJobInput, ClaimJobsOptions, ListEventsOptions, ListJobsOptions, OutboxRow, ListUnprocessedOutboxOptions, OutboxHealth, Page } from '../../types/events.js';
|
|
2
2
|
/**
|
|
3
3
|
* The durable spine: an append-only domain-event log plus a DB-backed job queue.
|
|
4
4
|
* All rows are organization-scoped. (The event-log read API — `listEvents` — lands
|
|
@@ -25,6 +25,13 @@ export interface EventJobAdapter {
|
|
|
25
25
|
listUnprocessedOutbox(options: ListUnprocessedOutboxOptions): Promise<OutboxRow[]>;
|
|
26
26
|
/** Stamp `processedAt` once an outbox row has been fanned out to every subscriber. */
|
|
27
27
|
markOutboxProcessed(organizationId: string, id: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Size and age of the unprocessed backlog — the "is the relay running?" signal.
|
|
30
|
+
* Omit `organizationId` for the instance-wide figure. See {@link OutboxHealth}.
|
|
31
|
+
*/
|
|
32
|
+
outboxHealth(options: {
|
|
33
|
+
organizationId?: string;
|
|
34
|
+
}): Promise<OutboxHealth>;
|
|
28
35
|
/** Schedule a job. Idempotent when `idempotencyKey` is set: a duplicate key returns the existing job. */
|
|
29
36
|
scheduleJob(input: ScheduleJobInput): Promise<Job>;
|
|
30
37
|
/**
|
|
@@ -49,7 +56,27 @@ export interface EventJobAdapter {
|
|
|
49
56
|
}): Promise<void>;
|
|
50
57
|
/** Cancel a job (e.g. a scheduled publish the user called off): status → `cancelled`, clear the lease. */
|
|
51
58
|
cancelJob(organizationId: string, id: string): Promise<void>;
|
|
52
|
-
/**
|
|
59
|
+
/** Read a single job by id (org-scoped). Null when it doesn't exist in that org. */
|
|
60
|
+
getJob(organizationId: string, id: string): Promise<Job | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Put a finished job back on the queue by hand — the operator's undo for a dead letter.
|
|
63
|
+
*
|
|
64
|
+
* Distinct from `retryJob`, which is the *runner's* backoff transition and deliberately
|
|
65
|
+
* leaves `attempts` alone. A dead-lettered job sits at `attempts === maxAttempts`, so
|
|
66
|
+
* reusing `retryJob` would hand it back to the runner only for the next claim to exhaust
|
|
67
|
+
* it again immediately. This resets the attempt counter and clears `lastError`, giving
|
|
68
|
+
* the job a fresh budget once whatever broke has been fixed.
|
|
69
|
+
*
|
|
70
|
+
* Guarded to `failed` and `cancelled` in SQL: requeueing a `pending` job is pointless and
|
|
71
|
+
* requeueing a `leased` one would race the worker currently holding it, whose settle would
|
|
72
|
+
* then overwrite the status. Returns the updated job, or null when the id doesn't exist in
|
|
73
|
+
* the org or wasn't in a requeueable state — so callers can tell "gone" from "not allowed"
|
|
74
|
+
* with a follow-up `getJob`.
|
|
75
|
+
*/
|
|
76
|
+
requeueJob(organizationId: string, id: string, options: {
|
|
77
|
+
runAt: Date;
|
|
78
|
+
}): Promise<Job | null>;
|
|
79
|
+
/** List jobs, newest first (read-only history / observability). Omit `organizationId` for instance-wide. */
|
|
53
80
|
listJobs(options: ListJobsOptions): Promise<Page<Job>>;
|
|
54
81
|
}
|
|
55
82
|
//# sourceMappingURL=events.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../src/lib/db/interfaces/events.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,WAAW,EACX,gBAAgB,EAChB,GAAG,EACH,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,4BAA4B,EAC5B,IAAI,EACJ,MAAM,oBAAoB,CAAC;AAE5B;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAE/B;;;;;;OAMG;IACH,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC3D,8CAA8C;IAC9C,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1E,wFAAwF;IACxF,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAGnE;;;;OAIG;IACH,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACnF,sFAAsF;IACtF,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../../src/lib/db/interfaces/events.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,WAAW,EACX,gBAAgB,EAChB,GAAG,EACH,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,SAAS,EACT,4BAA4B,EAC5B,YAAY,EACZ,IAAI,EACJ,MAAM,oBAAoB,CAAC;AAE5B;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAE/B;;;;;;OAMG;IACH,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC3D,8CAA8C;IAC9C,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1E,wFAAwF;IACxF,UAAU,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;IAGnE;;;;OAIG;IACH,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IACnF,sFAAsF;IACtF,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE;QAAE,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAG1E,yGAAyG;IACzG,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;IACnD;;;OAGG;IACH,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACxD,oCAAoC;IACpC,WAAW,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CACP,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE;QAAE,KAAK,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GACrC,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,uFAAuF;IACvF,OAAO,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvF,0GAA0G;IAC1G,SAAS,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,oFAAoF;IACpF,MAAM,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAChE;;;;;;;;;;;;;;OAcG;IACH,UAAU,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,KAAK,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAC9F,4GAA4G;IAC5G,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;CACvD"}
|
|
@@ -17,6 +17,7 @@ export type { UserProfileAdapter, NewUserProfileData } from './user.js';
|
|
|
17
17
|
export type { SchemaAdapter } from './schema.js';
|
|
18
18
|
export type { OrganizationAdapter } from './organization.js';
|
|
19
19
|
export type { InstanceAdapter, InstanceSettings } from './instance.js';
|
|
20
|
+
export { BOOTSTRAP_CLAIM_ID } from './instance.js';
|
|
20
21
|
export type { RolesAdapter } from './role.js';
|
|
21
22
|
export type { ReferenceAdapter, BackReferenceRow, BackReferenceLookup } from './reference.js';
|
|
22
23
|
export type { PluginSettingsAdapter, PluginSettingsRow } from './plugin-settings.js';
|