@jini-ai/cms 0.5.2 → 0.5.3
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/CHANGELOG.md +11 -0
- package/README.md +14 -8
- package/dist/comments/errors.d.ts +23 -0
- package/dist/comments/errors.js +25 -0
- package/dist/comments/express/index.d.ts +2 -0
- package/dist/comments/express/index.js +2 -0
- package/dist/comments/express/submit-handler.d.ts +30 -0
- package/dist/comments/express/submit-handler.js +85 -0
- package/dist/comments/hooks.d.ts +39 -0
- package/dist/comments/hooks.js +41 -0
- package/dist/comments/index.d.ts +11 -0
- package/dist/comments/index.js +11 -0
- package/dist/comments/ingress.d.ts +57 -0
- package/dist/comments/ingress.js +173 -0
- package/dist/comments/module.d.ts +94 -0
- package/dist/comments/module.js +73 -0
- package/dist/comments/ports.d.ts +184 -0
- package/dist/comments/ports.js +2 -0
- package/dist/comments/repo.memory.d.ts +70 -0
- package/dist/comments/repo.memory.js +120 -0
- package/dist/comments/sanitize.d.ts +15 -0
- package/dist/comments/sanitize.js +31 -0
- package/dist/comments/spam.heuristic.d.ts +8 -0
- package/dist/comments/spam.heuristic.js +54 -0
- package/dist/comments/sql/index.d.ts +3 -0
- package/dist/comments/sql/index.js +3 -0
- package/dist/comments/sql/repo.d.ts +77 -0
- package/dist/comments/sql/repo.js +190 -0
- package/dist/comments/sql/repo.rows.d.ts +49 -0
- package/dist/comments/sql/repo.rows.js +71 -0
- package/dist/comments/types.d.ts +187 -0
- package/dist/comments/types.js +39 -0
- package/dist/comments/write-service.d.ts +121 -0
- package/dist/comments/write-service.js +144 -0
- package/dist/core/commands/command.d.ts +2 -9
- package/dist/core/commands/command.js +2 -9
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +1 -0
- package/dist/core/keyset-cursor.d.ts +25 -0
- package/dist/core/keyset-cursor.js +20 -0
- package/dist/core/ports.d.ts +30 -0
- package/dist/core/ports.js +0 -1
- package/dist/core/tools/args.d.ts +1 -46
- package/dist/core/tools/args.js +1 -44
- package/dist/media/index.d.ts +0 -3
- package/dist/media/index.js +1 -4
- package/dist/media/node.d.ts +5 -0
- package/dist/media/node.js +6 -0
- package/dist/redirects/capture.d.ts +80 -0
- package/dist/redirects/capture.js +66 -0
- package/dist/redirects/express/index.d.ts +27 -0
- package/dist/redirects/express/index.js +40 -0
- package/dist/redirects/hit-sink.d.ts +56 -0
- package/dist/redirects/hit-sink.js +44 -0
- package/dist/redirects/host-config/index.d.ts +40 -0
- package/dist/redirects/host-config/index.js +145 -0
- package/dist/redirects/index.d.ts +14 -0
- package/dist/redirects/index.js +14 -0
- package/dist/redirects/matcher.d.ts +58 -0
- package/dist/redirects/matcher.js +144 -0
- package/dist/redirects/order.d.ts +30 -0
- package/dist/redirects/order.js +26 -0
- package/dist/redirects/ports.d.ts +242 -0
- package/dist/redirects/ports.internal.d.ts +57 -0
- package/dist/redirects/ports.internal.js +14 -0
- package/dist/redirects/ports.js +2 -0
- package/dist/redirects/redirects.d.ts +226 -0
- package/dist/redirects/redirects.js +590 -0
- package/dist/redirects/referrer-alias.d.ts +31 -0
- package/dist/redirects/referrer-alias.js +33 -0
- package/dist/redirects/repo.memory.d.ts +51 -0
- package/dist/redirects/repo.memory.js +98 -0
- package/dist/redirects/reserved-destination.d.ts +58 -0
- package/dist/redirects/reserved-destination.js +59 -0
- package/dist/redirects/resolver.d.ts +100 -0
- package/dist/redirects/resolver.js +87 -0
- package/dist/redirects/spa/index.d.ts +30 -0
- package/dist/redirects/spa/index.js +27 -0
- package/dist/redirects/sql/index.d.ts +4 -0
- package/dist/redirects/sql/index.js +4 -0
- package/dist/redirects/sql/repo.d.ts +82 -0
- package/dist/redirects/sql/repo.js +143 -0
- package/dist/redirects/sql/repo.rows.d.ts +49 -0
- package/dist/redirects/sql/repo.rows.js +79 -0
- package/dist/redirects/sql/row-types.d.ts +34 -0
- package/dist/redirects/sql/row-types.js +2 -0
- package/dist/redirects/target-gate.d.ts +30 -0
- package/dist/redirects/target-gate.js +85 -0
- package/dist/redirects/types.d.ts +213 -0
- package/dist/redirects/types.js +42 -0
- package/dist/seo/absolute-url.d.ts +58 -0
- package/dist/seo/absolute-url.js +117 -0
- package/dist/seo/dom/index.d.ts +14 -0
- package/dist/seo/dom/index.js +52 -0
- package/dist/seo/errors.d.ts +35 -0
- package/dist/seo/errors.js +35 -0
- package/dist/seo/feed.d.ts +49 -0
- package/dist/seo/feed.js +61 -0
- package/dist/seo/index.d.ts +13 -0
- package/dist/seo/index.js +13 -0
- package/dist/seo/media.d.ts +57 -0
- package/dist/seo/media.js +134 -0
- package/dist/seo/page-head.d.ts +18 -0
- package/dist/seo/page-head.js +114 -0
- package/dist/seo/ports.d.ts +221 -0
- package/dist/seo/ports.js +27 -0
- package/dist/seo/seo.d.ts +73 -0
- package/dist/seo/seo.js +230 -0
- package/dist/seo/sitemap.d.ts +139 -0
- package/dist/seo/sitemap.js +217 -0
- package/dist/seo/types.d.ts +278 -0
- package/dist/seo/types.js +2 -0
- package/dist/seo/write-service.d.ts +79 -0
- package/dist/seo/write-service.js +273 -0
- package/dist/seo/xml-escape.d.ts +16 -0
- package/dist/seo/xml-escape.js +19 -0
- package/dist/settings/express/cms-adapter.d.ts +20 -0
- package/dist/settings/express/cms-adapter.js +124 -0
- package/dist/settings/express/contracts.d.ts +135 -0
- package/dist/settings/express/contracts.js +2 -0
- package/dist/settings/express/http.d.ts +10 -0
- package/dist/settings/express/http.js +211 -0
- package/dist/settings/express/index.d.ts +5 -0
- package/dist/settings/express/index.js +5 -0
- package/dist/settings/express/shared.d.ts +68 -0
- package/dist/settings/express/shared.js +54 -0
- package/dist/settings/express/stream.d.ts +24 -0
- package/dist/settings/express/stream.js +147 -0
- package/dist/settings/index.d.ts +3 -38
- package/dist/settings/index.js +3 -35
- package/dist/taxonomy/write-service.d.ts +2 -30
- package/dist/widgets/authorize-helper.d.ts +42 -0
- package/dist/widgets/authorize-helper.js +16 -0
- package/dist/widgets/concurrency.d.ts +12 -0
- package/dist/widgets/concurrency.js +44 -0
- package/dist/widgets/config-validation.d.ts +34 -0
- package/dist/widgets/config-validation.js +125 -0
- package/dist/widgets/embed-service.d.ts +133 -0
- package/dist/widgets/embed-service.js +460 -0
- package/dist/widgets/embed-validation.d.ts +23 -0
- package/dist/widgets/embed-validation.js +38 -0
- package/dist/widgets/entry-payload.d.ts +103 -0
- package/dist/widgets/entry-payload.js +160 -0
- package/dist/widgets/errors.d.ts +136 -0
- package/dist/widgets/errors.js +136 -0
- package/dist/widgets/host-ports.d.ts +149 -0
- package/dist/widgets/host-ports.js +2 -0
- package/dist/widgets/html/html-embeds.d.ts +247 -0
- package/dist/widgets/html/html-embeds.js +408 -0
- package/dist/widgets/html/index.d.ts +5 -0
- package/dist/widgets/html/index.js +4 -0
- package/dist/widgets/html/legacy-widget-markers.d.ts +8 -0
- package/dist/widgets/html/legacy-widget-markers.js +45 -0
- package/dist/widgets/html/resolve-html-page-embeds.d.ts +42 -0
- package/dist/widgets/html/resolve-html-page-embeds.js +57 -0
- package/dist/widgets/index.d.ts +20 -0
- package/dist/widgets/index.js +19 -0
- package/dist/widgets/markers/html-attributes.d.ts +98 -0
- package/dist/widgets/markers/html-attributes.js +216 -0
- package/dist/widgets/markers/index.d.ts +3 -0
- package/dist/widgets/markers/index.js +3 -0
- package/dist/widgets/markers/marker.d.ts +496 -0
- package/dist/widgets/markers/marker.js +780 -0
- package/dist/widgets/page-embed-types.d.ts +42 -0
- package/dist/widgets/page-embed-types.js +42 -0
- package/dist/widgets/ports.d.ts +142 -0
- package/dist/widgets/ports.js +2 -0
- package/dist/widgets/read-service.d.ts +81 -0
- package/dist/widgets/read-service.js +79 -0
- package/dist/widgets/region-area-service.d.ts +110 -0
- package/dist/widgets/region-area-service.js +180 -0
- package/dist/widgets/registry.d.ts +217 -0
- package/dist/widgets/registry.js +172 -0
- package/dist/widgets/repo.memory.d.ts +41 -0
- package/dist/widgets/repo.memory.js +48 -0
- package/dist/widgets/resolver-service.d.ts +209 -0
- package/dist/widgets/resolver-service.js +909 -0
- package/dist/widgets/resolvers/contact-form.d.ts +26 -0
- package/dist/widgets/resolvers/contact-form.js +54 -0
- package/dist/widgets/resolvers/create-core-resolvers.d.ts +34 -0
- package/dist/widgets/resolvers/create-core-resolvers.js +11 -0
- package/dist/widgets/resolvers/index.d.ts +65 -0
- package/dist/widgets/resolvers/index.js +155 -0
- package/dist/widgets/resolvers/menu.d.ts +42 -0
- package/dist/widgets/resolvers/menu.js +43 -0
- package/dist/widgets/resolvers/ports.d.ts +82 -0
- package/dist/widgets/resolvers/ports.js +2 -0
- package/dist/widgets/resolvers/recent-entries.d.ts +71 -0
- package/dist/widgets/resolvers/recent-entries.js +165 -0
- package/dist/widgets/resolvers/taxonomy.d.ts +12 -0
- package/dist/widgets/resolvers/taxonomy.js +34 -0
- package/dist/widgets/sql/index.d.ts +3 -0
- package/dist/widgets/sql/index.js +3 -0
- package/dist/widgets/sql/repo.d.ts +41 -0
- package/dist/widgets/sql/repo.js +82 -0
- package/dist/widgets/sql/tables.d.ts +13 -0
- package/dist/widgets/sql/tables.js +2 -0
- package/dist/widgets/types.d.ts +213 -0
- package/dist/widgets/types.js +17 -0
- package/dist/widgets/where-used.d.ts +41 -0
- package/dist/widgets/where-used.js +11 -0
- package/dist/widgets/write-service.d.ts +204 -0
- package/dist/widgets/write-service.js +332 -0
- package/dist/workspace/delete.d.ts +1 -1
- package/package.json +220 -11
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file ADR-031 §3/§8 — the moderation write-service: wraps `CommentRepoPort.applyModeration`/
|
|
3
|
+
* `purge` with outbox event emission and the `comments.statusChanged` action hook. Routes call
|
|
4
|
+
* `authorize()` first (ADR-021 flat `comments.*` strings), then this service, never the repo
|
|
5
|
+
* directly — the same "typed, core-owned write, no side-door" discipline ADR-023 §7 requires of
|
|
6
|
+
* the repo itself, one layer up.
|
|
7
|
+
*/
|
|
8
|
+
import type { Clock as ClockPort, IdGenerator as IdGeneratorPort, UUID } from "@jini-ai/core/primitives";
|
|
9
|
+
import type { OutboxPort } from "../core/index.js";
|
|
10
|
+
import type { CommentHookRegistry } from "./hooks.js";
|
|
11
|
+
import type { CommentRepoPort } from "./ports.js";
|
|
12
|
+
import type { CommentStatus, ModerationAction } from "./types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Hands one comment's removal to whoever owns removal in this composition.
|
|
15
|
+
*
|
|
16
|
+
* Structurally typed ON PURPOSE — this file imports nothing from `features/trash`, exactly as
|
|
17
|
+
* `post.ts`'s `RemovePostFn` does not. The composition root binds the real implementation (which
|
|
18
|
+
* records the item for the Trash screen) and hands it in already bound to this domain's entity
|
|
19
|
+
* type, so the moderation service never learns that a Trash exists.
|
|
20
|
+
*
|
|
21
|
+
* `display` is REQUIRED because the caller already holds the record: `applyModeration` returns the
|
|
22
|
+
* updated row, so the two strings the Trash screen shows come from columns, with no second read and
|
|
23
|
+
* no payload parse.
|
|
24
|
+
*/
|
|
25
|
+
export type RemoveCommentFn = (required: {
|
|
26
|
+
workspaceId: string;
|
|
27
|
+
id: string;
|
|
28
|
+
display: {
|
|
29
|
+
title: string;
|
|
30
|
+
subtitle?: string | null;
|
|
31
|
+
};
|
|
32
|
+
at: string;
|
|
33
|
+
expectedVersion: number | null;
|
|
34
|
+
actor: {
|
|
35
|
+
principalId: string;
|
|
36
|
+
pluginId?: string | null;
|
|
37
|
+
};
|
|
38
|
+
}) => Promise<{
|
|
39
|
+
ok: true;
|
|
40
|
+
version: number | null;
|
|
41
|
+
} | {
|
|
42
|
+
ok: false;
|
|
43
|
+
reason: "not-found" | "version-changed";
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Drops any removal record for a comment that has come back out of `trash` by a route other than a
|
|
47
|
+
* Trash-screen restore — i.e. an ordinary moderation decision.
|
|
48
|
+
*
|
|
49
|
+
* This is the half of the pair it is easy to forget, and forgetting it is the expensive mistake:
|
|
50
|
+
* without it, approving a trashed comment leaves a row in the index pointing at a comment that is
|
|
51
|
+
* live on the page again, so the Trash screen offers to permanently delete published content.
|
|
52
|
+
*/
|
|
53
|
+
export type ForgetRemovedCommentFn = (required: {
|
|
54
|
+
workspaceId: string;
|
|
55
|
+
id: string;
|
|
56
|
+
}) => Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Runs `fn` inside one database transaction. MUST be reentrant — see the composition root's runner.
|
|
59
|
+
*
|
|
60
|
+
* Structurally typed for the same reason as {@link RemoveCommentFn}: no trash import here.
|
|
61
|
+
*/
|
|
62
|
+
export type CommentTransactionRunner = <T>(fn: () => Promise<T>) => Promise<T>;
|
|
63
|
+
export interface CommentWriteServiceDeps {
|
|
64
|
+
repo: CommentRepoPort;
|
|
65
|
+
outbox: OutboxPort;
|
|
66
|
+
hooks: CommentHookRegistry;
|
|
67
|
+
clock: ClockPort;
|
|
68
|
+
idGen: IdGeneratorPort;
|
|
69
|
+
/** See {@link RemoveCommentFn}. Called only on the transition INTO `trash`. */
|
|
70
|
+
remove: RemoveCommentFn;
|
|
71
|
+
/** See {@link ForgetRemovedCommentFn}. Called only on the transition OUT of `trash`. */
|
|
72
|
+
forgetRemoved: ForgetRemovedCommentFn;
|
|
73
|
+
/**
|
|
74
|
+
* Wraps the moderation write and the index write so they are both-or-neither. `applyModeration`
|
|
75
|
+
* opens its own `better-sqlite3` transaction, which nests as a SAVEPOINT when one is already
|
|
76
|
+
* open, so this runner may be the outer one.
|
|
77
|
+
*/
|
|
78
|
+
runInTransaction: CommentTransactionRunner;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Raised when the moderation write landed but its Trash index write could not.
|
|
82
|
+
*
|
|
83
|
+
* Only reachable if the comment row changed between two statements of the same transaction, which
|
|
84
|
+
* it cannot — so this is an invariant breach, not a runtime condition. It is thrown rather than
|
|
85
|
+
* returned so the enclosing transaction rolls the moderation back: a comment hidden with no Trash
|
|
86
|
+
* row is unrecoverable from the UI, which is strictly worse than a failed moderation the operator
|
|
87
|
+
* can retry.
|
|
88
|
+
*/
|
|
89
|
+
export declare class CommentRemovalIndexError extends Error {
|
|
90
|
+
}
|
|
91
|
+
export type ModerationResult = {
|
|
92
|
+
ok: true;
|
|
93
|
+
} | {
|
|
94
|
+
ok: false;
|
|
95
|
+
reason: "not-found" | "conflict";
|
|
96
|
+
currentVersion?: number | undefined;
|
|
97
|
+
};
|
|
98
|
+
export interface CommentWriteService {
|
|
99
|
+
applyModeration(required: {
|
|
100
|
+
workspaceId: UUID;
|
|
101
|
+
id: UUID;
|
|
102
|
+
expectedVersion: number;
|
|
103
|
+
action: ModerationAction;
|
|
104
|
+
toStatus: CommentStatus;
|
|
105
|
+
actorPrincipalId: UUID;
|
|
106
|
+
/** The assistant's AI marker on the Trash row's `actor.pluginId` (2026-09-21, trash T4c) —
|
|
107
|
+
* unset by every non-assistant caller (the admin moderation route), so a human's own moderation
|
|
108
|
+
* stays human-only. Held here only for the transition into `trash` ({@link syncRemovalIndex});
|
|
109
|
+
* never persisted onto this domain's OWN moderation log, which has no such column. */
|
|
110
|
+
actorPluginId?: UUID | null;
|
|
111
|
+
note: string | null;
|
|
112
|
+
}, optional?: Record<string, never>): Promise<ModerationResult>;
|
|
113
|
+
purge(required: {
|
|
114
|
+
workspaceId: UUID;
|
|
115
|
+
id: UUID;
|
|
116
|
+
actorPrincipalId: UUID;
|
|
117
|
+
note: string | null;
|
|
118
|
+
}, optional?: Record<string, never>): Promise<ModerationResult>;
|
|
119
|
+
}
|
|
120
|
+
export declare function createCommentWriteService(deps: CommentWriteServiceDeps, _optional?: Record<string, never>): CommentWriteService;
|
|
121
|
+
//# sourceMappingURL=write-service.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { nowIso as clockNowIso } from "@jini-ai/core/primitives";
|
|
2
|
+
/** The one `CommentStatus` value that means "deleted" — the marker the Trash index mirrors. */
|
|
3
|
+
const TRASHED_STATUS = "trash";
|
|
4
|
+
/** Longest body excerpt stored as the Trash row's subtitle; matches the backfill's `substr(...,120)`. */
|
|
5
|
+
const TRASH_SUBTITLE_MAX = 120;
|
|
6
|
+
/**
|
|
7
|
+
* Raised when the moderation write landed but its Trash index write could not.
|
|
8
|
+
*
|
|
9
|
+
* Only reachable if the comment row changed between two statements of the same transaction, which
|
|
10
|
+
* it cannot — so this is an invariant breach, not a runtime condition. It is thrown rather than
|
|
11
|
+
* returned so the enclosing transaction rolls the moderation back: a comment hidden with no Trash
|
|
12
|
+
* row is unrecoverable from the UI, which is strictly worse than a failed moderation the operator
|
|
13
|
+
* can retry.
|
|
14
|
+
*/
|
|
15
|
+
export class CommentRemovalIndexError extends Error {
|
|
16
|
+
}
|
|
17
|
+
const EVENT_NAME_BY_ACTION = {
|
|
18
|
+
approve: "comments.approved",
|
|
19
|
+
mark_spam: "comments.marked_spam",
|
|
20
|
+
trash: "comments.trashed",
|
|
21
|
+
};
|
|
22
|
+
// Inline object literal at the enqueue() call site (not a `CommentDomainEvent`-typed
|
|
23
|
+
// intermediate) — see ingress.ts's identical note on why: TS only infers a
|
|
24
|
+
// `Record<string, unknown>`-compatible index signature for a FRESH literal argument.
|
|
25
|
+
async function emitEvent(deps, required) {
|
|
26
|
+
await deps.outbox.enqueue({
|
|
27
|
+
id: deps.idGen.newId(),
|
|
28
|
+
name: required.name,
|
|
29
|
+
occurredAt: clockNowIso({ clock: deps.clock }),
|
|
30
|
+
aggregateId: required.commentId,
|
|
31
|
+
workspaceId: required.workspaceId,
|
|
32
|
+
payload: { commentId: required.commentId, entryId: required.entryId, status: required.status },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Keeps the Trash index in step with a moderation decision that just landed.
|
|
37
|
+
*
|
|
38
|
+
* Keys off the TRANSITION, not off the target status, and takes the transition from
|
|
39
|
+
* `log.fromStatus` — which `applyModeration` reads inside its own compare-and-set, so it is the one
|
|
40
|
+
* signal here that cannot race a concurrent moderation. That is what makes both failure modes
|
|
41
|
+
* unreachable rather than merely unlikely:
|
|
42
|
+
*
|
|
43
|
+
* - **double index write** — `fromStatus !== trash` fires at most once per entry into the Trash,
|
|
44
|
+
* so a repeated `trash` action cannot add a second row (the index's own uniqueness is a second
|
|
45
|
+
* net, not the first one);
|
|
46
|
+
* - **stranded index row** — `fromStatus === trash` with any other target fires on every exit,
|
|
47
|
+
* including the ones that never touch the Trash screen, so an approved comment cannot stay
|
|
48
|
+
* listed as deleted.
|
|
49
|
+
*
|
|
50
|
+
* Runs inside the caller's transaction, so a throw here rolls the moderation back with it.
|
|
51
|
+
*
|
|
52
|
+
* @complexity O(1) — at most one delegated write.
|
|
53
|
+
*/
|
|
54
|
+
async function syncRemovalIndex(deps, required) {
|
|
55
|
+
const from = required.log.fromStatus;
|
|
56
|
+
const to = required.record.status;
|
|
57
|
+
if (to === TRASHED_STATUS && from !== TRASHED_STATUS) {
|
|
58
|
+
const removed = await deps.remove({
|
|
59
|
+
workspaceId: required.record.workspaceId,
|
|
60
|
+
id: required.record.id,
|
|
61
|
+
// From the row `applyModeration` just returned — columns only, no second read.
|
|
62
|
+
display: {
|
|
63
|
+
title: `Comment on ${required.record.entryId}`,
|
|
64
|
+
subtitle: required.record.bodyText.slice(0, TRASH_SUBTITLE_MAX),
|
|
65
|
+
},
|
|
66
|
+
at: required.at,
|
|
67
|
+
// The version AFTER the flip: what the sweeper's compare-and-delete checks, and what a
|
|
68
|
+
// restore bumps past so the item always survives a race.
|
|
69
|
+
expectedVersion: required.record.version,
|
|
70
|
+
actor: { principalId: required.log.actorPrincipalId, pluginId: required.actorPluginId },
|
|
71
|
+
});
|
|
72
|
+
if (!removed.ok) {
|
|
73
|
+
throw new CommentRemovalIndexError(`comments: comment '${required.record.id}' was moved to trash but could not be indexed (${removed.reason})`);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (from === TRASHED_STATUS && to !== TRASHED_STATUS) {
|
|
78
|
+
await deps.forgetRemoved({ workspaceId: required.record.workspaceId, id: required.record.id });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export function createCommentWriteService(deps, _optional = {}) {
|
|
82
|
+
return {
|
|
83
|
+
async applyModeration(required) {
|
|
84
|
+
// Pulled out before the spread below: `deps.repo.applyModeration` persists this domain's OWN
|
|
85
|
+
// moderation log, which has no `actorPluginId` column — spreading it in would be an excess
|
|
86
|
+
// property on a fresh object literal (a real `tsc` error, not just a lint nit). The Trash's
|
|
87
|
+
// `actor.pluginId` is a SEPARATE concern, carried only as far as `syncRemovalIndex` below.
|
|
88
|
+
const { actorPluginId, ...repoRequired } = required;
|
|
89
|
+
const at = clockNowIso({ clock: deps.clock });
|
|
90
|
+
const result = await deps.runInTransaction(async () => {
|
|
91
|
+
const applied = await deps.repo.applyModeration({ ...repoRequired, at });
|
|
92
|
+
if (!applied.ok)
|
|
93
|
+
return applied;
|
|
94
|
+
await syncRemovalIndex(deps, { at, log: applied.log, record: applied.record, actorPluginId: actorPluginId ?? null });
|
|
95
|
+
return applied;
|
|
96
|
+
});
|
|
97
|
+
if (!result.ok)
|
|
98
|
+
return result;
|
|
99
|
+
await deps.hooks.runStatusChangedHooks({
|
|
100
|
+
workspaceId: required.workspaceId,
|
|
101
|
+
commentId: required.id,
|
|
102
|
+
fromStatus: result.log.fromStatus,
|
|
103
|
+
toStatus: result.record.status,
|
|
104
|
+
});
|
|
105
|
+
const eventName = EVENT_NAME_BY_ACTION[required.action];
|
|
106
|
+
if (eventName) {
|
|
107
|
+
await emitEvent(deps, {
|
|
108
|
+
workspaceId: required.workspaceId,
|
|
109
|
+
name: eventName,
|
|
110
|
+
commentId: required.id,
|
|
111
|
+
entryId: result.record.entryId,
|
|
112
|
+
status: result.record.status,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return { ok: true };
|
|
116
|
+
},
|
|
117
|
+
async purge(required) {
|
|
118
|
+
const at = clockNowIso({ clock: deps.clock });
|
|
119
|
+
const before = await deps.repo.findById({ workspaceId: required.workspaceId, id: required.id });
|
|
120
|
+
const result = await deps.runInTransaction(async () => {
|
|
121
|
+
const purged = await deps.repo.purge({ ...required, at });
|
|
122
|
+
// The row is gone for good, so its index row would otherwise outlive it and offer the Trash
|
|
123
|
+
// screen a "Restore" that can never succeed. Same both-or-neither transaction as the
|
|
124
|
+
// moderation path above.
|
|
125
|
+
if (purged.ok)
|
|
126
|
+
await deps.forgetRemoved({ workspaceId: required.workspaceId, id: required.id });
|
|
127
|
+
return purged;
|
|
128
|
+
});
|
|
129
|
+
if (!result.ok)
|
|
130
|
+
return result;
|
|
131
|
+
if (before) {
|
|
132
|
+
await emitEvent(deps, {
|
|
133
|
+
workspaceId: required.workspaceId,
|
|
134
|
+
name: "comments.purged",
|
|
135
|
+
commentId: required.id,
|
|
136
|
+
entryId: before.entryId,
|
|
137
|
+
status: before.status,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return { ok: true };
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=write-service.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolPermissionDeniedError as ForbiddenError } from "@jini-ai/core";
|
|
1
2
|
import type { Clock, IdGenerator, JsonObject, UUID } from "@jini-ai/core/primitives";
|
|
2
3
|
import type { OutboxPort } from "../ports.js";
|
|
3
4
|
import type { ChangeSetOperation, ChangeSetRepoPort } from "./change-set.js";
|
|
@@ -146,15 +147,7 @@ export declare class DuplicateCommandError extends Error {
|
|
|
146
147
|
* `DuplicateCommandError` either.
|
|
147
148
|
* See docs/decisions/DR-006-mutation-audit-atomicity.md.
|
|
148
149
|
*/
|
|
149
|
-
export
|
|
150
|
-
readonly permission: string;
|
|
151
|
-
readonly reason: string;
|
|
152
|
-
constructor({ message, permission, reason }: {
|
|
153
|
-
message: string;
|
|
154
|
-
permission: string;
|
|
155
|
-
reason: string;
|
|
156
|
-
}, _optional?: Record<string, never>);
|
|
157
|
-
}
|
|
150
|
+
export { ForbiddenError };
|
|
158
151
|
/**
|
|
159
152
|
* Execute a mutation through the command gateway.
|
|
160
153
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ToolPermissionDeniedError as ForbiddenError } from "@jini-ai/core";
|
|
1
2
|
import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
2
3
|
/**
|
|
3
4
|
* Thrown when a command's idempotency key was already used in the workspace.
|
|
@@ -18,15 +19,7 @@ export class DuplicateCommandError extends Error {
|
|
|
18
19
|
* `DuplicateCommandError` either.
|
|
19
20
|
* See docs/decisions/DR-006-mutation-audit-atomicity.md.
|
|
20
21
|
*/
|
|
21
|
-
export
|
|
22
|
-
permission;
|
|
23
|
-
reason;
|
|
24
|
-
constructor({ message, permission, reason }, _optional = {}) {
|
|
25
|
-
super(message);
|
|
26
|
-
this.permission = permission;
|
|
27
|
-
this.reason = reason;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
22
|
+
export { ForbiddenError };
|
|
30
23
|
/**
|
|
31
24
|
* Detects a UNIQUE-constraint violation on `changeSets.insert()`'s idempotency-key index —
|
|
32
25
|
* the losing side of the TOCTOU race between the pre-execute `findByIdempotencyKey` check and
|
package/dist/core/index.d.ts
CHANGED
|
@@ -44,10 +44,11 @@
|
|
|
44
44
|
* measured reason no domain could be extracted on its own. A per-entity applier registry belongs
|
|
45
45
|
* behind a registration call the host makes, not behind a kernel barrel.
|
|
46
46
|
*/
|
|
47
|
-
export type { DomainEvent, EventBusPort, OutboxRecord, OutboxPort, } from './ports.js';
|
|
47
|
+
export type { DomainEvent, EventBusPort, OutboxRecord, OutboxPort, TransactionalRepoPort, } from './ports.js';
|
|
48
48
|
export type { ChangeSetStatus, ChangeSetOperation, ChangeSetRecord, ChangeSetItemRecord, ChangeSetWithItems, ChangeSetRepoPort, } from './commands/change-set.js';
|
|
49
49
|
export * from './commands/command.js';
|
|
50
50
|
export * from './entity-liveness.js';
|
|
51
51
|
export * from './tools/args.js';
|
|
52
52
|
export type { AuthorizationPort, AuthorizationRequired, AuthorizationOptional } from './authorization.js';
|
|
53
|
+
export { encodeKeysetCursor, decodeKeysetCursor, type KeysetPosition } from "./keyset-cursor.js";
|
|
53
54
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/core/index.js
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file The opaque `${createdAt}::${id}` keyset cursor shared by the `(created_at, id)`-ordered
|
|
3
|
+
* list queries (the database ledger, the comment moderation queue).
|
|
4
|
+
*
|
|
5
|
+
* The cursor carries the position itself rather than naming a row to look up, so a page resumes
|
|
6
|
+
* correctly even after the row it ended on was deleted: the comment queue's cursor used to be the
|
|
7
|
+
* last comment's id, and once another moderator purged that comment, every "Load more" was refused
|
|
8
|
+
* with "invalid cursor" and retrying sent the same cursor forever.
|
|
9
|
+
*/
|
|
10
|
+
/** A position in a `(created_at, id)` order. */
|
|
11
|
+
export type KeysetPosition = {
|
|
12
|
+
createdAt: string;
|
|
13
|
+
id: string;
|
|
14
|
+
};
|
|
15
|
+
/** Builds the cursor for the row a page ended on. @complexity O(1). */
|
|
16
|
+
export declare function encodeKeysetCursor(required: KeysetPosition, _optional?: Record<string, never>): string;
|
|
17
|
+
/** Splits a cursor {@link encodeKeysetCursor} built. A cursor it could not have built used to decode
|
|
18
|
+
* to `null` and read as "no cursor", silently restarting the scan from its first page (the newest
|
|
19
|
+
* ledger row; the oldest queued comment).
|
|
20
|
+
* @throws {ToolInputError} `invalid cursor` (a 400 to the tool and admin transports alike).
|
|
21
|
+
* @complexity O(cursor length). */
|
|
22
|
+
export declare function decodeKeysetCursor(required: {
|
|
23
|
+
cursor: string;
|
|
24
|
+
}, _optional?: Record<string, never>): KeysetPosition;
|
|
25
|
+
//# sourceMappingURL=keyset-cursor.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ToolInputError } from "@jini-ai/core";
|
|
2
|
+
/** Builds the cursor for the row a page ended on. @complexity O(1). */
|
|
3
|
+
export function encodeKeysetCursor(required, _optional = {}) {
|
|
4
|
+
return `${required.createdAt}::${required.id}`;
|
|
5
|
+
}
|
|
6
|
+
/** Splits a cursor {@link encodeKeysetCursor} built. A cursor it could not have built used to decode
|
|
7
|
+
* to `null` and read as "no cursor", silently restarting the scan from its first page (the newest
|
|
8
|
+
* ledger row; the oldest queued comment).
|
|
9
|
+
* @throws {ToolInputError} `invalid cursor` (a 400 to the tool and admin transports alike).
|
|
10
|
+
* @complexity O(cursor length). */
|
|
11
|
+
export function decodeKeysetCursor(required, _optional = {}) {
|
|
12
|
+
const separatorIndex = required.cursor.indexOf("::");
|
|
13
|
+
const createdAt = required.cursor.slice(0, separatorIndex);
|
|
14
|
+
const id = required.cursor.slice(separatorIndex + 2);
|
|
15
|
+
if (separatorIndex < 0 || id.length === 0 || Number.isNaN(Date.parse(createdAt))) {
|
|
16
|
+
throw new ToolInputError({ message: "invalid cursor" });
|
|
17
|
+
}
|
|
18
|
+
return { createdAt, id };
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=keyset-cursor.js.map
|
package/dist/core/ports.d.ts
CHANGED
|
@@ -147,4 +147,34 @@ export interface OutboxPort {
|
|
|
147
147
|
}, optional?: Record<string, never>): Promise<void>;
|
|
148
148
|
}
|
|
149
149
|
/** Clock and ID generator contracts are owned by the shared kernel primitives. */
|
|
150
|
+
/**
|
|
151
|
+
* Opens/commits/rolls back one transaction around `fn` — the same shape and contract as this
|
|
152
|
+
* codebase's other write chokepoints that need one (`RedirectsWriteDeps.transaction`,
|
|
153
|
+
* `SqliteSettingsRepo.transaction`).
|
|
154
|
+
*
|
|
155
|
+
* `deleteTerm`/`deleteTaxonomy` run their ENTIRE body — every guard read (`findById`,
|
|
156
|
+
* `countChildren`, `countByTerm`) AND every write (`delete`, `revisions.insert`, `outbox.enqueue`)
|
|
157
|
+
* — inside one call to this. Two failure modes this closes that a guard-then-transaction split
|
|
158
|
+
* would not:
|
|
159
|
+
*
|
|
160
|
+
* 1. **TOCTOU on the guard.** A guard read taken BEFORE the transaction opens can go stale the
|
|
161
|
+
* instant it returns — content can be assigned to the term in the gap between the read and the
|
|
162
|
+
* delete, and the delete would proceed having checked a now-wrong count. Putting the read
|
|
163
|
+
* inside the same transaction as the delete is what makes the guard's answer still true at the
|
|
164
|
+
* moment the delete actually runs.
|
|
165
|
+
* 2. **Partial cascade.** `deleteTaxonomy` deletes N member terms then the taxonomy row itself,
|
|
166
|
+
* with no FK/CASCADE at the schema level (`db/schema.ts` — `taxonomies`/`terms`/`entry_terms`/
|
|
167
|
+
* `taxonomy_revisions` are plain columns, no foreign keys) to undo a mid-cascade failure. A
|
|
168
|
+
* real transaction is the only thing that can.
|
|
169
|
+
*
|
|
170
|
+
* A caller-supplied identity/no-op implementation is a valid choice for a backend that has no
|
|
171
|
+
* separate transaction primitive to offer (see `repo.memory.ts`'s `InMemoryTaxonomyRepo` for the
|
|
172
|
+
* disclosed rationale) — the port exists so `deleteTerm`/`deleteTaxonomy` never have to know which
|
|
173
|
+
* kind of backend they're running against.
|
|
174
|
+
*/
|
|
175
|
+
export interface TransactionalRepoPort {
|
|
176
|
+
transaction<T>({ fn }: {
|
|
177
|
+
fn: () => Promise<T>;
|
|
178
|
+
}): Promise<T>;
|
|
179
|
+
}
|
|
150
180
|
//# sourceMappingURL=ports.d.ts.map
|
package/dist/core/ports.js
CHANGED
|
@@ -1,49 +1,4 @@
|
|
|
1
1
|
/** CMS permission checking preserves the CMS command denial contract. Generic registration wiring lives in the kernel. */
|
|
2
|
-
|
|
3
|
-
import type { AuthorizationPort } from "../authorization.js";
|
|
2
|
+
export { requireToolPermission, adaptLegacyAuthorize, type RequireToolPermissionRequired, type RequireToolPermissionOptional, } from "@jini-ai/core";
|
|
4
3
|
export type { AuthorizationPort, AuthorizationRequired, AuthorizationOptional } from "../authorization.js";
|
|
5
|
-
export interface RequireToolPermissionRequired {
|
|
6
|
-
authorize: AuthorizationPort;
|
|
7
|
-
workspaceId: string;
|
|
8
|
-
principalId: string;
|
|
9
|
-
permission: string;
|
|
10
|
-
}
|
|
11
|
-
export interface RequireToolPermissionOptional {
|
|
12
|
-
entityType?: string | undefined;
|
|
13
|
-
entityId?: string | undefined;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Runs the identical inline `authorize()` check the corresponding admin HTTP route performs, for a
|
|
17
|
-
* tool whose own domain function carries no `authorize()` call to inherit.
|
|
18
|
-
*
|
|
19
|
-
* This is NOT a second evaluator alongside another check — that is exactly the single-evaluator
|
|
20
|
-
* rule's concern. For each tool that
|
|
21
|
-
* calls it, this IS the only gate that tool's execution ever reaches, reached by an identical path
|
|
22
|
-
* to the one a human clicking the same admin route reaches. Domains split cleanly on which kind
|
|
23
|
-
* they are: content-types/Forms/Identity mutations and every Widgets write-service call self-enforce
|
|
24
|
-
* inside the domain function and must NOT call this; Comments/Members/Newsletter/Media/Menus/
|
|
25
|
-
* Database/Recovery gate in the route instead, so their handlers call this in the route's place.
|
|
26
|
-
* Which kind a given tool is stays documented next to that tool's handler, not here.
|
|
27
|
-
*
|
|
28
|
-
* Throws `core/commands`'s `ForbiddenError` — the same class `executeCommand` itself throws — so a
|
|
29
|
-
* tool caller and a route caller see the identical error shape for an identical denial. Throw
|
|
30
|
-
* rather than a 403 body because the caller here is `ToolExecutor`, which reads a thrown error as a
|
|
31
|
-
* failed execution and has no response to write to.
|
|
32
|
-
*
|
|
33
|
-
* Import the defining command module directly: its barrel also exports mutation appliers that
|
|
34
|
-
* name content features. Pulling that barrel into shared permission wiring would couple every
|
|
35
|
-
* tool domain to those features; the direct import preserves the identical denial class without
|
|
36
|
-
* that dependency closure.
|
|
37
|
-
*
|
|
38
|
-
* @param required - The `authorize()` evaluator and the workspace the run is scoped to.
|
|
39
|
-
* @param optional - Entity scope for who is asking, for what permission, optionally against which entity.
|
|
40
|
-
* @throws {ForbiddenError} If `authorize()` denies.
|
|
41
|
-
* @complexity O(1) beyond the injected `authorize()` call.
|
|
42
|
-
* @overallScore 100
|
|
43
|
-
*/
|
|
44
|
-
export declare function requireToolPermission(required: RequireToolPermissionRequired, optional?: RequireToolPermissionOptional): Promise<void>;
|
|
45
|
-
/** Adapt an existing evaluator without losing entity scope. @complexity O(1) plus evaluation. */
|
|
46
|
-
export declare function adaptLegacyAuthorize(required: {
|
|
47
|
-
authorize: AuthorizeFn;
|
|
48
|
-
}, _optional?: Record<string, never>): AuthorizationPort;
|
|
49
4
|
//# sourceMappingURL=args.d.ts.map
|
package/dist/core/tools/args.js
CHANGED
|
@@ -1,46 +1,3 @@
|
|
|
1
1
|
/** CMS permission checking preserves the CMS command denial contract. Generic registration wiring lives in the kernel. */
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Runs the identical inline `authorize()` check the corresponding admin HTTP route performs, for a
|
|
5
|
-
* tool whose own domain function carries no `authorize()` call to inherit.
|
|
6
|
-
*
|
|
7
|
-
* This is NOT a second evaluator alongside another check — that is exactly the single-evaluator
|
|
8
|
-
* rule's concern. For each tool that
|
|
9
|
-
* calls it, this IS the only gate that tool's execution ever reaches, reached by an identical path
|
|
10
|
-
* to the one a human clicking the same admin route reaches. Domains split cleanly on which kind
|
|
11
|
-
* they are: content-types/Forms/Identity mutations and every Widgets write-service call self-enforce
|
|
12
|
-
* inside the domain function and must NOT call this; Comments/Members/Newsletter/Media/Menus/
|
|
13
|
-
* Database/Recovery gate in the route instead, so their handlers call this in the route's place.
|
|
14
|
-
* Which kind a given tool is stays documented next to that tool's handler, not here.
|
|
15
|
-
*
|
|
16
|
-
* Throws `core/commands`'s `ForbiddenError` — the same class `executeCommand` itself throws — so a
|
|
17
|
-
* tool caller and a route caller see the identical error shape for an identical denial. Throw
|
|
18
|
-
* rather than a 403 body because the caller here is `ToolExecutor`, which reads a thrown error as a
|
|
19
|
-
* failed execution and has no response to write to.
|
|
20
|
-
*
|
|
21
|
-
* Import the defining command module directly: its barrel also exports mutation appliers that
|
|
22
|
-
* name content features. Pulling that barrel into shared permission wiring would couple every
|
|
23
|
-
* tool domain to those features; the direct import preserves the identical denial class without
|
|
24
|
-
* that dependency closure.
|
|
25
|
-
*
|
|
26
|
-
* @param required - The `authorize()` evaluator and the workspace the run is scoped to.
|
|
27
|
-
* @param optional - Entity scope for who is asking, for what permission, optionally against which entity.
|
|
28
|
-
* @throws {ForbiddenError} If `authorize()` denies.
|
|
29
|
-
* @complexity O(1) beyond the injected `authorize()` call.
|
|
30
|
-
* @overallScore 100
|
|
31
|
-
*/
|
|
32
|
-
export async function requireToolPermission(required, optional = {}) {
|
|
33
|
-
const result = await required.authorize({
|
|
34
|
-
principalId: required.principalId, permission: required.permission, workspaceId: required.workspaceId,
|
|
35
|
-
}, optional);
|
|
36
|
-
if (!result.allowed)
|
|
37
|
-
throw new ForbiddenError({
|
|
38
|
-
message: `principal '${required.principalId}' is not authorized for '${required.permission}' (${result.reason})`,
|
|
39
|
-
permission: required.permission, reason: result.reason,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/** Adapt an existing evaluator without losing entity scope. @complexity O(1) plus evaluation. */
|
|
43
|
-
export function adaptLegacyAuthorize(required, _optional = {}) {
|
|
44
|
-
return (request, optional = {}) => required.authorize({ ...request, ...optional });
|
|
45
|
-
}
|
|
2
|
+
export { requireToolPermission, adaptLegacyAuthorize, } from "@jini-ai/core";
|
|
46
3
|
//# sourceMappingURL=args.js.map
|
package/dist/media/index.d.ts
CHANGED
|
@@ -21,7 +21,6 @@ export { InMemoryMediaRepo, InMemoryAssetBlobRepo, InMemoryAssetRenditionRepo, I
|
|
|
21
21
|
export { withSha256Lock } from "./blob-gc-lock.js";
|
|
22
22
|
export { DEFAULT_GC_GRACE_MS, resolveGcGraceMs, isBlobUnreferenced, tombstoneBlobIfUnreferenced, runBlobGcDeletePass, runBlobGcUnlinkPass, runBlobGcCycle, runMonthlyOrphanSweepStub, } from "./blob-gc.js";
|
|
23
23
|
export { InMemoryBlobStore } from "./blob-store.memory.js";
|
|
24
|
-
export { LocalFsBlobStore, type LocalFsBlobStoreDeps } from "./blob-store.fs.js";
|
|
25
24
|
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, isValidMediaSlugFormat, type UploadMediaInput, type UploadMediaDeps, type UpdateMediaMetadataInput, type FindMediaByIdOrSlugRequired, } from "./media-service.js";
|
|
26
25
|
export type { TransformFit, TransformFormat, TransformParams, TransformDefinitionRecord, } from "./transform-types.js";
|
|
27
26
|
export { TransformValidationError, mimeForTransformFormat, MAX_TRANSFORM_DIMENSION_PX } from "./transform-types.js";
|
|
@@ -30,7 +29,6 @@ export { registerTransform, getLatestTransformDefinition, isLatestTransformVersi
|
|
|
30
29
|
export { resolveMediaRendition, type ResolveMediaRenditionDeps, type ResolveMediaRenditionInput, type ResolveMediaRenditionResult, } from "./rendition-service.js";
|
|
31
30
|
export type { ImageTransformerPort, TransformImageInput, TransformImageOutput } from "./image-transformer.js";
|
|
32
31
|
export { InMemoryImageTransformer } from "./image-transformer.js";
|
|
33
|
-
export { SharpImageTransformer, ImageTransformUnavailableError, ImageSourceCorruptError } from "./image-transformer.sharp.js";
|
|
34
32
|
export { sniffContentType, type SniffedContentType } from "./content-type-sniffer.js";
|
|
35
33
|
/** The agent-tool catalog for this domain (see `agent-tools.ts` for what is deliberately omitted). */
|
|
36
34
|
export { mediaAgentToolCatalog, } from "./agent-tools.js";
|
|
@@ -49,5 +47,4 @@ export type { IsBlobUnreferencedRequired, TombstoneBlobRequired, RunBlobGcDelete
|
|
|
49
47
|
export { rollbackUploadedMedia } from "./media-service.js";
|
|
50
48
|
export type { MediaRowCleanupDeps } from "./media-service.js";
|
|
51
49
|
export { VIDEO_DEFAULT_FRAMES, VIDEO_MAX_FRAMES, VIDEO_MAX_EDGE_PX, VIDEO_MAX_INPUT_BYTES, VIDEO_MAX_FRAME_BYTES, VIDEO_MAX_OUTPUT_BYTES, VIDEO_MAX_SHEET_BYTES, VIDEO_MAX_SHEETS, VIDEO_EXTRACTION_TIMEOUT_MS, planVideoFrames, parseVideoProbe, getVideoFrameEdge, planVideoContactSheets, type VideoMetadata, type VideoFrame, type VideoFrameOptions, type VideoFrameResult, type VideoFrameExtractor, type VideoSheetTile, type VideoSheetLayout, type VideoContactSheet, } from './video-frames.js';
|
|
52
|
-
export { createFfmpegVideoFrameExtractor, findVideoBinaries, runLowPriorityVideoProcess, type VideoBinaries, type VideoBinaryFinder, type VideoProcessRunner, } from './video-frames.ffmpeg.js';
|
|
53
50
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/media/index.js
CHANGED
|
@@ -6,14 +6,12 @@ export { InMemoryMediaRepo, InMemoryAssetBlobRepo, InMemoryAssetRenditionRepo, I
|
|
|
6
6
|
export { withSha256Lock } from "./blob-gc-lock.js";
|
|
7
7
|
export { DEFAULT_GC_GRACE_MS, resolveGcGraceMs, isBlobUnreferenced, tombstoneBlobIfUnreferenced, runBlobGcDeletePass, runBlobGcUnlinkPass, runBlobGcCycle, runMonthlyOrphanSweepStub, } from "./blob-gc.js";
|
|
8
8
|
export { InMemoryBlobStore } from "./blob-store.memory.js";
|
|
9
|
-
export { LocalFsBlobStore } from "./blob-store.fs.js";
|
|
10
9
|
export { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_ALLOWED_MIME_TYPES, resolveWriteOnceSource, uploadMedia, listMedia, getMediaById, findMediaByIdOrSlug, updateMediaMetadata, trashMedia, purgeMedia, isValidMediaSlugFormat, } from "./media-service.js";
|
|
11
10
|
export { TransformValidationError, mimeForTransformFormat, MAX_TRANSFORM_DIMENSION_PX } from "./transform-types.js";
|
|
12
11
|
export { withRenditionLock, withTransformRegistryLock } from "./transform-lock.js";
|
|
13
12
|
export { registerTransform, getLatestTransformDefinition, isLatestTransformVersion, isReferencedByPublishedContent, } from "./transform-registry.js";
|
|
14
13
|
export { resolveMediaRendition, } from "./rendition-service.js";
|
|
15
14
|
export { InMemoryImageTransformer } from "./image-transformer.js";
|
|
16
|
-
export { SharpImageTransformer, ImageTransformUnavailableError, ImageSourceCorruptError } from "./image-transformer.sharp.js";
|
|
17
15
|
// -----------------------------------------------------------------------------
|
|
18
16
|
// Original-bytes admin preview route support. See `content-type-sniffer.ts`'s
|
|
19
17
|
// file header for the disclosed scope: an allowlist magic-byte sniffer, not a
|
|
@@ -31,7 +29,6 @@ export { mediaAgentToolCatalog, } from "./agent-tools.js";
|
|
|
31
29
|
*/
|
|
32
30
|
export { buildMediaRegistrations, mediaDerivedRisk, } from "./tool-registrations.js";
|
|
33
31
|
export { rollbackUploadedMedia } from "./media-service.js";
|
|
34
|
-
// Video previews: policy/port
|
|
32
|
+
// Video previews: policy/port. The bounded optional-host codec adapter lives in ./node.js.
|
|
35
33
|
export { VIDEO_DEFAULT_FRAMES, VIDEO_MAX_FRAMES, VIDEO_MAX_EDGE_PX, VIDEO_MAX_INPUT_BYTES, VIDEO_MAX_FRAME_BYTES, VIDEO_MAX_OUTPUT_BYTES, VIDEO_MAX_SHEET_BYTES, VIDEO_MAX_SHEETS, VIDEO_EXTRACTION_TIMEOUT_MS, planVideoFrames, parseVideoProbe, getVideoFrameEdge, planVideoContactSheets, } from './video-frames.js';
|
|
36
|
-
export { createFfmpegVideoFrameExtractor, findVideoBinaries, runLowPriorityVideoProcess, } from './video-frames.ffmpeg.js';
|
|
37
34
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Node-bound media adapters; hosts supply the filesystem and optional codecs. */
|
|
2
|
+
export { LocalFsBlobStore, type LocalFsBlobStoreDeps } from "./blob-store.fs.js";
|
|
3
|
+
export { SharpImageTransformer, ImageTransformUnavailableError, ImageSourceCorruptError } from "./image-transformer.sharp.js";
|
|
4
|
+
export { createFfmpegVideoFrameExtractor, findVideoBinaries, runLowPriorityVideoProcess, type VideoBinaries, type VideoBinaryFinder, type VideoProcessRunner, } from './video-frames.ffmpeg.js';
|
|
5
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Node-bound media adapters; hosts supply the filesystem and optional codecs. */
|
|
2
|
+
export { LocalFsBlobStore } from "./blob-store.fs.js";
|
|
3
|
+
export { SharpImageTransformer, ImageTransformUnavailableError, ImageSourceCorruptError } from "./image-transformer.sharp.js";
|
|
4
|
+
// Video previews: policy/port plus a bounded optional-host codec adapter.
|
|
5
|
+
export { createFfmpegVideoFrameExtractor, findVideoBinaries, runLowPriorityVideoProcess, } from './video-frames.ffmpeg.js';
|
|
6
|
+
//# sourceMappingURL=node.js.map
|