@byline/core 3.8.0 → 3.10.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/@types/db-types.d.ts +103 -0
- package/dist/lib/errors.d.ts +11 -0
- package/dist/lib/errors.js +11 -0
- package/dist/services/document-lifecycle/audit.d.ts +45 -0
- package/dist/services/document-lifecycle/audit.js +67 -0
- package/dist/services/document-lifecycle/delete.js +21 -3
- package/dist/services/document-lifecycle/internals.d.ts +12 -4
- package/dist/services/document-lifecycle/internals.js +19 -5
- package/dist/services/document-lifecycle/status.js +28 -11
- package/dist/services/document-lifecycle/system-fields.d.ts +3 -1
- package/dist/services/document-lifecycle/system-fields.js +56 -19
- package/dist/services/document-lifecycle.test.node.js +118 -6
- package/package.json +2 -2
|
@@ -203,11 +203,39 @@ export interface IDbAdapter {
|
|
|
203
203
|
collections: ICollectionCommands;
|
|
204
204
|
documents: IDocumentCommands;
|
|
205
205
|
counters: ICounterCommands;
|
|
206
|
+
/**
|
|
207
|
+
* Append-only audit-log writes (docs/AUDIT.md — Workstream 2). Optional
|
|
208
|
+
* capability, paired with `withTransaction`: a consumer that records audit
|
|
209
|
+
* entries asserts both are present and throws otherwise (it must never
|
|
210
|
+
* silently skip the audit row). Adapters that model the audit log
|
|
211
|
+
* implement it; others omit it.
|
|
212
|
+
*/
|
|
213
|
+
audit?: IAuditCommands;
|
|
206
214
|
};
|
|
207
215
|
queries: {
|
|
208
216
|
collections: ICollectionQueries;
|
|
209
217
|
documents: IDocumentQueries;
|
|
218
|
+
/** Audit-log reads — per-document history, system-wide report. See docs/AUDIT.md. */
|
|
219
|
+
audit?: IAuditQueries;
|
|
210
220
|
};
|
|
221
|
+
/**
|
|
222
|
+
* Optional capability: run `fn` inside a single database transaction so the
|
|
223
|
+
* writes it performs commit or roll back atomically. The adapter propagates
|
|
224
|
+
* the transaction to every `commands.*` call made within `fn` (see
|
|
225
|
+
* docs/TRANSACTIONS.md — AsyncLocalStorage propagation), so a service can
|
|
226
|
+
* compose multiple commands into one unit of work without threading a
|
|
227
|
+
* transaction handle through their signatures.
|
|
228
|
+
*
|
|
229
|
+
* **Loud-failure contract.** Optional because not every adapter can provide
|
|
230
|
+
* interactive transactions — a pure HTTP-gateway serverless driver (Neon
|
|
231
|
+
* HTTP, Cloudflare D1, …) cannot. An adapter that cannot **must omit this
|
|
232
|
+
* method** (or implement it to throw); a consumer that requires atomicity
|
|
233
|
+
* (e.g. the audit log) MUST assert its presence and throw — never silently
|
|
234
|
+
* run non-atomically, which would defeat the very guarantee it provides. See
|
|
235
|
+
* docs/TRANSACTIONS.md ("Serverless / HTTP-gateway databases — the contract
|
|
236
|
+
* seam").
|
|
237
|
+
*/
|
|
238
|
+
withTransaction?: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
211
239
|
/**
|
|
212
240
|
* Optional maintenance: stamp `source_locale` (the per-document content
|
|
213
241
|
* anchor) on documents created before the column existed, setting NULL rows
|
|
@@ -221,6 +249,81 @@ export interface IDbAdapter {
|
|
|
221
249
|
rowsUpdated: number;
|
|
222
250
|
}>;
|
|
223
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* The realm of the actor that performed an audited change. `'admin'` for
|
|
254
|
+
* admin-user actions, `'user'` reserved for the end-user realm, `'system'`
|
|
255
|
+
* for deliberate internal-tooling writes. See docs/AUDIT.md.
|
|
256
|
+
*/
|
|
257
|
+
export type AuditActorRealm = 'admin' | 'user' | 'system';
|
|
258
|
+
/** Input to `IAuditCommands.append` — one audit-log row. */
|
|
259
|
+
export interface AuditLogAppendInput {
|
|
260
|
+
/** The document the change concerns; NULL for admin-realm (non-document) events. */
|
|
261
|
+
documentId?: string | null;
|
|
262
|
+
collectionId?: string | null;
|
|
263
|
+
/** The acting user id, only when it is a real persisted user (a UUID); NULL otherwise. */
|
|
264
|
+
actorId?: string | null;
|
|
265
|
+
actorRealm: AuditActorRealm;
|
|
266
|
+
/** Namespaced action, e.g. `document.path.changed`. */
|
|
267
|
+
action: string;
|
|
268
|
+
/** The changed field where meaningful (e.g. `path`); NULL for whole-entity events. */
|
|
269
|
+
field?: string | null;
|
|
270
|
+
/** Prior value (JSON-serialisable); NULL where not applicable. */
|
|
271
|
+
before?: unknown;
|
|
272
|
+
/** New value (JSON-serialisable); NULL where not applicable. */
|
|
273
|
+
after?: unknown;
|
|
274
|
+
}
|
|
275
|
+
/** A materialised audit-log row. */
|
|
276
|
+
export interface AuditLogEntry {
|
|
277
|
+
id: string;
|
|
278
|
+
documentId: string | null;
|
|
279
|
+
collectionId: string | null;
|
|
280
|
+
actorId: string | null;
|
|
281
|
+
actorRealm: string;
|
|
282
|
+
action: string;
|
|
283
|
+
field: string | null;
|
|
284
|
+
before: unknown;
|
|
285
|
+
after: unknown;
|
|
286
|
+
occurredAt: Date;
|
|
287
|
+
}
|
|
288
|
+
/** A page of audit-log entries with pagination metadata. */
|
|
289
|
+
export interface AuditLogPage {
|
|
290
|
+
entries: AuditLogEntry[];
|
|
291
|
+
meta: {
|
|
292
|
+
total: number;
|
|
293
|
+
page: number;
|
|
294
|
+
pageSize: number;
|
|
295
|
+
totalPages: number;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Append-only audit-log writes. The companion read interface is
|
|
300
|
+
* `IAuditQueries`. See docs/AUDIT.md — Workstream 2.
|
|
301
|
+
*/
|
|
302
|
+
export interface IAuditCommands {
|
|
303
|
+
/**
|
|
304
|
+
* Append one immutable audit-log row. The adapter generates the row id
|
|
305
|
+
* (UUIDv7) and `occurred_at`. Runs on the ambient executor, so when called
|
|
306
|
+
* inside `withTransaction` it commits atomically with the mutation it
|
|
307
|
+
* records — the load-bearing guarantee of the audit log.
|
|
308
|
+
*/
|
|
309
|
+
append(input: AuditLogAppendInput): Promise<{
|
|
310
|
+
id: string;
|
|
311
|
+
}>;
|
|
312
|
+
}
|
|
313
|
+
/** Audit-log reads. See docs/AUDIT.md — Workstreams 3 & 4. */
|
|
314
|
+
export interface IAuditQueries {
|
|
315
|
+
/**
|
|
316
|
+
* The audit history for one document, newest first, paged. Backs the
|
|
317
|
+
* document-history view. The caller is responsible for the access gate
|
|
318
|
+
* (the document's own read pipeline) before reaching this — it does no
|
|
319
|
+
* scoping of its own.
|
|
320
|
+
*/
|
|
321
|
+
getDocumentAuditLog(params: {
|
|
322
|
+
document_id: string;
|
|
323
|
+
page?: number;
|
|
324
|
+
page_size?: number;
|
|
325
|
+
}): Promise<AuditLogPage>;
|
|
326
|
+
}
|
|
224
327
|
/**
|
|
225
328
|
* Adapter capability for the shared-pool counter mechanism backing the
|
|
226
329
|
* `counter` field type. See `packages/core/src/@types/field-types.ts`
|
package/dist/lib/errors.d.ts
CHANGED
|
@@ -86,6 +86,7 @@ export declare const ErrorCodes: {
|
|
|
86
86
|
readonly STORAGE: "ERR_STORAGE";
|
|
87
87
|
readonly READ_BUDGET_EXCEEDED: "ERR_READ_BUDGET_EXCEEDED";
|
|
88
88
|
readonly PATH_CONFLICT: "ERR_PATH_CONFLICT";
|
|
89
|
+
readonly AUDIT_UNSUPPORTED: "ERR_AUDIT_UNSUPPORTED";
|
|
89
90
|
};
|
|
90
91
|
export declare const ERR_UNHANDLED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
91
92
|
export declare const ERR_NOT_FOUND: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
@@ -103,3 +104,13 @@ export declare const ERR_READ_BUDGET_EXCEEDED: (opts: BylineErrorOptions, errorC
|
|
|
103
104
|
* `byline_document_paths(collection_id, locale, path)`.
|
|
104
105
|
*/
|
|
105
106
|
export declare const ERR_PATH_CONFLICT: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
|
107
|
+
/**
|
|
108
|
+
* Thrown when an audited write (a document-grain change that must be recorded
|
|
109
|
+
* atomically — path / available-locales / status / delete) runs against a db
|
|
110
|
+
* adapter that does not provide both the `withTransaction` capability and the
|
|
111
|
+
* `commands.audit` / `queries.audit` surfaces. A misconfiguration, not a
|
|
112
|
+
* user error: an auditability guarantee cannot be honoured non-atomically, so
|
|
113
|
+
* the write is refused loudly rather than recorded with a gap. See
|
|
114
|
+
* docs/TRANSACTIONS.md and docs/AUDIT.md.
|
|
115
|
+
*/
|
|
116
|
+
export declare const ERR_AUDIT_UNSUPPORTED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
|
package/dist/lib/errors.js
CHANGED
|
@@ -119,6 +119,7 @@ export const ErrorCodes = {
|
|
|
119
119
|
STORAGE: 'ERR_STORAGE',
|
|
120
120
|
READ_BUDGET_EXCEEDED: 'ERR_READ_BUDGET_EXCEEDED',
|
|
121
121
|
PATH_CONFLICT: 'ERR_PATH_CONFLICT',
|
|
122
|
+
AUDIT_UNSUPPORTED: 'ERR_AUDIT_UNSUPPORTED',
|
|
122
123
|
};
|
|
123
124
|
// ---------------------------------------------------------------------------
|
|
124
125
|
// Pre-instantiated factories
|
|
@@ -139,3 +140,13 @@ export const ERR_READ_BUDGET_EXCEEDED = createErrorType(ErrorCodes.READ_BUDGET_E
|
|
|
139
140
|
* `byline_document_paths(collection_id, locale, path)`.
|
|
140
141
|
*/
|
|
141
142
|
export const ERR_PATH_CONFLICT = createErrorType(ErrorCodes.PATH_CONFLICT, 'warn');
|
|
143
|
+
/**
|
|
144
|
+
* Thrown when an audited write (a document-grain change that must be recorded
|
|
145
|
+
* atomically — path / available-locales / status / delete) runs against a db
|
|
146
|
+
* adapter that does not provide both the `withTransaction` capability and the
|
|
147
|
+
* `commands.audit` / `queries.audit` surfaces. A misconfiguration, not a
|
|
148
|
+
* user error: an auditability guarantee cannot be honoured non-atomically, so
|
|
149
|
+
* the write is refused loudly rather than recorded with a gap. See
|
|
150
|
+
* docs/TRANSACTIONS.md and docs/AUDIT.md.
|
|
151
|
+
*/
|
|
152
|
+
export const ERR_AUDIT_UNSUPPORTED = createErrorType(ErrorCodes.AUDIT_UNSUPPORTED);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
import type { AuditActorRealm, AuditLogAppendInput, IDbAdapter } from '../../@types/index.js';
|
|
9
|
+
import type { DocumentLifecycleContext } from './context.js';
|
|
10
|
+
/** Namespaced audit actions for document-grain changes. */
|
|
11
|
+
export declare const AUDIT_ACTIONS: {
|
|
12
|
+
readonly pathChanged: "document.path.changed";
|
|
13
|
+
readonly localesChanged: "document.locales.changed";
|
|
14
|
+
readonly statusChanged: "document.status.changed";
|
|
15
|
+
readonly deleted: "document.deleted";
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* The actor id + realm for an audit-log row. Mirrors `actorId()`: a real
|
|
19
|
+
* persisted user carries a UUID id and is recorded with realm `'admin'`
|
|
20
|
+
* (these write-points are admin-gated, document-grain operations). A synthetic
|
|
21
|
+
* script/seed actor (non-UUID) or no actor is a system/tooling write — NULL id,
|
|
22
|
+
* realm `'system'`. (A future `UserAuth`-driven write-point would extend this
|
|
23
|
+
* to `'user'`.)
|
|
24
|
+
*/
|
|
25
|
+
export declare function auditActor(ctx: DocumentLifecycleContext): {
|
|
26
|
+
actorId: string | undefined;
|
|
27
|
+
actorRealm: AuditActorRealm;
|
|
28
|
+
};
|
|
29
|
+
/** A non-null audit capability resolved from an adapter that supports it. */
|
|
30
|
+
export interface AuditCapability {
|
|
31
|
+
withTransaction: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
32
|
+
append: (input: AuditLogAppendInput) => Promise<{
|
|
33
|
+
id: string;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Assert the adapter can record an audited write atomically — it must provide
|
|
38
|
+
* **both** `withTransaction` and `commands.audit`. Returns a non-null
|
|
39
|
+
* capability the caller composes; throws `ERR_AUDIT_UNSUPPORTED` otherwise,
|
|
40
|
+
* rather than silently skipping the audit row or running it non-atomically.
|
|
41
|
+
* See docs/TRANSACTIONS.md and docs/AUDIT.md.
|
|
42
|
+
*/
|
|
43
|
+
export declare function requireAuditCapability(db: IDbAdapter): AuditCapability;
|
|
44
|
+
/** Order-insensitive equality for the advertised-locale set. */
|
|
45
|
+
export declare function sameLocaleSet(a: readonly string[], b: readonly string[]): boolean;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This Source Code is subject to the terms of the Mozilla Public
|
|
3
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
4
|
+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) Infonomic Company Limited
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Audit-log write helpers for the document-grain lifecycle write-points
|
|
10
|
+
* (docs/AUDIT.md — Workstream 2). The audit log records the changes the
|
|
11
|
+
* immutable version stream does NOT capture an actor for: non-versioned
|
|
12
|
+
* system-field writes (path, available-locales), in-place status transitions,
|
|
13
|
+
* and deletions. Each such mutation and its audit row commit atomically inside
|
|
14
|
+
* `withTransaction` — a silently-unwritten audit row is the one unacceptable
|
|
15
|
+
* outcome (see docs/TRANSACTIONS.md).
|
|
16
|
+
*/
|
|
17
|
+
import { ERR_AUDIT_UNSUPPORTED } from '../../lib/errors.js';
|
|
18
|
+
import { actorId } from './internals.js';
|
|
19
|
+
/** Namespaced audit actions for document-grain changes. */
|
|
20
|
+
export const AUDIT_ACTIONS = {
|
|
21
|
+
pathChanged: 'document.path.changed',
|
|
22
|
+
localesChanged: 'document.locales.changed',
|
|
23
|
+
statusChanged: 'document.status.changed',
|
|
24
|
+
deleted: 'document.deleted',
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The actor id + realm for an audit-log row. Mirrors `actorId()`: a real
|
|
28
|
+
* persisted user carries a UUID id and is recorded with realm `'admin'`
|
|
29
|
+
* (these write-points are admin-gated, document-grain operations). A synthetic
|
|
30
|
+
* script/seed actor (non-UUID) or no actor is a system/tooling write — NULL id,
|
|
31
|
+
* realm `'system'`. (A future `UserAuth`-driven write-point would extend this
|
|
32
|
+
* to `'user'`.)
|
|
33
|
+
*/
|
|
34
|
+
export function auditActor(ctx) {
|
|
35
|
+
const id = actorId(ctx);
|
|
36
|
+
return id != null
|
|
37
|
+
? { actorId: id, actorRealm: 'admin' }
|
|
38
|
+
: { actorId: undefined, actorRealm: 'system' };
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Assert the adapter can record an audited write atomically — it must provide
|
|
42
|
+
* **both** `withTransaction` and `commands.audit`. Returns a non-null
|
|
43
|
+
* capability the caller composes; throws `ERR_AUDIT_UNSUPPORTED` otherwise,
|
|
44
|
+
* rather than silently skipping the audit row or running it non-atomically.
|
|
45
|
+
* See docs/TRANSACTIONS.md and docs/AUDIT.md.
|
|
46
|
+
*/
|
|
47
|
+
export function requireAuditCapability(db) {
|
|
48
|
+
const withTransaction = db.withTransaction;
|
|
49
|
+
const audit = db.commands.audit;
|
|
50
|
+
if (withTransaction == null || audit == null) {
|
|
51
|
+
throw ERR_AUDIT_UNSUPPORTED({
|
|
52
|
+
message: 'audited write requires a db adapter with withTransaction + commands.audit support',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
withTransaction: (fn) => withTransaction(fn),
|
|
57
|
+
append: (input) => audit.append(input),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Order-insensitive equality for the advertised-locale set. */
|
|
61
|
+
export function sameLocaleSet(a, b) {
|
|
62
|
+
if (a.length !== b.length)
|
|
63
|
+
return false;
|
|
64
|
+
const sa = [...a].sort();
|
|
65
|
+
const sb = [...b].sort();
|
|
66
|
+
return sa.every((v, i) => v === sb[i]);
|
|
67
|
+
}
|
|
@@ -10,6 +10,7 @@ import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
|
10
10
|
import { ERR_NOT_FOUND } from '../../lib/errors.js';
|
|
11
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
12
12
|
import { getUploadFields } from '../../utils/storage-utils.js';
|
|
13
|
+
import { AUDIT_ACTIONS, auditActor, requireAuditCapability } from './audit.js';
|
|
13
14
|
import { invokeHook } from './internals.js';
|
|
14
15
|
/**
|
|
15
16
|
* Soft-delete a document.
|
|
@@ -91,9 +92,26 @@ export async function deleteDocument(ctx, params) {
|
|
|
91
92
|
};
|
|
92
93
|
// 2. beforeDelete hook.
|
|
93
94
|
await invokeHook(hooks?.beforeDelete, hookCtx);
|
|
94
|
-
// 3. Soft-delete all versions.
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
// 3. Soft-delete all versions, atomically with the audit record. A
|
|
96
|
+
// whole-document delete mints no new version, so the version stream
|
|
97
|
+
// never records it — the audit log is the only place a deletion is
|
|
98
|
+
// accountable (docs/AUDIT.md). Storage-file cleanup (step 4) is a
|
|
99
|
+
// DB↔external side-effect and stays OUTSIDE the transaction — it is
|
|
100
|
+
// post-commit, best-effort compensation (docs/TRANSACTIONS.md).
|
|
101
|
+
const audit = requireAuditCapability(db);
|
|
102
|
+
const actor = auditActor(ctx);
|
|
103
|
+
let deletedVersionCount = 0;
|
|
104
|
+
await audit.withTransaction(async () => {
|
|
105
|
+
deletedVersionCount = await db.commands.documents.softDeleteDocument({
|
|
106
|
+
document_id: params.documentId,
|
|
107
|
+
});
|
|
108
|
+
await audit.append({
|
|
109
|
+
documentId: params.documentId,
|
|
110
|
+
collectionId: ctx.collectionId,
|
|
111
|
+
actorId: actor.actorId,
|
|
112
|
+
actorRealm: actor.actorRealm,
|
|
113
|
+
action: AUDIT_ACTIONS.deleted,
|
|
114
|
+
});
|
|
97
115
|
});
|
|
98
116
|
// 4. Clean up storage files. Non-fatal: logs errors but does not throw.
|
|
99
117
|
if (ctx.storage && storagePathsToDelete.length > 0) {
|
|
@@ -17,10 +17,18 @@ import type { SlugifierFn } from '../../utils/slugify.js';
|
|
|
17
17
|
import type { DocumentLifecycleContext } from './context.js';
|
|
18
18
|
/**
|
|
19
19
|
* The acting user's id for the version audit trail (`created_by` on
|
|
20
|
-
* `byline_document_versions`).
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* `byline_document_versions`).
|
|
21
|
+
*
|
|
22
|
+
* Returns the id only when it is a real **persisted user id** — i.e. a UUID.
|
|
23
|
+
* Synthetic actors used by scripts, seeds, and tests (e.g.
|
|
24
|
+
* `createSuperAdminContext({ id: 'import-docs-script' })`, or the default
|
|
25
|
+
* `'super-admin'`) are **not** users: their non-UUID ids would be rejected by
|
|
26
|
+
* the `uuid` column outright, and the correct audit value for a system/tooling
|
|
27
|
+
* write is NULL regardless. So a non-UUID id — and a missing `requestContext`
|
|
28
|
+
* (the seeds/migrations escape hatch) — both yield `undefined` → NULL
|
|
29
|
+
* `created_by`, which the history strip renders as "unknown". Real
|
|
30
|
+
* `AdminAuth` / `UserAuth` actors always carry UUID ids, so their attribution
|
|
31
|
+
* is unaffected. See docs/AUDIT.md — Workstream 1.
|
|
24
32
|
*/
|
|
25
33
|
export declare function actorId(ctx: DocumentLifecycleContext): string | undefined;
|
|
26
34
|
/**
|
|
@@ -17,15 +17,29 @@ import { ERR_PATH_CONFLICT, ErrorCodes } from '../../lib/errors.js';
|
|
|
17
17
|
import { generateKeyBetween } from '../../lib/fractional-index.js';
|
|
18
18
|
import { createReadContext } from '../populate.js';
|
|
19
19
|
import { embedRichTextFields } from '../richtext-embed.js';
|
|
20
|
+
/**
|
|
21
|
+
* Matches a canonical UUID (any version). Real admin / end-user actors carry
|
|
22
|
+
* UUID ids (`uuidv7`); synthetic actors do not.
|
|
23
|
+
*/
|
|
24
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20
25
|
/**
|
|
21
26
|
* The acting user's id for the version audit trail (`created_by` on
|
|
22
|
-
* `byline_document_versions`).
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
27
|
+
* `byline_document_versions`).
|
|
28
|
+
*
|
|
29
|
+
* Returns the id only when it is a real **persisted user id** — i.e. a UUID.
|
|
30
|
+
* Synthetic actors used by scripts, seeds, and tests (e.g.
|
|
31
|
+
* `createSuperAdminContext({ id: 'import-docs-script' })`, or the default
|
|
32
|
+
* `'super-admin'`) are **not** users: their non-UUID ids would be rejected by
|
|
33
|
+
* the `uuid` column outright, and the correct audit value for a system/tooling
|
|
34
|
+
* write is NULL regardless. So a non-UUID id — and a missing `requestContext`
|
|
35
|
+
* (the seeds/migrations escape hatch) — both yield `undefined` → NULL
|
|
36
|
+
* `created_by`, which the history strip renders as "unknown". Real
|
|
37
|
+
* `AdminAuth` / `UserAuth` actors always carry UUID ids, so their attribution
|
|
38
|
+
* is unaffected. See docs/AUDIT.md — Workstream 1.
|
|
26
39
|
*/
|
|
27
40
|
export function actorId(ctx) {
|
|
28
|
-
|
|
41
|
+
const id = ctx.requestContext?.actor?.id;
|
|
42
|
+
return id != null && UUID_RE.test(id) ? id : undefined;
|
|
29
43
|
}
|
|
30
44
|
/**
|
|
31
45
|
* Safely invoke an optional hook slot, awaiting the result if it returns a
|
|
@@ -10,6 +10,7 @@ import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
|
10
10
|
import { ERR_INVALID_TRANSITION, ERR_NOT_FOUND } from '../../lib/errors.js';
|
|
11
11
|
import { withLogContext } from '../../lib/logger.js';
|
|
12
12
|
import { getWorkflow, validateStatusTransition } from '../../workflow/workflow.js';
|
|
13
|
+
import { AUDIT_ACTIONS, auditActor, requireAuditCapability } from './audit.js';
|
|
13
14
|
import { invokeHook } from './internals.js';
|
|
14
15
|
/**
|
|
15
16
|
* Change a document's workflow status.
|
|
@@ -85,18 +86,34 @@ export async function changeDocumentStatus(ctx, params) {
|
|
|
85
86
|
};
|
|
86
87
|
// 3. beforeStatusChange hook.
|
|
87
88
|
await invokeHook(hooks?.beforeStatusChange, hookCtx);
|
|
88
|
-
// 4. Mutate status in-place
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
await db.commands.documents.
|
|
96
|
-
|
|
97
|
-
|
|
89
|
+
// 4–5. Mutate status in-place + auto-archive, atomically with the audit
|
|
90
|
+
// record. Status mutates the version row rather than minting a new
|
|
91
|
+
// version, so the version stream never captures *who* changed it —
|
|
92
|
+
// the audit log is its only accountability home (docs/AUDIT.md).
|
|
93
|
+
const audit = requireAuditCapability(db);
|
|
94
|
+
const actor = auditActor(ctx);
|
|
95
|
+
await audit.withTransaction(async () => {
|
|
96
|
+
await db.commands.documents.setDocumentStatus({
|
|
97
|
+
document_version_id: documentVersionId,
|
|
98
|
+
status: params.nextStatus,
|
|
98
99
|
});
|
|
99
|
-
|
|
100
|
+
if (params.nextStatus === 'published') {
|
|
101
|
+
await db.commands.documents.archivePublishedVersions({
|
|
102
|
+
document_id: params.documentId,
|
|
103
|
+
excludeVersionId: documentVersionId,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
await audit.append({
|
|
107
|
+
documentId: params.documentId,
|
|
108
|
+
collectionId,
|
|
109
|
+
actorId: actor.actorId,
|
|
110
|
+
actorRealm: actor.actorRealm,
|
|
111
|
+
action: AUDIT_ACTIONS.statusChanged,
|
|
112
|
+
field: 'status',
|
|
113
|
+
before: currentStatus,
|
|
114
|
+
after: params.nextStatus,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
100
117
|
// 6. afterStatusChange hook.
|
|
101
118
|
await invokeHook(hooks?.afterStatusChange, hookCtx);
|
|
102
119
|
return { previousStatus: currentStatus, newStatus: params.nextStatus };
|
|
@@ -38,7 +38,9 @@ export interface UpdateDocumentSystemFieldsResult {
|
|
|
38
38
|
* `setDocumentAvailableLocales`.
|
|
39
39
|
*
|
|
40
40
|
* No content hooks fire — these are not content writes. Accountability for
|
|
41
|
-
* these mutations is the
|
|
41
|
+
* these mutations is the document-grain audit log: each field that actually
|
|
42
|
+
* changes records a `document.path.changed` / `document.locales.changed` row
|
|
43
|
+
* atomically with the write (docs/AUDIT.md — Workstream 2).
|
|
42
44
|
*
|
|
43
45
|
* @throws {BylineError} ERR_NOT_FOUND if the document does not exist.
|
|
44
46
|
* @throws {BylineError} ERR_PATH_CONFLICT if the path is already in use.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { assertActorCanPerform } from '../../auth/assert-actor-can-perform.js';
|
|
9
9
|
import { ERR_NOT_FOUND } from '../../lib/errors.js';
|
|
10
10
|
import { withLogContext } from '../../lib/logger.js';
|
|
11
|
+
import { AUDIT_ACTIONS, auditActor, requireAuditCapability, sameLocaleSet } from './audit.js';
|
|
11
12
|
import { resolvePathForUpdate, rethrowPathConflict } from './internals.js';
|
|
12
13
|
/**
|
|
13
14
|
* Write a document's system-managed, document-grain fields — `path` and the
|
|
@@ -34,7 +35,9 @@ import { resolvePathForUpdate, rethrowPathConflict } from './internals.js';
|
|
|
34
35
|
* `setDocumentAvailableLocales`.
|
|
35
36
|
*
|
|
36
37
|
* No content hooks fire — these are not content writes. Accountability for
|
|
37
|
-
* these mutations is the
|
|
38
|
+
* these mutations is the document-grain audit log: each field that actually
|
|
39
|
+
* changes records a `document.path.changed` / `document.locales.changed` row
|
|
40
|
+
* atomically with the write (docs/AUDIT.md — Workstream 2).
|
|
38
41
|
*
|
|
39
42
|
* @throws {BylineError} ERR_NOT_FOUND if the document does not exist.
|
|
40
43
|
* @throws {BylineError} ERR_PATH_CONFLICT if the path is already in use.
|
|
@@ -72,25 +75,59 @@ export async function updateDocumentSystemFields(ctx, params) {
|
|
|
72
75
|
documentId: params.documentId,
|
|
73
76
|
logger: ctx.logger,
|
|
74
77
|
});
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
path: pathForCommand,
|
|
82
|
-
})
|
|
83
|
-
.catch((err) => rethrowPathConflict(err, pathForCommand, defaultLocale));
|
|
84
|
-
}
|
|
85
|
-
// Advertised locales: rewrite the document-grain set wholesale.
|
|
78
|
+
// Both document-grain writes and their audit rows commit atomically.
|
|
79
|
+
// These fields are non-versioned, so the version stream never records
|
|
80
|
+
// them — the audit log is their only accountability home. One audit row
|
|
81
|
+
// per field that actually changed (docs/AUDIT.md).
|
|
82
|
+
const currentPath = originalData.path;
|
|
83
|
+
const currentLocales = originalData.availableLocales ?? [];
|
|
86
84
|
const availableLocalesWritten = params.availableLocales !== undefined;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
85
|
+
const audit = requireAuditCapability(db);
|
|
86
|
+
const actor = auditActor(ctx);
|
|
87
|
+
await audit.withTransaction(async () => {
|
|
88
|
+
if (pathForCommand !== undefined) {
|
|
89
|
+
await db.commands.documents
|
|
90
|
+
.updateDocumentPath({
|
|
91
|
+
documentId: params.documentId,
|
|
92
|
+
collectionId,
|
|
93
|
+
locale: sourceLocale,
|
|
94
|
+
path: pathForCommand,
|
|
95
|
+
})
|
|
96
|
+
.catch((err) => rethrowPathConflict(err, pathForCommand, defaultLocale));
|
|
97
|
+
if (pathForCommand !== currentPath) {
|
|
98
|
+
await audit.append({
|
|
99
|
+
documentId: params.documentId,
|
|
100
|
+
collectionId,
|
|
101
|
+
actorId: actor.actorId,
|
|
102
|
+
actorRealm: actor.actorRealm,
|
|
103
|
+
action: AUDIT_ACTIONS.pathChanged,
|
|
104
|
+
field: 'path',
|
|
105
|
+
before: currentPath ?? null,
|
|
106
|
+
after: pathForCommand,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Advertised locales: rewrite the document-grain set wholesale.
|
|
111
|
+
if (params.availableLocales !== undefined) {
|
|
112
|
+
await db.commands.documents.setDocumentAvailableLocales({
|
|
113
|
+
documentId: params.documentId,
|
|
114
|
+
collectionId,
|
|
115
|
+
availableLocales: params.availableLocales,
|
|
116
|
+
});
|
|
117
|
+
if (!sameLocaleSet(currentLocales, params.availableLocales)) {
|
|
118
|
+
await audit.append({
|
|
119
|
+
documentId: params.documentId,
|
|
120
|
+
collectionId,
|
|
121
|
+
actorId: actor.actorId,
|
|
122
|
+
actorRealm: actor.actorRealm,
|
|
123
|
+
action: AUDIT_ACTIONS.localesChanged,
|
|
124
|
+
field: 'availableLocales',
|
|
125
|
+
before: currentLocales,
|
|
126
|
+
after: params.availableLocales,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
});
|
|
94
131
|
return {
|
|
95
132
|
documentId: params.documentId,
|
|
96
133
|
path: pathForCommand,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { AdminAuth, AuthError, AuthErrorCodes, createRequestContext, createSuperAdminContext, } from '@byline/auth';
|
|
9
9
|
import { describe, expect, it, vi } from 'vitest';
|
|
10
10
|
import { BylineError, ERR_PATH_CONFLICT, ErrorCodes } from '../lib/errors.js';
|
|
11
|
-
import { changeDocumentStatus, copyToLocale, createDocument, deleteDocument, duplicateDocument, restoreDocumentVersion, unpublishDocument, updateDocument, updateDocumentWithPatches, } from './document-lifecycle/index.js';
|
|
11
|
+
import { changeDocumentStatus, copyToLocale, createDocument, deleteDocument, duplicateDocument, restoreDocumentVersion, unpublishDocument, updateDocument, updateDocumentSystemFields, updateDocumentWithPatches, } from './document-lifecycle/index.js';
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
// Fixtures / Helpers
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
@@ -36,6 +36,11 @@ function createMockDb() {
|
|
|
36
36
|
const getDocumentById = vi.fn().mockResolvedValue(null);
|
|
37
37
|
const getCurrentVersionMetadata = vi.fn().mockResolvedValue(null);
|
|
38
38
|
const getCurrentPath = vi.fn().mockResolvedValue('current-path');
|
|
39
|
+
// Audit capability (docs/AUDIT.md — W2). `withTransaction` is a passthrough
|
|
40
|
+
// in unit tests (runs the unit of work immediately, no real tx); `append`
|
|
41
|
+
// records the calls so write-point tests can assert the audit rows emitted.
|
|
42
|
+
const auditAppend = vi.fn().mockResolvedValue({ id: 'audit-1' });
|
|
43
|
+
const withTransaction = vi.fn(async (fn) => fn());
|
|
39
44
|
const db = {
|
|
40
45
|
commands: {
|
|
41
46
|
collections: {
|
|
@@ -45,8 +50,8 @@ function createMockDb() {
|
|
|
45
50
|
},
|
|
46
51
|
documents: {
|
|
47
52
|
createDocumentVersion,
|
|
48
|
-
updateDocumentPath: vi.fn(),
|
|
49
|
-
setDocumentAvailableLocales: vi.fn(),
|
|
53
|
+
updateDocumentPath: vi.fn().mockResolvedValue(undefined),
|
|
54
|
+
setDocumentAvailableLocales: vi.fn().mockResolvedValue(undefined),
|
|
50
55
|
setDocumentStatus,
|
|
51
56
|
archivePublishedVersions,
|
|
52
57
|
softDeleteDocument,
|
|
@@ -57,7 +62,9 @@ function createMockDb() {
|
|
|
57
62
|
ensureCounterGroup: vi.fn(),
|
|
58
63
|
nextCounterValue: vi.fn(),
|
|
59
64
|
},
|
|
65
|
+
audit: { append: auditAppend },
|
|
60
66
|
},
|
|
67
|
+
withTransaction: withTransaction,
|
|
61
68
|
queries: {
|
|
62
69
|
collections: {
|
|
63
70
|
getAllCollections: vi.fn(),
|
|
@@ -92,6 +99,8 @@ function createMockDb() {
|
|
|
92
99
|
getDocumentById,
|
|
93
100
|
getCurrentVersionMetadata,
|
|
94
101
|
getCurrentPath,
|
|
102
|
+
auditAppend,
|
|
103
|
+
withTransaction,
|
|
95
104
|
};
|
|
96
105
|
}
|
|
97
106
|
const noopLogger = {
|
|
@@ -104,6 +113,10 @@ const noopLogger = {
|
|
|
104
113
|
trace: vi.fn(),
|
|
105
114
|
silent: vi.fn(),
|
|
106
115
|
};
|
|
116
|
+
// A real persisted-user id is a UUID; `actorId()` only attributes UUIDs (see
|
|
117
|
+
// the regression note below). Use a valid UUID so the default context's
|
|
118
|
+
// writes are attributed.
|
|
119
|
+
const TEST_ACTOR_ID = '01901234-0000-7000-8000-000000000001';
|
|
107
120
|
function buildCtx(db, definition = minimalCollection) {
|
|
108
121
|
return {
|
|
109
122
|
db,
|
|
@@ -117,7 +130,7 @@ function buildCtx(db, definition = minimalCollection) {
|
|
|
117
130
|
// tests do not have to care about ability enforcement. The dedicated
|
|
118
131
|
// "enforcement" block below covers the missing-context / missing-ability
|
|
119
132
|
// negative cases.
|
|
120
|
-
requestContext: createSuperAdminContext({ id:
|
|
133
|
+
requestContext: createSuperAdminContext({ id: TEST_ACTOR_ID }),
|
|
121
134
|
};
|
|
122
135
|
}
|
|
123
136
|
// ---------------------------------------------------------------------------
|
|
@@ -148,7 +161,18 @@ describe('Document lifecycle service', () => {
|
|
|
148
161
|
});
|
|
149
162
|
// Audit contract (docs/AUDIT.md — W1): every version row
|
|
150
163
|
// records the actor that created it.
|
|
151
|
-
expect(createDocumentVersion.mock.calls[0]?.[0].createdBy).toBe(
|
|
164
|
+
expect(createDocumentVersion.mock.calls[0]?.[0].createdBy).toBe(TEST_ACTOR_ID);
|
|
165
|
+
});
|
|
166
|
+
it('writes NULL createdBy for a synthetic (non-UUID) script/seed actor', async () => {
|
|
167
|
+
// Regression guard (v3.8.0): a synthetic super-admin id such as
|
|
168
|
+
// `import-docs-script` is not a real user and is not a UUID — writing
|
|
169
|
+
// it into the `created_by` UUID column crashed every import/seed. Such
|
|
170
|
+
// system/tooling writes must attribute to NULL, not the synthetic id.
|
|
171
|
+
const { db, createDocumentVersion } = createMockDb();
|
|
172
|
+
const ctx = buildCtx(db);
|
|
173
|
+
ctx.requestContext = createSuperAdminContext({ id: 'import-docs-script' });
|
|
174
|
+
await createDocument(ctx, { data: { title: 'Hello' }, locale: 'en' });
|
|
175
|
+
expect(createDocumentVersion.mock.calls[0]?.[0].createdBy).toBeUndefined();
|
|
152
176
|
});
|
|
153
177
|
it('invokes beforeCreate and afterCreate hooks in order', async () => {
|
|
154
178
|
const callOrder = [];
|
|
@@ -326,7 +350,7 @@ describe('Document lifecycle service', () => {
|
|
|
326
350
|
documentId: 'doc-1',
|
|
327
351
|
data: { title: 'New' },
|
|
328
352
|
});
|
|
329
|
-
expect(createDocumentVersion.mock.calls[0]?.[0].createdBy).toBe(
|
|
353
|
+
expect(createDocumentVersion.mock.calls[0]?.[0].createdBy).toBe(TEST_ACTOR_ID);
|
|
330
354
|
});
|
|
331
355
|
it('fetches the original before calling hooks', async () => {
|
|
332
356
|
const { db, getDocumentById, createDocumentVersion } = createMockDb();
|
|
@@ -668,6 +692,24 @@ describe('Document lifecycle service', () => {
|
|
|
668
692
|
expect(result.previousStatus).toBe('draft');
|
|
669
693
|
expect(result.newStatus).toBe('published');
|
|
670
694
|
});
|
|
695
|
+
it('records a document.status.changed audit row atomically (from → to)', async () => {
|
|
696
|
+
const { db, getCurrentVersionMetadata, auditAppend, withTransaction } = createMockDb();
|
|
697
|
+
getCurrentVersionMetadata.mockResolvedValue({ ...metadataRow });
|
|
698
|
+
const ctx = buildCtx(db);
|
|
699
|
+
await changeDocumentStatus(ctx, { documentId: 'doc-1', nextStatus: 'published' });
|
|
700
|
+
// The mutation + audit row run inside one withTransaction (docs/AUDIT.md).
|
|
701
|
+
expect(withTransaction).toHaveBeenCalledOnce();
|
|
702
|
+
expect(auditAppend).toHaveBeenCalledWith(expect.objectContaining({
|
|
703
|
+
documentId: 'doc-1',
|
|
704
|
+
collectionId: 'col-1',
|
|
705
|
+
actorId: TEST_ACTOR_ID,
|
|
706
|
+
actorRealm: 'admin',
|
|
707
|
+
action: 'document.status.changed',
|
|
708
|
+
field: 'status',
|
|
709
|
+
before: 'draft',
|
|
710
|
+
after: 'published',
|
|
711
|
+
}));
|
|
712
|
+
});
|
|
671
713
|
it('throws ERR_NOT_FOUND when document is missing', async () => {
|
|
672
714
|
const { db, getCurrentVersionMetadata } = createMockDb();
|
|
673
715
|
getCurrentVersionMetadata.mockResolvedValue(null);
|
|
@@ -859,6 +901,76 @@ describe('Document lifecycle service', () => {
|
|
|
859
901
|
expect(beforeDelete).toHaveBeenCalledWith(expect.objectContaining({ documentId: 'doc-1', path: 'doc-to-delete' }));
|
|
860
902
|
expect(afterDelete).toHaveBeenCalledWith(expect.objectContaining({ documentId: 'doc-1', path: 'doc-to-delete' }));
|
|
861
903
|
});
|
|
904
|
+
it('records a document.deleted audit row atomically with the soft-delete', async () => {
|
|
905
|
+
const { db, getDocumentById, softDeleteDocument, auditAppend, withTransaction } = createMockDb();
|
|
906
|
+
getDocumentById.mockResolvedValue({
|
|
907
|
+
document_version_id: 'ver-1',
|
|
908
|
+
document_id: 'doc-1',
|
|
909
|
+
path: 'doc-to-delete',
|
|
910
|
+
fields: {},
|
|
911
|
+
});
|
|
912
|
+
const ctx = buildCtx(db);
|
|
913
|
+
await deleteDocument(ctx, { documentId: 'doc-1' });
|
|
914
|
+
expect(withTransaction).toHaveBeenCalledOnce();
|
|
915
|
+
expect(softDeleteDocument).toHaveBeenCalledWith({ document_id: 'doc-1' });
|
|
916
|
+
expect(auditAppend).toHaveBeenCalledWith(expect.objectContaining({
|
|
917
|
+
documentId: 'doc-1',
|
|
918
|
+
collectionId: 'col-1',
|
|
919
|
+
actorRealm: 'admin',
|
|
920
|
+
action: 'document.deleted',
|
|
921
|
+
}));
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
// -----------------------------------------------------------------------
|
|
925
|
+
// updateDocumentSystemFields (audited, non-versioned)
|
|
926
|
+
// -----------------------------------------------------------------------
|
|
927
|
+
describe('updateDocumentSystemFields', () => {
|
|
928
|
+
function setupDoc(getDocumentById, overrides) {
|
|
929
|
+
getDocumentById.mockResolvedValue({
|
|
930
|
+
document_version_id: 'ver-1',
|
|
931
|
+
document_id: 'doc-1',
|
|
932
|
+
path: 'old-slug',
|
|
933
|
+
source_locale: 'en',
|
|
934
|
+
availableLocales: ['en'],
|
|
935
|
+
fields: {},
|
|
936
|
+
...overrides,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
it('records document.path.changed when the path actually changes', async () => {
|
|
940
|
+
const { db, getDocumentById, auditAppend, withTransaction } = createMockDb();
|
|
941
|
+
setupDoc(getDocumentById);
|
|
942
|
+
const ctx = buildCtx(db);
|
|
943
|
+
await updateDocumentSystemFields(ctx, { documentId: 'doc-1', path: 'new-slug' });
|
|
944
|
+
expect(withTransaction).toHaveBeenCalledOnce();
|
|
945
|
+
expect(auditAppend).toHaveBeenCalledWith(expect.objectContaining({
|
|
946
|
+
action: 'document.path.changed',
|
|
947
|
+
field: 'path',
|
|
948
|
+
before: 'old-slug',
|
|
949
|
+
after: 'new-slug',
|
|
950
|
+
}));
|
|
951
|
+
});
|
|
952
|
+
it('records no audit row when the path is unchanged', async () => {
|
|
953
|
+
const { db, getDocumentById, auditAppend } = createMockDb();
|
|
954
|
+
setupDoc(getDocumentById);
|
|
955
|
+
const ctx = buildCtx(db);
|
|
956
|
+
await updateDocumentSystemFields(ctx, { documentId: 'doc-1', path: 'old-slug' });
|
|
957
|
+
expect(auditAppend).not.toHaveBeenCalled();
|
|
958
|
+
});
|
|
959
|
+
it('records document.locales.changed with before/after sets', async () => {
|
|
960
|
+
const { db, getDocumentById, auditAppend } = createMockDb();
|
|
961
|
+
setupDoc(getDocumentById);
|
|
962
|
+
const ctx = buildCtx(db);
|
|
963
|
+
await updateDocumentSystemFields(ctx, {
|
|
964
|
+
documentId: 'doc-1',
|
|
965
|
+
availableLocales: ['en', 'fr'],
|
|
966
|
+
});
|
|
967
|
+
expect(auditAppend).toHaveBeenCalledWith(expect.objectContaining({
|
|
968
|
+
action: 'document.locales.changed',
|
|
969
|
+
field: 'availableLocales',
|
|
970
|
+
before: ['en'],
|
|
971
|
+
after: ['en', 'fr'],
|
|
972
|
+
}));
|
|
973
|
+
});
|
|
862
974
|
});
|
|
863
975
|
// -----------------------------------------------------------------------
|
|
864
976
|
// restoreDocumentVersion
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@byline/core",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
|
-
"version": "3.
|
|
5
|
+
"version": "3.10.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=20.9.0"
|
|
8
8
|
},
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"sharp": "^0.34.5",
|
|
80
80
|
"uuid": "^14.0.0",
|
|
81
81
|
"zod": "^4.4.3",
|
|
82
|
-
"@byline/auth": "3.
|
|
82
|
+
"@byline/auth": "3.10.0"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@biomejs/biome": "2.4.15",
|