@beechcms/core 0.4.0-preview.9 → 0.4.1
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/auth/hash-provider.d.ts +13 -0
- package/dist/auth/hash-provider.d.ts.map +1 -0
- package/dist/auth/hash-provider.js +1 -0
- package/dist/auth/password-reset-token.repository.d.ts +28 -0
- package/dist/auth/password-reset-token.repository.d.ts.map +1 -0
- package/dist/auth/password-reset-token.repository.js +1 -0
- package/dist/auth/session.repository.d.ts +47 -0
- package/dist/auth/session.repository.d.ts.map +1 -0
- package/dist/auth/session.repository.js +1 -0
- package/dist/auth/token-service.d.ts +23 -0
- package/dist/auth/token-service.d.ts.map +1 -0
- package/dist/auth/token-service.js +1 -0
- package/dist/auth/user.repository.d.ts +47 -0
- package/dist/auth/user.repository.d.ts.map +1 -0
- package/dist/auth/user.repository.js +1 -0
- package/dist/clock.d.ts +25 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +9 -0
- package/dist/content-scan.repository.d.ts +9 -0
- package/dist/content-scan.repository.d.ts.map +1 -0
- package/dist/content-scan.repository.js +1 -0
- package/dist/content.repository.d.ts +95 -0
- package/dist/content.repository.d.ts.map +1 -0
- package/dist/content.repository.js +29 -0
- package/dist/engine.d.ts +24 -24
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +98 -34
- package/dist/id-generator.d.ts +19 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +7 -0
- package/dist/idempotency.repository.d.ts +13 -0
- package/dist/idempotency.repository.d.ts.map +1 -0
- package/dist/idempotency.repository.js +1 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -3
- package/dist/media.repository.d.ts +61 -0
- package/dist/media.repository.d.ts.map +1 -0
- package/dist/media.repository.js +1 -0
- package/dist/notifications/notification-service.d.ts +29 -0
- package/dist/notifications/notification-service.d.ts.map +1 -0
- package/dist/notifications/notification-service.js +1 -0
- package/dist/notifications/notification.repository.d.ts +51 -0
- package/dist/notifications/notification.repository.d.ts.map +1 -0
- package/dist/notifications/notification.repository.js +10 -0
- package/dist/observability/activity-log.repository.d.ts +58 -0
- package/dist/observability/activity-log.repository.d.ts.map +1 -0
- package/dist/observability/activity-log.repository.js +1 -0
- package/dist/observability/activity-logger.d.ts +49 -0
- package/dist/observability/activity-logger.d.ts.map +1 -0
- package/dist/observability/activity-logger.js +12 -0
- package/dist/observability/analytics.repository.d.ts +37 -0
- package/dist/observability/analytics.repository.d.ts.map +1 -0
- package/dist/observability/analytics.repository.js +1 -0
- package/dist/rate-limit/rate-limiter.d.ts +13 -0
- package/dist/rate-limit/rate-limiter.d.ts.map +1 -0
- package/dist/rate-limit/rate-limiter.js +1 -0
- package/dist/search/search.repository.d.ts +45 -0
- package/dist/search/search.repository.d.ts.map +1 -0
- package/dist/search/search.repository.js +1 -0
- package/dist/seed-registry.d.ts +49 -0
- package/dist/seed-registry.d.ts.map +1 -0
- package/dist/seed-registry.js +29 -0
- package/dist/storage.d.ts +72 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +1 -0
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +7 -7
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +15 -9
- package/dist/widget/widget.repository.d.ts +113 -0
- package/dist/widget/widget.repository.d.ts.map +1 -0
- package/dist/widget/widget.repository.js +1 -0
- package/package.json +9 -3
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface IHashProvider {
|
|
2
|
+
/**
|
|
3
|
+
* Hashes a plaintext password using a one-way algorithm so that the original
|
|
4
|
+
* value can never be recovered from the stored digest.
|
|
5
|
+
*/
|
|
6
|
+
hash(plaintextPassword: string): Promise<string>;
|
|
7
|
+
/**
|
|
8
|
+
* Verifies a plaintext password against a stored hash using a constant-time
|
|
9
|
+
* comparison to prevent timing-based attacks that could reveal whether a hash exists.
|
|
10
|
+
*/
|
|
11
|
+
verify(plaintextPassword: string, storedHash: string): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=hash-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hash-provider.d.ts","sourceRoot":"","sources":["../../src/auth/hash-provider.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,IAAI,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEhD;;;OAGG;IACH,MAAM,CAAC,iBAAiB,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACxE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface NewPasswordResetToken {
|
|
2
|
+
userId: string;
|
|
3
|
+
tokenHash: string;
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
}
|
|
6
|
+
export interface ValidatedResetToken {
|
|
7
|
+
id: string;
|
|
8
|
+
userId: string;
|
|
9
|
+
email: string;
|
|
10
|
+
}
|
|
11
|
+
export interface IPasswordResetTokenRepository {
|
|
12
|
+
/**
|
|
13
|
+
* Marks all pending (unused) tokens for the user as consumed before issuing a new one.
|
|
14
|
+
* Ensures only one active reset token exists per user at any time.
|
|
15
|
+
*/
|
|
16
|
+
invalidatePending(userId: string, nowTimestamp: number): Promise<void>;
|
|
17
|
+
/** Stores a new password reset token. Only the hash is persisted, never the plaintext. */
|
|
18
|
+
create(record: NewPasswordResetToken): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* Finds a valid reset token by its hash, joining the users table to return the
|
|
21
|
+
* associated email in the same query to avoid a second round-trip.
|
|
22
|
+
* Returns null if the token is expired, already used, or not found.
|
|
23
|
+
*/
|
|
24
|
+
findValidByHashWithEmail(tokenHash: string, nowTimestamp: number): Promise<ValidatedResetToken | null>;
|
|
25
|
+
/** Marks a token as consumed so it cannot be used again. */
|
|
26
|
+
markUsed(tokenId: string, nowTimestamp: number): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=password-reset-token.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"password-reset-token.repository.d.ts","sourceRoot":"","sources":["../../src/auth/password-reset-token.repository.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,6BAA6B;IAC5C;;;OAGG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEtE,0FAA0F;IAC1F,MAAM,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEpD;;;;OAIG;IACH,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC,CAAA;IAEtG,4DAA4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface NewRefreshToken {
|
|
2
|
+
id: string;
|
|
3
|
+
userId: string;
|
|
4
|
+
tokenHash: string;
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
}
|
|
7
|
+
export interface RefreshTokenRecord {
|
|
8
|
+
id: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
tokenHash: string;
|
|
11
|
+
expiresAt: number;
|
|
12
|
+
createdAt: number;
|
|
13
|
+
revokedAt: number | null;
|
|
14
|
+
}
|
|
15
|
+
export interface ActiveSessionSummary {
|
|
16
|
+
id: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
expiresAt: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ISessionRepository {
|
|
21
|
+
/** Stores a new refresh token record. Only the hash is persisted, never the plaintext. */
|
|
22
|
+
saveRefreshToken(record: NewRefreshToken): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Finds an active refresh token by its hash.
|
|
25
|
+
* nowTimestamp is compared against expiresAt and revokedAt to guarantee the token
|
|
26
|
+
* is both unexpired and not revoked before returning it.
|
|
27
|
+
*/
|
|
28
|
+
findActiveByHash(tokenHash: string, nowTimestamp: number): Promise<RefreshTokenRecord | null>;
|
|
29
|
+
/**
|
|
30
|
+
* Marks a refresh token as revoked so it cannot be used again.
|
|
31
|
+
* Returns true if the token was found and revoked, false if it was already revoked or absent.
|
|
32
|
+
*/
|
|
33
|
+
revokeByHash(tokenHash: string, nowTimestamp: number): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Revokes all active refresh tokens for a user.
|
|
36
|
+
* Must be called on password change to invalidate all existing sessions immediately.
|
|
37
|
+
*/
|
|
38
|
+
revokeAllForUser(userId: string, nowTimestamp: number): Promise<void>;
|
|
39
|
+
/** Returns a paginated list of active sessions for the user, ordered newest first. */
|
|
40
|
+
listActiveForUser(userId: string, nowTimestamp: number, limit: number): Promise<ActiveSessionSummary[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Revokes a specific session by its database ID, scoped to the owning user
|
|
43
|
+
* to prevent one user from revoking another user's sessions.
|
|
44
|
+
*/
|
|
45
|
+
revokeById(sessionId: string, userId: string, nowTimestamp: number): Promise<boolean>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=session.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.repository.d.ts","sourceRoot":"","sources":["../../src/auth/session.repository.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,0FAA0F;IAC1F,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAExD;;;;OAIG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAA;IAE7F;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAEvE;;;OAGG;IACH,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAErE,sFAAsF;IACtF,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,EAAE,CAAC,CAAA;IAEvG;;;OAGG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CACtF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface JwtClaims {
|
|
2
|
+
sub: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface IssueTokenOptions {
|
|
8
|
+
/** Time to live in seconds. Defaults to 900 (15 minutes). */
|
|
9
|
+
ttlSeconds?: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ITokenService {
|
|
12
|
+
/**
|
|
13
|
+
* Issues a signed JWT for the given claims. The token is short-lived by design;
|
|
14
|
+
* callers that need a longer-lived session should use a refresh token instead.
|
|
15
|
+
*/
|
|
16
|
+
issue(claims: JwtClaims, options?: IssueTokenOptions): Promise<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Verifies a JWT and returns its decoded claims, or null on ANY failure
|
|
19
|
+
* (expired, tampered, wrong issuer/audience, malformed). Never throws.
|
|
20
|
+
*/
|
|
21
|
+
verify(token: string): Promise<JwtClaims | null>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=token-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-service.d.ts","sourceRoot":"","sources":["../../src/auth/token-service.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEtE;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAA;CACjD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface UserRecord {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string | null;
|
|
5
|
+
passwordHash: string;
|
|
6
|
+
role: string;
|
|
7
|
+
avatarUrl: string | null;
|
|
8
|
+
notificationPreferences: string;
|
|
9
|
+
}
|
|
10
|
+
export interface NewUserInput {
|
|
11
|
+
id: string;
|
|
12
|
+
email: string;
|
|
13
|
+
passwordHash: string;
|
|
14
|
+
role: string;
|
|
15
|
+
name: string | null;
|
|
16
|
+
}
|
|
17
|
+
export interface IUserRepository {
|
|
18
|
+
/**
|
|
19
|
+
* Returns the total number of registered users.
|
|
20
|
+
* Used to block re-setup when at least one administrator account already exists.
|
|
21
|
+
*/
|
|
22
|
+
countAll(): Promise<number>;
|
|
23
|
+
/** Retrieves a user by their unique identifier, or null if not found. */
|
|
24
|
+
findById(userId: string): Promise<UserRecord | null>;
|
|
25
|
+
/** Retrieves a user by their email address, or null if not found. */
|
|
26
|
+
findByEmail(email: string): Promise<UserRecord | null>;
|
|
27
|
+
/** Inserts a new user record. */
|
|
28
|
+
create(user: NewUserInput): Promise<void>;
|
|
29
|
+
/** Updates the user's display name and/or email address. */
|
|
30
|
+
updateProfile(userId: string, fields: {
|
|
31
|
+
name?: string;
|
|
32
|
+
email?: string;
|
|
33
|
+
}): Promise<void>;
|
|
34
|
+
/** Replaces the user's stored password hash after a successful password change. */
|
|
35
|
+
updatePasswordHash(userId: string, newPasswordHash: string): Promise<void>;
|
|
36
|
+
/** Sets or clears the user's avatar URL. */
|
|
37
|
+
updateAvatarUrl(userId: string, avatarUrl: string | null): Promise<void>;
|
|
38
|
+
/** Persists the user's notification preferences as a JSON string. */
|
|
39
|
+
updateNotificationPreferences(userId: string, preferencesJson: string): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Checks whether the given email is already registered to a different user.
|
|
42
|
+
* Used during an email change to detect conflicts without exposing whether
|
|
43
|
+
* an unrelated account exists.
|
|
44
|
+
*/
|
|
45
|
+
emailBelongsToAnotherUser(email: string, currentUserId: string): Promise<boolean>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=user.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user.repository.d.ts","sourceRoot":"","sources":["../../src/auth/user.repository.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,uBAAuB,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IAE3B,yEAAyE;IACzE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;IAEpD,qEAAqE;IACrE,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;IAEtD,iCAAiC;IACjC,MAAM,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEzC,4DAA4D;IAC5D,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAEvF,mFAAmF;IACnF,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAE1E,4CAA4C;IAC5C,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAExE,qEAAqE;IACrE,6BAA6B,CAAC,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAErF;;;;OAIG;IACH,yBAAyB,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAClF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time abstraction. Hides direct calls to {@link Date.now} so callers can
|
|
3
|
+
* swap in deterministic clocks during tests without resorting to global
|
|
4
|
+
* timer mocks (e.g. vi.useFakeTimers, sinon, monkey-patching Date).
|
|
5
|
+
*/
|
|
6
|
+
export interface IClock {
|
|
7
|
+
/**
|
|
8
|
+
* Returns the current Unix timestamp in milliseconds.
|
|
9
|
+
* Equivalent to Date.now() in production.
|
|
10
|
+
* Overridable in tests for deterministic time-sensitive assertions.
|
|
11
|
+
*/
|
|
12
|
+
now(): number;
|
|
13
|
+
/**
|
|
14
|
+
* Returns the current Unix timestamp in whole seconds.
|
|
15
|
+
* Equivalent to Math.floor(Date.now() / 1000) in production.
|
|
16
|
+
* Used by JWT issuance, session expiry, and analytics day-bucket computations.
|
|
17
|
+
*/
|
|
18
|
+
nowSeconds(): number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Production singleton. Delegates to the host runtime clock. Stateless and
|
|
22
|
+
* therefore safe to share across requests.
|
|
23
|
+
*/
|
|
24
|
+
export declare const SystemClock: IClock;
|
|
25
|
+
//# sourceMappingURL=clock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,MAAM;IACrB;;;;OAIG;IACH,GAAG,IAAI,MAAM,CAAA;IAEb;;;;OAIG;IACH,UAAU,IAAI,MAAM,CAAA;CACrB;AAID;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,MAGzB,CAAA"}
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
const MILLISECONDS_PER_SECOND = 1000;
|
|
2
|
+
/**
|
|
3
|
+
* Production singleton. Delegates to the host runtime clock. Stateless and
|
|
4
|
+
* therefore safe to share across requests.
|
|
5
|
+
*/
|
|
6
|
+
export const SystemClock = {
|
|
7
|
+
now: () => Date.now(),
|
|
8
|
+
nowSeconds: () => Math.floor(Date.now() / MILLISECONDS_PER_SECOND),
|
|
9
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Seed } from './types.js';
|
|
2
|
+
export interface IContentScanRepository {
|
|
3
|
+
/**
|
|
4
|
+
* Scans across all registered seeds to identify media keys that are currently referenced
|
|
5
|
+
* by any content entry. Used for orphaned media detection and storage analytics.
|
|
6
|
+
*/
|
|
7
|
+
getReferencedMediaKeys(seeds: Seed[]): Promise<Set<string>>;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=content-scan.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-scan.repository.d.ts","sourceRoot":"","sources":["../src/content-scan.repository.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,sBAAsB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;CAC5D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Seed, SelectOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Base Repository Error
|
|
4
|
+
*/
|
|
5
|
+
export declare class RepositoryError extends Error {
|
|
6
|
+
cause?: unknown | undefined;
|
|
7
|
+
constructor(message: string, cause?: unknown | undefined);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Thrown when an entry is not found by ID or Slug
|
|
11
|
+
*/
|
|
12
|
+
export declare class EntryNotFoundError extends RepositoryError {
|
|
13
|
+
constructor(message: string);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Thrown when a slug already exists for a content type
|
|
17
|
+
*/
|
|
18
|
+
export declare class SlugConflictError extends RepositoryError {
|
|
19
|
+
constructor(message: string);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Interface defining the standard operations for content persistence.
|
|
23
|
+
* This is platform-agnostic and should be implemented for specific databases (e.g., D1).
|
|
24
|
+
*/
|
|
25
|
+
export interface ContentRepository {
|
|
26
|
+
/**
|
|
27
|
+
* Retrieves a paginated and filtered list of entries.
|
|
28
|
+
*/
|
|
29
|
+
findMany(seed: Seed, options: SelectOptions): Promise<{
|
|
30
|
+
items: Record<string, any>[];
|
|
31
|
+
total: number;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Finds a single entry by its unique ID.
|
|
35
|
+
* Throws EntryNotFoundError if not found.
|
|
36
|
+
*/
|
|
37
|
+
findById(seed: Seed, id: string): Promise<Record<string, any>>;
|
|
38
|
+
/**
|
|
39
|
+
* Finds a single entry by its unique slug.
|
|
40
|
+
* Throws EntryNotFoundError if not found.
|
|
41
|
+
*/
|
|
42
|
+
findBySlug(seed: Seed, slug: string): Promise<Record<string, any>>;
|
|
43
|
+
/**
|
|
44
|
+
* Computes facets (status counts and distinct tags) for a content type.
|
|
45
|
+
*/
|
|
46
|
+
getFacets(seed: Seed): Promise<{
|
|
47
|
+
statuses: Record<string, number>;
|
|
48
|
+
tagsByColumn: Record<string, string[]>;
|
|
49
|
+
}>;
|
|
50
|
+
/**
|
|
51
|
+
* Creates a new content entry in the live table.
|
|
52
|
+
* Throws SlugConflictError if the slug is already taken.
|
|
53
|
+
*/
|
|
54
|
+
create(seed: Seed, id: string, slug: string, status: string, data: Record<string, any>): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Partially updates an existing entry in the live table.
|
|
57
|
+
* Throws EntryNotFoundError if the ID does not exist.
|
|
58
|
+
*/
|
|
59
|
+
update(seed: Seed, id: string, data: Record<string, any>, status?: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Deletes an entry from the live table.
|
|
62
|
+
* Returns the deleted row data (useful for media cleanup).
|
|
63
|
+
* Throws EntryNotFoundError if the ID does not exist.
|
|
64
|
+
*/
|
|
65
|
+
delete(seed: Seed, id: string): Promise<{
|
|
66
|
+
row: Record<string, any>;
|
|
67
|
+
}>;
|
|
68
|
+
/**
|
|
69
|
+
* Saves or updates a pending draft in the mirror table.
|
|
70
|
+
*/
|
|
71
|
+
saveDraft(seed: Seed, entryId: string, data: Record<string, any>): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Retrieves the pending draft for a given entry.
|
|
74
|
+
* Returns null if no draft exists.
|
|
75
|
+
*/
|
|
76
|
+
getDraft(seed: Seed, entryId: string): Promise<Record<string, any> | null>;
|
|
77
|
+
/**
|
|
78
|
+
* Atomic promotion of a draft to the live table.
|
|
79
|
+
* Must use a transaction (db.batch) to update live and delete draft.
|
|
80
|
+
*/
|
|
81
|
+
publishDraft(seed: Seed, entryId: string): Promise<void>;
|
|
82
|
+
/**
|
|
83
|
+
* Discards the pending draft.
|
|
84
|
+
*/
|
|
85
|
+
deleteDraft(seed: Seed, entryId: string): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Checks if a slug is already taken by another entry.
|
|
88
|
+
*/
|
|
89
|
+
existsSlug(seed: Seed, slug: string, excludeId?: string): Promise<boolean>;
|
|
90
|
+
/**
|
|
91
|
+
* Checks if an entry has a pending draft.
|
|
92
|
+
*/
|
|
93
|
+
hasDraft(seed: Seed, entryId: string): Promise<boolean>;
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=content.repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.repository.d.ts","sourceRoot":"","sources":["../src/content.repository.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base Repository Error
|
|
3
|
+
*/
|
|
4
|
+
export class RepositoryError extends Error {
|
|
5
|
+
cause;
|
|
6
|
+
constructor(message, cause) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.cause = cause;
|
|
9
|
+
this.name = 'RepositoryError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Thrown when an entry is not found by ID or Slug
|
|
14
|
+
*/
|
|
15
|
+
export class EntryNotFoundError extends RepositoryError {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'EntryNotFoundError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when a slug already exists for a content type
|
|
23
|
+
*/
|
|
24
|
+
export class SlugConflictError extends RepositoryError {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = 'SlugConflictError';
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/engine.d.ts
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Botanical Engine: Schema Compiler + Query Builder.
|
|
3
3
|
*
|
|
4
|
-
* In v0.4.0
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* In v0.4.0 each Seed has a dedicated SQL table (`content_{slug}`) with
|
|
5
|
+
* real typed columns. This module generates the DDL and builds parameterized queries.
|
|
6
|
+
* It does not know about HTTP, auth, or UI — it's a pure TypeScript library.
|
|
7
7
|
*/
|
|
8
8
|
import type { Seed, Branch, SelectOptions, ParameterizedQuery } from './types.js';
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
* +
|
|
10
|
+
* Generates `CREATE TABLE IF NOT EXISTS content_{slug}` with system columns
|
|
11
|
+
* + one column per Branch. Pure function: same Seed → same SQL.
|
|
12
12
|
*/
|
|
13
13
|
export declare function generateCreateTable(seed: Seed): string;
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Generates the drafts table `content_{slug}_drafts` for Seeds with `allowDrafts: true`.
|
|
16
|
+
* All branch columns are nullable (drafts are partial).
|
|
17
|
+
* Returns null if the Seed does not have `allowDrafts: true`.
|
|
18
18
|
*/
|
|
19
19
|
export declare function generateDraftTable(seed: Seed): string | null;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* Generates `ALTER TABLE content_{slug} ADD COLUMN {alias} {type}`.
|
|
22
|
+
* New columns are always nullable (SQLite limit on ALTER TABLE).
|
|
23
23
|
*/
|
|
24
24
|
export declare function generateAddColumn(seed: Seed, branch: Branch): string;
|
|
25
25
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Generates B-tree indexes for status, created_at and every filterable Branch
|
|
27
|
+
* with an indexable type (text, number, date, boolean).
|
|
28
28
|
*/
|
|
29
29
|
export declare function generateIndexes(seed: Seed): string[];
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
31
|
+
* Generates the FTS5 virtual table for indexable text/richtext Branches.
|
|
32
|
+
* Returns null if the Seed has no branches with search enabled.
|
|
33
33
|
*/
|
|
34
34
|
export declare function generateFtsTable(seed: Seed): string | null;
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
36
|
+
* Generates the 3 SQLite triggers (insert/update/delete) that keep the FTS
|
|
37
|
+
* automatically synchronized — eliminates the need for manual syncFts.
|
|
38
|
+
* Returns an empty array if the Seed has no indexable branches.
|
|
39
39
|
*/
|
|
40
40
|
export declare function generateFtsTriggers(seed: Seed): string[];
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Builds a parameterized SELECT on `content_{slug}`.
|
|
43
|
+
* Never uses json_extract — every column is a real column.
|
|
44
|
+
* Unknown columns in filters/orderBy are ignored (fail-closed).
|
|
45
45
|
*/
|
|
46
46
|
export declare function buildSelectQuery(seed: Seed, options?: SelectOptions): ParameterizedQuery;
|
|
47
47
|
export interface SchemaColumn {
|
|
@@ -51,17 +51,17 @@ export interface SchemaColumn {
|
|
|
51
51
|
isPk: boolean;
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* Returns the list of expected columns for a Seed's table.
|
|
55
|
+
* Used by `beech seed:load --diff` to compare current vs expected schema.
|
|
56
56
|
*/
|
|
57
57
|
export declare function getExpectedColumns(seed: Seed): SchemaColumn[];
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
59
|
+
* Serializes a value for writing to the DB.
|
|
60
60
|
* boolean → 0/1 | date → Unix timestamp | json/asset-list → JSON string
|
|
61
61
|
*/
|
|
62
62
|
export declare function serializeForDb(branch: Branch, value: unknown): string | number | null;
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
64
|
+
* Deserializes a value read from the DB for the API response.
|
|
65
65
|
* 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object
|
|
66
66
|
*/
|
|
67
67
|
export declare function deserializeFromDb(branch: Branch, value: unknown): unknown;
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EACV,IAAI,EACJ,MAAM,EAKN,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EACV,IAAI,EACJ,MAAM,EAKN,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAA;AAgFnB;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAsBtD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAuB5D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAkBpD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAc1D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAgCxD;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,aAAkB,GAAG,kBAAkB,CAqE5F;AA+GD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACpC,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,EAAE,CAc7D;AAID;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAmCrF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CA6BzE"}
|