@jini-ai/cms 0.5.6 → 0.5.7
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 +4 -0
- package/dist/media/media-service.d.ts +1 -3
- package/dist/media/media-service.js +2 -26
- package/dist/media/slug.d.ts +8 -0
- package/dist/media/slug.js +32 -0
- package/package.json +53 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 0.5.7 — 2026-10-12
|
|
2
|
+
|
|
3
|
+
- New render-only leaf subpaths: `./forms/html-render`, `./forms/submission-attempts`, `./widgets/page-embed-types`, `./widgets/html/html-embeds`, `./widgets/html/gallery-render`, `./navigation/types`, `./navigation/dropdown` and `./media/slug` (the media slug rule, still re-exported from `./media`). Existing barrels are unchanged.
|
|
4
|
+
|
|
1
5
|
## 0.5.4 — 2026-10-08
|
|
2
6
|
|
|
3
7
|
- Ships `dist/forms/**`: 0.5.3's `files` negation `!forms/**` also stripped `dist/forms`, so `@jini-ai/cms/forms` (and `/html`, `/sql`, `/express`) failed to resolve. The negation is removed: `files` already ships only `dist`, and the nested `forms/` package is never packed.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { isValidMediaSlugFormat } from "./slug.js";
|
|
1
2
|
import type { Clock, IdGenerator, UUID } from "@jini-ai/core/primitives";
|
|
2
3
|
import { MediaConflictError, MediaNotFoundError, MediaSourceImmutableError, MediaStillReferencedError, MediaValidationError, type MediaRecord, type MediaSource } from "./types.js";
|
|
3
4
|
import type { AssetBlobRepoPort, AssetRenditionRepoPort, BlobStorePort, MediaRepoPort } from "./ports.js";
|
|
@@ -53,9 +54,6 @@ export declare function resolveWriteOnceSource(required: {
|
|
|
53
54
|
existing: MediaSource | undefined;
|
|
54
55
|
requestedSha256: string;
|
|
55
56
|
}, _optional?: Record<string, never>): MediaSource;
|
|
56
|
-
export declare function isValidMediaSlugFormat(requiredArgs: {
|
|
57
|
-
slug: string;
|
|
58
|
-
}, optionalArgs?: Record<string, never>): boolean;
|
|
59
57
|
export interface UploadMediaInput {
|
|
60
58
|
workspaceId: UUID;
|
|
61
59
|
bytes: Uint8Array;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isValidMediaSlugFormat } from "./slug.js";
|
|
2
|
+
export { isValidMediaSlugFormat } from "./slug.js";
|
|
1
3
|
import { createSystemClock } from "@jini-ai/core/primitives";
|
|
2
4
|
import { nowIso as kernelNowIso } from "@jini-ai/core/primitives";
|
|
3
5
|
/**
|
|
@@ -145,28 +147,6 @@ function deriveTitleFromFilename(filename) {
|
|
|
145
147
|
const base = filename.trim().replace(/\.[^./\\]+$/, "");
|
|
146
148
|
return base.trim() || "Untitled";
|
|
147
149
|
}
|
|
148
|
-
/** Shared slug-format rule (2026-09-07) — same shape as `post`'s `SLUG_FORMAT_PATTERN`: lowercase
|
|
149
|
-
* letters, digits, and dashes only. Media slugs have no reserved-word list (`post`'s `admin`/`api`
|
|
150
|
-
* exclusions exist because a post slug can become a literal URL path segment a route dispatches
|
|
151
|
-
* on; a media slug is only ever a lookup key, never a route itself, so that hazard doesn't apply). */
|
|
152
|
-
const MEDIA_SLUG_FORMAT_PATTERN = /^[a-z0-9-]+$/;
|
|
153
|
-
/**
|
|
154
|
-
* Slugs that spell a UUID are refused outright (2026-09-07 fix).
|
|
155
|
-
*
|
|
156
|
-
* A lowercase UUID is `[a-z0-9-]+` character for character, so {@link MEDIA_SLUG_FORMAT_PATTERN}
|
|
157
|
-
* happily accepted one — and {@link findMediaByIdOrSlug} used to resolve SLUG FIRST, so pointing one
|
|
158
|
-
* asset's slug at another asset's `id` silently took over every reference already authored against
|
|
159
|
-
* that id (every `/m/{id}/…` URL a host's renderer has ever emitted for the victim). That resolution
|
|
160
|
-
* order is now id-first, which closes the hijack for rows that already carry such a slug; this rule
|
|
161
|
-
* is the write-path half — a slug that can only ever shadow an id is not a slug anyone wants, so it
|
|
162
|
-
* is refused where a human can still see why rather than silently shadowed at read time.
|
|
163
|
-
*
|
|
164
|
-
* Shape-based, not a lookup: it rejects EVERY UUID-shaped value, not only ones that currently
|
|
165
|
-
* collide, so a slug cannot become a hijack later when some future upload happens to mint that id.
|
|
166
|
-
* Ordinary hex-and-dash slugs (`abc-123-def`, `2026-09-07-launch-clip`) are unaffected — nothing but
|
|
167
|
-
* the exact 8-4-4-4-12 hex grouping matches.
|
|
168
|
-
*/
|
|
169
|
-
const MEDIA_SLUG_UUID_SHAPE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
170
150
|
/** Shared slug max-length rule (2026-09-07) — same bound as `post.ts`'s `MAX_SLUG_LENGTH`, reused
|
|
171
151
|
* rather than inventing a second number. `post.ts` only enforces this on its EXPLICIT/caller-
|
|
172
152
|
* supplied slug path (`resolveExplicitSlug`, reject-with-error); its own title-derived path is
|
|
@@ -177,10 +157,6 @@ const MEDIA_SLUG_UUID_SHAPE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9
|
|
|
177
157
|
* human for a shorter value — silently truncating an internally-derived value is the same
|
|
178
158
|
* "coerce, don't fail" convention `deriveTitleFromFilename`/`slugifyMediaTitle` already use. */
|
|
179
159
|
const MEDIA_MAX_SLUG_LENGTH = 120;
|
|
180
|
-
export function isValidMediaSlugFormat(requiredArgs, optionalArgs = {}) {
|
|
181
|
-
const { slug } = requiredArgs;
|
|
182
|
-
return MEDIA_SLUG_FORMAT_PATTERN.test(slug) && !MEDIA_SLUG_UUID_SHAPE_PATTERN.test(slug);
|
|
183
|
-
}
|
|
184
160
|
/** Truncates a slug candidate to `maxLength`, stripping a trailing dash the cut can introduce (so a
|
|
185
161
|
* truncation never leaves a slug ending mid-word with a dangling "-"), falling back to `"untitled"`
|
|
186
162
|
* if nothing legible survives (only reachable when `maxLength` itself is tiny, e.g. while reserving
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Return whether a media lookup slug has the accepted shape, without I/O.
|
|
2
|
+
* @complexity O(n) time in the slug length, O(1) space.
|
|
3
|
+
* @example isValidMediaSlugFormat({ slug: "summer-photo" }); // true
|
|
4
|
+
*/
|
|
5
|
+
export declare function isValidMediaSlugFormat(requiredArgs: {
|
|
6
|
+
slug: string;
|
|
7
|
+
}, optionalArgs?: Record<string, never>): boolean;
|
|
8
|
+
//# sourceMappingURL=slug.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** @file Pure media slug policy; writes and public URL rendering share the same format and UUID-shadowing rule. */
|
|
2
|
+
/** Shared slug-format rule (2026-09-07) — same shape as `post`'s `SLUG_FORMAT_PATTERN`: lowercase
|
|
3
|
+
* letters, digits, and dashes only. Media slugs have no reserved-word list (`post`'s `admin`/`api`
|
|
4
|
+
* exclusions exist because a post slug can become a literal URL path segment a route dispatches
|
|
5
|
+
* on; a media slug is only ever a lookup key, never a route itself, so that hazard doesn't apply). */
|
|
6
|
+
const MEDIA_SLUG_FORMAT_PATTERN = /^[a-z0-9-]+$/;
|
|
7
|
+
/**
|
|
8
|
+
* Slugs that spell a UUID are refused outright (2026-09-07 fix).
|
|
9
|
+
*
|
|
10
|
+
* A lowercase UUID is `[a-z0-9-]+` character for character, so {@link MEDIA_SLUG_FORMAT_PATTERN}
|
|
11
|
+
* happily accepted one — and {@link findMediaByIdOrSlug} used to resolve SLUG FIRST, so pointing one
|
|
12
|
+
* asset's slug at another asset's `id` silently took over every reference already authored against
|
|
13
|
+
* that id (every `/m/{id}/…` URL a host's renderer has ever emitted for the victim). That resolution
|
|
14
|
+
* order is now id-first, which closes the hijack for rows that already carry such a slug; this rule
|
|
15
|
+
* is the write-path half — a slug that can only ever shadow an id is not a slug anyone wants, so it
|
|
16
|
+
* is refused where a human can still see why rather than silently shadowed at read time.
|
|
17
|
+
*
|
|
18
|
+
* Shape-based, not a lookup: it rejects EVERY UUID-shaped value, not only ones that currently
|
|
19
|
+
* collide, so a slug cannot become a hijack later when some future upload happens to mint that id.
|
|
20
|
+
* Ordinary hex-and-dash slugs (`abc-123-def`, `2026-09-07-launch-clip`) are unaffected — nothing but
|
|
21
|
+
* the exact 8-4-4-4-12 hex grouping matches.
|
|
22
|
+
*/
|
|
23
|
+
const MEDIA_SLUG_UUID_SHAPE_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
24
|
+
/** Return whether a media lookup slug has the accepted shape, without I/O.
|
|
25
|
+
* @complexity O(n) time in the slug length, O(1) space.
|
|
26
|
+
* @example isValidMediaSlugFormat({ slug: "summer-photo" }); // true
|
|
27
|
+
*/
|
|
28
|
+
export function isValidMediaSlugFormat(requiredArgs, optionalArgs = {}) {
|
|
29
|
+
const { slug } = requiredArgs;
|
|
30
|
+
return MEDIA_SLUG_FORMAT_PATTERN.test(slug) && !MEDIA_SLUG_UUID_SHAPE_PATTERN.test(slug);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=slug.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jini-ai/cms",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.7",
|
|
4
4
|
"description": "Content-model capability for Jini-hosted products: framework-free contracts, ports, and domain services in `/core` and per-domain subpaths; Node-bound adapters in domain-specific subpaths.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -29,6 +29,14 @@
|
|
|
29
29
|
"./trash": "universal",
|
|
30
30
|
"./settings/express": "node",
|
|
31
31
|
"./media/node": "node",
|
|
32
|
+
"./forms/html-render": "universal",
|
|
33
|
+
"./forms/submission-attempts": "universal",
|
|
34
|
+
"./widgets/page-embed-types": "universal",
|
|
35
|
+
"./widgets/html/html-embeds": "node",
|
|
36
|
+
"./widgets/html/gallery-render": "universal",
|
|
37
|
+
"./navigation/types": "universal",
|
|
38
|
+
"./navigation/dropdown": "universal",
|
|
39
|
+
"./media/slug": "universal",
|
|
32
40
|
"./forms": "universal",
|
|
33
41
|
"./forms/html": "node",
|
|
34
42
|
"./forms/sql": "node",
|
|
@@ -214,6 +222,46 @@
|
|
|
214
222
|
"import": "./dist/media/node.js",
|
|
215
223
|
"default": "./dist/media/node.js"
|
|
216
224
|
},
|
|
225
|
+
"./forms/html-render": {
|
|
226
|
+
"types": "./dist/forms/html-render.d.ts",
|
|
227
|
+
"import": "./dist/forms/html-render.js",
|
|
228
|
+
"default": "./dist/forms/html-render.js"
|
|
229
|
+
},
|
|
230
|
+
"./forms/submission-attempts": {
|
|
231
|
+
"types": "./dist/forms/submission-attempts.d.ts",
|
|
232
|
+
"import": "./dist/forms/submission-attempts.js",
|
|
233
|
+
"default": "./dist/forms/submission-attempts.js"
|
|
234
|
+
},
|
|
235
|
+
"./widgets/page-embed-types": {
|
|
236
|
+
"types": "./dist/widgets/page-embed-types.d.ts",
|
|
237
|
+
"import": "./dist/widgets/page-embed-types.js",
|
|
238
|
+
"default": "./dist/widgets/page-embed-types.js"
|
|
239
|
+
},
|
|
240
|
+
"./widgets/html/html-embeds": {
|
|
241
|
+
"types": "./dist/widgets/html/html-embeds.d.ts",
|
|
242
|
+
"import": "./dist/widgets/html/html-embeds.js",
|
|
243
|
+
"default": "./dist/widgets/html/html-embeds.js"
|
|
244
|
+
},
|
|
245
|
+
"./widgets/html/gallery-render": {
|
|
246
|
+
"types": "./dist/widgets/html/gallery-render.d.ts",
|
|
247
|
+
"import": "./dist/widgets/html/gallery-render.js",
|
|
248
|
+
"default": "./dist/widgets/html/gallery-render.js"
|
|
249
|
+
},
|
|
250
|
+
"./navigation/types": {
|
|
251
|
+
"types": "./dist/navigation/types.d.ts",
|
|
252
|
+
"import": "./dist/navigation/types.js",
|
|
253
|
+
"default": "./dist/navigation/types.js"
|
|
254
|
+
},
|
|
255
|
+
"./navigation/dropdown": {
|
|
256
|
+
"types": "./dist/navigation/dropdown.d.ts",
|
|
257
|
+
"import": "./dist/navigation/dropdown.js",
|
|
258
|
+
"default": "./dist/navigation/dropdown.js"
|
|
259
|
+
},
|
|
260
|
+
"./media/slug": {
|
|
261
|
+
"types": "./dist/media/slug.d.ts",
|
|
262
|
+
"import": "./dist/media/slug.js",
|
|
263
|
+
"default": "./dist/media/slug.js"
|
|
264
|
+
},
|
|
217
265
|
"./forms": {
|
|
218
266
|
"types": "./dist/forms/index.d.ts",
|
|
219
267
|
"import": "./dist/forms/index.js",
|
|
@@ -364,12 +412,12 @@
|
|
|
364
412
|
"@types/better-sqlite3": "^7.6.13",
|
|
365
413
|
"@electric-sql/pglite": "0.5.8",
|
|
366
414
|
"jsdom": "^25.0.1",
|
|
367
|
-
"@jini-ai/
|
|
368
|
-
"@jini-ai/db": "0.3.
|
|
369
|
-
"@jini-ai/
|
|
415
|
+
"@jini-ai/core": "0.4.4",
|
|
416
|
+
"@jini-ai/db": "0.3.3",
|
|
417
|
+
"@jini-ai/http-kit": "0.4.1"
|
|
370
418
|
},
|
|
371
419
|
"dependencies": {
|
|
372
|
-
"@jini-ai/core": "^0.4.
|
|
420
|
+
"@jini-ai/core": "^0.4.4"
|
|
373
421
|
},
|
|
374
422
|
"peerDependencies": {
|
|
375
423
|
"sharp": "^0.35.3",
|