@byline/core 3.21.0 → 4.1.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.
Files changed (77) hide show
  1. package/dist/@types/collection-types.d.ts +66 -31
  2. package/dist/@types/db-types.d.ts +93 -47
  3. package/dist/@types/field-types.d.ts +25 -1
  4. package/dist/@types/query-predicate.d.ts +3 -3
  5. package/dist/@types/search-types.d.ts +3 -4
  6. package/dist/@types/site-config.d.ts +51 -12
  7. package/dist/auth/apply-before-read.d.ts +23 -11
  8. package/dist/auth/apply-before-read.js +139 -33
  9. package/dist/auth/apply-before-read.test.node.js +241 -3
  10. package/dist/auth/index.d.ts +1 -1
  11. package/dist/auth/index.js +1 -1
  12. package/dist/auth/read-context-scope.d.ts +20 -0
  13. package/dist/auth/read-context-scope.js +48 -0
  14. package/dist/codegen/index.js +34 -3
  15. package/dist/codegen/index.test.node.js +20 -2
  16. package/dist/config/attach-hooks.d.ts +25 -0
  17. package/dist/config/attach-hooks.js +130 -0
  18. package/dist/config/attach-hooks.test.node.d.ts +1 -0
  19. package/dist/config/attach-hooks.test.node.js +173 -0
  20. package/dist/config/config-hooks.test.node.d.ts +1 -0
  21. package/dist/config/config-hooks.test.node.js +56 -0
  22. package/dist/config/config.d.ts +9 -5
  23. package/dist/config/config.js +20 -2
  24. package/dist/config/routes.d.ts +5 -5
  25. package/dist/config/routes.js +42 -9
  26. package/dist/config/routes.test.node.d.ts +1 -0
  27. package/dist/config/routes.test.node.js +152 -0
  28. package/dist/core.d.ts +2 -2
  29. package/dist/core.js +20 -14
  30. package/dist/core.test.node.d.ts +1 -0
  31. package/dist/core.test.node.js +28 -0
  32. package/dist/host/host-request-bridge.d.ts +62 -0
  33. package/dist/host/host-request-bridge.js +38 -0
  34. package/dist/index.d.ts +5 -3
  35. package/dist/index.js +5 -3
  36. package/dist/lib/errors.d.ts +13 -0
  37. package/dist/lib/errors.js +14 -0
  38. package/dist/query/parse-where.d.ts +9 -0
  39. package/dist/query/parse-where.js +146 -2
  40. package/dist/query/parse-where.test.node.js +60 -1
  41. package/dist/services/collection-bootstrap.test.node.js +30 -59
  42. package/dist/services/discover-counter-groups.test.node.js +23 -0
  43. package/dist/services/document-lifecycle/audit.d.ts +22 -1
  44. package/dist/services/document-lifecycle/audit.js +32 -1
  45. package/dist/services/document-lifecycle/create.js +13 -6
  46. package/dist/services/document-lifecycle/delete.d.ts +17 -1
  47. package/dist/services/document-lifecycle/delete.js +91 -26
  48. package/dist/services/document-lifecycle/index.d.ts +1 -1
  49. package/dist/services/document-lifecycle/internals.d.ts +0 -20
  50. package/dist/services/document-lifecycle/internals.js +22 -46
  51. package/dist/services/document-lifecycle/status.d.ts +1 -1
  52. package/dist/services/document-lifecycle/status.js +20 -3
  53. package/dist/services/document-lifecycle/system-fields.d.ts +19 -6
  54. package/dist/services/document-lifecycle/system-fields.js +112 -74
  55. package/dist/services/document-lifecycle/tree.d.ts +22 -24
  56. package/dist/services/document-lifecycle/tree.js +244 -123
  57. package/dist/services/document-lifecycle/tree.test.node.d.ts +8 -0
  58. package/dist/services/document-lifecycle/tree.test.node.js +663 -0
  59. package/dist/services/document-lifecycle/update.js +2 -1
  60. package/dist/services/document-lifecycle.test.node.js +360 -16
  61. package/dist/services/document-read.d.ts +14 -6
  62. package/dist/services/document-read.js +47 -9
  63. package/dist/services/field-upload.test.node.js +70 -0
  64. package/dist/services/index.d.ts +2 -2
  65. package/dist/services/index.js +2 -2
  66. package/dist/services/populate.d.ts +7 -3
  67. package/dist/services/populate.js +180 -28
  68. package/dist/services/populate.test.node.js +59 -0
  69. package/dist/services/richtext-embed.d.ts +39 -2
  70. package/dist/services/richtext-embed.js +4 -34
  71. package/dist/services/richtext-embed.test.node.js +15 -0
  72. package/dist/services/richtext-populate.d.ts +20 -2
  73. package/dist/services/richtext-populate.js +160 -19
  74. package/dist/services/richtext-populate.test.node.js +523 -2
  75. package/dist/utils/root-relative-redirect.d.ts +6 -0
  76. package/dist/utils/root-relative-redirect.js +37 -0
  77. package/package.json +2 -2
@@ -0,0 +1,152 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizeRootRelativeRedirect } from '../utils/root-relative-redirect.js';
3
+ import { defineClientConfig, getClientConfig } from './config.js';
4
+ import { resolveRoutes } from './routes.js';
5
+ describe('resolveRoutes', () => {
6
+ it('returns canonical defaults', () => {
7
+ const routes = resolveRoutes();
8
+ expect(routes).toEqual({ admin: '/admin', api: '/api', signIn: '/sign-in' });
9
+ expect(Object.isFrozen(routes)).toBe(true);
10
+ });
11
+ it.each([
12
+ ['cms', '/cms'],
13
+ ['/cms', '/cms'],
14
+ ['/cms/', '/cms'],
15
+ ['///cms///', '/cms'],
16
+ ['/internal/cms/', '/internal/cms'],
17
+ ])('canonicalizes an admin route of %j', (admin, expected) => {
18
+ expect(resolveRoutes({ admin }).admin).toBe(expected);
19
+ });
20
+ it('falls back for empty route values', () => {
21
+ expect(resolveRoutes({ admin: '', api: ' ', signIn: '' })).toEqual({
22
+ admin: '/admin',
23
+ api: '/api',
24
+ signIn: '/sign-in',
25
+ });
26
+ });
27
+ it.each([
28
+ '/../cms',
29
+ '/cms?next=x',
30
+ '/cms#section',
31
+ '/cms\\users',
32
+ '/%63ms',
33
+ ])('rejects a non-segment admin route of %j', (admin) => {
34
+ expect(() => resolveRoutes({ admin })).toThrow(/routes\.admin/);
35
+ });
36
+ it('normalizes and validates the API route too', () => {
37
+ expect(resolveRoutes({ api: 'content-api/' }).api).toBe('/content-api');
38
+ expect(resolveRoutes({ api: '/internal/api' }).api).toBe('/internal/api');
39
+ });
40
+ it.each([
41
+ ['staff/login', '/staff/login'],
42
+ ['/staff/login/', '/staff/login'],
43
+ ['/staff//login/', '/staff/login'],
44
+ ])('canonicalizes a sign-in route of %j', (signIn, expected) => {
45
+ expect(resolveRoutes({ signIn }).signIn).toBe(expected);
46
+ });
47
+ it.each([
48
+ 'https://evil.test/login',
49
+ '//evil.test/login',
50
+ '/staff/login?next=/cms',
51
+ '/staff/login#form',
52
+ '/staff\\login',
53
+ '/staff/%6cogin',
54
+ '/staff/../login',
55
+ ])('rejects an unsafe sign-in route of %j', (signIn) => {
56
+ expect(() => resolveRoutes({ signIn })).toThrow(/routes\.signIn/);
57
+ });
58
+ it('rejects conflicting admin and API segments', () => {
59
+ expect(() => resolveRoutes({ admin: '/internal', api: 'internal/' })).toThrow(/routes\.admin and routes\.api/);
60
+ });
61
+ it.each([
62
+ ['/internal', '/internal/api'],
63
+ ['/internal/admin', '/internal'],
64
+ ])('rejects overlapping admin and API trees %j and %j', (admin, api) => {
65
+ expect(() => resolveRoutes({ admin, api })).toThrow(/routes\.admin and routes\.api/);
66
+ });
67
+ it.each([
68
+ ['/cms', '/cms'],
69
+ ['/cms', '/cms/login'],
70
+ ['/cms', '/api/login'],
71
+ ['/internal/cms', '/internal'],
72
+ ])('rejects conflicting admin %j and sign-in %j paths', (admin, signIn) => {
73
+ expect(() => resolveRoutes({ admin, signIn })).toThrow(/routes\.signIn/);
74
+ });
75
+ });
76
+ describe('normalizeRootRelativeRedirect', () => {
77
+ it.each([
78
+ ['/cms', '/cms'],
79
+ ['/cms/account?tab=profile#name', '/cms/account?tab=profile#name'],
80
+ ['/cms/account?query=hello world', '/cms/account?query=hello%20world'],
81
+ ])('accepts and canonicalizes %j', (value, expected) => {
82
+ expect(normalizeRootRelativeRedirect(value)).toBe(expected);
83
+ });
84
+ it.each([
85
+ '',
86
+ ' /cms',
87
+ 'https://evil.test',
88
+ '//evil.test',
89
+ '/\\evil.test',
90
+ '/cms\\account',
91
+ '/cms\naccount',
92
+ '/cms\u0085account',
93
+ '/cms/../account',
94
+ '/cms/./account',
95
+ '/%2F%2Fevil.test',
96
+ '/cms/%2e%2e/account',
97
+ ])('rejects %j', (value) => {
98
+ expect(normalizeRootRelativeRedirect(value)).toBeUndefined();
99
+ });
100
+ });
101
+ describe('route configuration boundary', () => {
102
+ it('stores and exposes resolved routes independently of partial input', () => {
103
+ const input = { admin: 'internal/cms/' };
104
+ const registered = defineClientConfig({
105
+ serverURL: 'https://example.test',
106
+ routes: input,
107
+ collections: [],
108
+ admin: [],
109
+ i18n: {
110
+ interface: { defaultLocale: 'en', locales: [] },
111
+ content: { defaultLocale: 'en', locales: [] },
112
+ },
113
+ });
114
+ input.admin = '/changed';
115
+ expect(registered.routes).toEqual({
116
+ admin: '/internal/cms',
117
+ api: '/api',
118
+ signIn: '/sign-in',
119
+ });
120
+ expect(getClientConfig().routes).toBe(registered.routes);
121
+ expect(Object.isFrozen(registered.routes)).toBe(true);
122
+ });
123
+ it('prevents post-registration route mutation', () => {
124
+ const { routes } = defineClientConfig({
125
+ serverURL: 'https://example.test',
126
+ routes: { admin: '/internal/cms' },
127
+ collections: [],
128
+ admin: [],
129
+ i18n: {
130
+ interface: { defaultLocale: 'en', locales: [] },
131
+ content: { defaultLocale: 'en', locales: [] },
132
+ },
133
+ });
134
+ expect(() => {
135
+ ;
136
+ routes.admin = '/changed';
137
+ }).toThrow(TypeError);
138
+ expect(getClientConfig().routes.admin).toBe('/internal/cms');
139
+ });
140
+ it('rejects unsafe routes during registration', () => {
141
+ expect(() => defineClientConfig({
142
+ serverURL: 'https://example.test',
143
+ routes: { admin: '/cms?next=/account' },
144
+ collections: [],
145
+ admin: [],
146
+ i18n: {
147
+ interface: { defaultLocale: 'en', locales: [] },
148
+ content: { defaultLocale: 'en', locales: [] },
149
+ },
150
+ })).toThrow(/routes\.admin/);
151
+ });
152
+ });
package/dist/core.d.ts CHANGED
@@ -9,9 +9,9 @@ import { type AbilityDescriptor, AbilityRegistry, type SessionProvider } from '@
9
9
  import type { Logger as PinoLogger } from 'pino';
10
10
  import { type BylineLogger } from './lib/logger.js';
11
11
  import { type CollectionRecord } from './services/collection-bootstrap.js';
12
- import type { CollectionDefinition, IDbAdapter, IStorageProvider, ServerConfig } from './@types/index.js';
12
+ import type { CollectionDefinition, IDbAdapter, IStorageProvider, ResolvedServerConfig, ServerConfig } from './@types/index.js';
13
13
  export interface BylineCore<TAdminStore = unknown> {
14
- config: ServerConfig<TAdminStore>;
14
+ config: ResolvedServerConfig<TAdminStore>;
15
15
  collections: readonly CollectionDefinition[];
16
16
  db: IDbAdapter;
17
17
  storage: IStorageProvider | undefined;
package/dist/core.js CHANGED
@@ -7,11 +7,12 @@
7
7
  */
8
8
  import { AbilityRegistry } from '@byline/auth';
9
9
  import { registerCollectionAbilities } from './auth/register-collection-abilities.js';
10
- import { defineBylineCore, defineServerConfig, getBylineCoreUnsafe } from './config/config.js';
10
+ import { defineBylineCore, getBylineCoreUnsafe, registerServerConfig, resolveServerConfig, } from './config/config.js';
11
11
  import { createBylineLogger, defineLogger } from './lib/logger.js';
12
12
  import { Registry } from './lib/registry.js';
13
13
  import { ensureCollections } from './services/collection-bootstrap.js';
14
14
  import { discoverCounterGroups } from './services/discover-counter-groups.js';
15
+ import { validateTreeAuditCapability } from './services/document-lifecycle/audit.js';
15
16
  import { validateTranslations } from './services/i18n-validator.js';
16
17
  import { validateRichTextFieldFlags } from './services/richtext-populate.js';
17
18
  import { validateSearchConfig } from './services/validate-search-config.js';
@@ -32,11 +33,12 @@ export const initBylineCore = async (config, pinoLogger) => {
32
33
  const { pino } = await import('pino');
33
34
  pinoLogger = pino({ level: 'info' });
34
35
  }
36
+ const resolvedConfig = resolveServerConfig(config);
35
37
  const registry = new Registry()
36
- .addValue('config', config)
37
- .addValue('collections', config.collections)
38
- .addValue('db', config.db)
39
- .addValue('storage', config.storage)
38
+ .addValue('config', resolvedConfig)
39
+ .addValue('collections', resolvedConfig.collections)
40
+ .addValue('db', resolvedConfig.db)
41
+ .addValue('storage', resolvedConfig.storage)
40
42
  .addFactory('logger', createBylineLogger);
41
43
  const composed = registry.compose({ pinoLogger });
42
44
  // Validate richText field flags against the registered server adapter
@@ -44,15 +46,18 @@ export const initBylineCore = async (config, pinoLogger) => {
44
46
  // (both flags off) and missing-adapter cases at boot rather than at
45
47
  // request time.
46
48
  validateRichTextFieldFlags(composed.collections, {
47
- populate: config.fields?.richText?.populate != null,
48
- embed: config.fields?.richText?.embed != null,
49
+ populate: resolvedConfig.fields?.richText?.populate != null,
50
+ embed: resolvedConfig.fields?.richText?.embed != null,
49
51
  });
50
52
  // Validate search configuration: a collection that opts into search must
51
53
  // have a SearchProvider registered, otherwise indexing / client.search()
52
54
  // would silently no-op. Fail-fast at boot, same posture as richText above.
53
55
  validateSearchConfig(composed.collections, {
54
- provider: config.search != null,
56
+ provider: resolvedConfig.search != null,
55
57
  });
58
+ // Tree edges are unversioned metadata and may only run on adapters that can
59
+ // lock, mutate, and append audit rows in one transaction.
60
+ validateTreeAuditCapability(composed.collections, composed.db);
56
61
  // Validate the admin i18n translation registry against the configured
57
62
  // interface locale set. Throws on structural errors (missing bundle for
58
63
  // a declared locale, defaultLocale outside the permitted set, …); soft
@@ -62,9 +67,9 @@ export const initBylineCore = async (config, pinoLogger) => {
62
67
  // validator takes the assembled shape so locale config and bundle
63
68
  // travel together.
64
69
  const i18nValidation = validateTranslations({
65
- defaultLocale: config.i18n.interface.defaultLocale,
66
- locales: config.i18n.interface.locales,
67
- translations: config.i18n.translations,
70
+ defaultLocale: resolvedConfig.i18n.interface.defaultLocale,
71
+ locales: resolvedConfig.i18n.interface.locales,
72
+ translations: resolvedConfig.i18n.translations,
68
73
  });
69
74
  for (const warning of i18nValidation.warnings) {
70
75
  composed.logger.warn({
@@ -73,9 +78,6 @@ export const initBylineCore = async (config, pinoLogger) => {
73
78
  missingKeys: warning.missingKeys,
74
79
  }, `[i18n] '${warning.locale}.${warning.namespace}' is missing ${warning.missingKeys.length} key(s) relative to the other locales`);
75
80
  }
76
- // Backward compat: populate globalThis singletons
77
- defineServerConfig(config);
78
- defineLogger(composed.logger);
79
81
  // Reconcile collection definitions with the database: insert new rows,
80
82
  // bump schema versions when the fingerprint has drifted, and build the
81
83
  // in-memory record cache used by the lifecycle/upload/client paths.
@@ -140,6 +142,10 @@ export const initBylineCore = async (config, pinoLogger) => {
140
142
  sessionProvider: composed.config.sessionProvider,
141
143
  adminStore: composed.config.adminStore,
142
144
  };
145
+ // Commit globals only after the replacement core has fully initialized. A
146
+ // failed reinitialization leaves the prior config, logger, and core intact.
147
+ registerServerConfig(resolvedConfig);
148
+ defineLogger(composed.logger);
143
149
  // Register on the global singleton so server-side packages
144
150
  // (`@byline/host-tanstack-start/server-fns/*`, future hosts) can read
145
151
  // post-init state via `getBylineCore()` instead of importing the
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { defineServerConfig, getServerConfig } from './config/config.js';
3
+ import { initBylineCore } from './core.js';
4
+ function serverConfig(admin) {
5
+ return {
6
+ serverURL: 'https://example.test',
7
+ routes: { admin },
8
+ collections: [],
9
+ db: {},
10
+ i18n: {
11
+ interface: { defaultLocale: 'en', locales: [] },
12
+ content: { defaultLocale: 'en', locales: [] },
13
+ },
14
+ };
15
+ }
16
+ describe('initBylineCore configuration registration', () => {
17
+ it('does not overwrite a valid singleton when synchronous validation fails', async () => {
18
+ const valid = defineServerConfig(serverConfig('/stable/admin'));
19
+ const invalid = serverConfig('/replacement/admin');
20
+ invalid.i18n = {
21
+ interface: { defaultLocale: 'en', locales: ['en'] },
22
+ content: { defaultLocale: 'en', locales: [] },
23
+ };
24
+ await expect(initBylineCore(invalid, {})).rejects.toThrow(/translations bundle/i);
25
+ expect(getServerConfig()).toBe(valid);
26
+ expect(getServerConfig().routes.admin).toBe('/stable/admin');
27
+ });
28
+ });
@@ -0,0 +1,62 @@
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
+ * The host-framework request bridge — the seam that makes the server-side
10
+ * client stack (`@byline/client/server`) host-agnostic.
11
+ *
12
+ * Everything request-scoped in that stack bottoms out in three
13
+ * primitives: "which request am I in" (identity for per-request
14
+ * memoization), "read a cookie", and "write a cookie" (admin session
15
+ * refresh rotates tokens). A host adapter implements those three over its
16
+ * framework's runtime and registers the bridge once at server boot;
17
+ * `@byline/client/server` is written against the interface and never
18
+ * imports a framework.
19
+ *
20
+ * Registration follows the same pattern as the config singletons in
21
+ * `config/config.ts`: a `Symbol.for` slot on `globalThis`, so every copy
22
+ * of this module (Vite SSR can resolve workspace-linked packages through
23
+ * different module graphs) shares the same state. Server-only by nature —
24
+ * hosts register from their boot path (side-effect imports guarantee
25
+ * registration before any request is handled), and nothing in a browser
26
+ * graph should ever reach for it.
27
+ */
28
+ export interface HostCookieSetOptions {
29
+ httpOnly?: boolean;
30
+ sameSite?: 'lax' | 'strict' | 'none';
31
+ secure?: boolean;
32
+ path?: string;
33
+ maxAge?: number;
34
+ }
35
+ export interface HostRequestBridge {
36
+ /**
37
+ * A stable identity object for the current HTTP request, or `undefined`
38
+ * when running outside a request (seed scripts, background jobs, unit
39
+ * tests). Used purely as a WeakMap key for per-request memoization —
40
+ * never inspected.
41
+ */
42
+ getRequest(): object | undefined;
43
+ /** Read a request cookie. Returns `undefined` when not present. */
44
+ getCookie(name: string): string | undefined;
45
+ /** Write a response cookie (admin session refresh, preview toggles). */
46
+ setCookie(name: string, value: string, options?: HostCookieSetOptions): void;
47
+ }
48
+ /**
49
+ * Register the host adapter's bridge. Idempotent and last-write-wins —
50
+ * host adapters register from side-effect imports, which may evaluate
51
+ * more than once across module graphs.
52
+ */
53
+ export declare function registerHostRequestBridge(bridge: HostRequestBridge): void;
54
+ /** The registered bridge, or `undefined` when no host has registered one. */
55
+ export declare function tryGetHostRequestBridge(): HostRequestBridge | undefined;
56
+ /**
57
+ * The registered bridge, throwing with setup guidance when absent. Cookie
58
+ * reads/writes require a host; scripts and tests that have no request
59
+ * should pass an explicit `requestContext` to `createBylineClient`
60
+ * instead of using the request-bound getters.
61
+ */
62
+ export declare function getHostRequestBridge(): HostRequestBridge;
@@ -0,0 +1,38 @@
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
+ const BYLINE_HOST_REQUEST_BRIDGE = Symbol.for('__byline_host_request_bridge__');
9
+ /**
10
+ * Register the host adapter's bridge. Idempotent and last-write-wins —
11
+ * host adapters register from side-effect imports, which may evaluate
12
+ * more than once across module graphs.
13
+ */
14
+ export function registerHostRequestBridge(bridge) {
15
+ ;
16
+ globalThis[BYLINE_HOST_REQUEST_BRIDGE] = bridge;
17
+ }
18
+ /** The registered bridge, or `undefined` when no host has registered one. */
19
+ export function tryGetHostRequestBridge() {
20
+ return globalThis[BYLINE_HOST_REQUEST_BRIDGE] ?? undefined;
21
+ }
22
+ /**
23
+ * The registered bridge, throwing with setup guidance when absent. Cookie
24
+ * reads/writes require a host; scripts and tests that have no request
25
+ * should pass an explicit `requestContext` to `createBylineClient`
26
+ * instead of using the request-bound getters.
27
+ */
28
+ export function getHostRequestBridge() {
29
+ const bridge = tryGetHostRequestBridge();
30
+ if (!bridge) {
31
+ throw new Error('No HostRequestBridge registered. A host adapter (e.g. ' +
32
+ '@byline/host-tanstack-start) must call registerHostRequestBridge() ' +
33
+ 'at server boot before request-bound client getters can resolve ' +
34
+ 'cookies. Scripts and tests should pass an explicit requestContext ' +
35
+ 'to createBylineClient instead.');
36
+ }
37
+ return bridge;
38
+ }
package/dist/index.d.ts CHANGED
@@ -1,20 +1,22 @@
1
1
  export * from './@types/index.js';
2
- export { applyBeforeRead, assertActorCanPerform, COLLECTION_ABILITY_VERBS, type CollectionAbilityVerb, collectionAbilityKey, registerCollectionAbilities, } from './auth/index.js';
2
+ export { applyBeforeRead, assertActorCanPerform, bindReadContextAuthority, COLLECTION_ABILITY_VERBS, type CollectionAbilityVerb, collectionAbilityKey, compileBeforeReadFilters, registerCollectionAbilities, } from './auth/index.js';
3
3
  export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, resolveItemViewColumns, } from './config/config.js';
4
4
  export { resolveRoutes } from './config/routes.js';
5
5
  export { validateAdminConfigs } from './config/validate-admin-configs.js';
6
6
  export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
7
7
  export { type BylineCore, getBylineCore, initBylineCore } from './core.js';
8
8
  export * from './defaults/default-values.js';
9
- export { BylineError, ERR_AUDIT_UNSUPPORTED, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_STORAGE, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, type ErrorReport, } from './lib/errors.js';
9
+ export { getHostRequestBridge, type HostCookieSetOptions, type HostRequestBridge, registerHostRequestBridge, tryGetHostRequestBridge, } from './host/host-request-bridge.js';
10
+ export { BylineError, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, type ErrorReport, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
10
11
  export { generateKeyBetween, generateNKeysBetween, validateOrderKey, } from './lib/fractional-index.js';
11
12
  export { type BylineLogger, getLogger } from './lib/logger.js';
12
13
  export { AsyncRegistry, type RegisteredServices, Registry } from './lib/registry.js';
13
14
  export * from './patches/index.js';
14
- export { mergePredicates, type ParseContext, type ParsedSort, type ParsedWhere, parseSort, parseWhere, } from './query/parse-where.js';
15
+ export { mergePredicates, type ParseContext, type ParsedSort, type ParsedWhere, parsePredicateFilters, parseSort, parseWhere, } from './query/parse-where.js';
15
16
  export { getCollectionSchemasForPath } from './schemas/zod/cache.js';
16
17
  export * from './services/index.js';
17
18
  export * from './storage/index.js';
19
+ export { normalizeRootRelativeRedirect } from './utils/root-relative-redirect.js';
18
20
  export { formatTextValue, looksLikeISODate, type SlugifierFn, type SlugifyContext, slugify, } from './utils/slugify.js';
19
21
  export { getUploadFields, hasUploadField, isUploadField } from './utils/storage-utils.js';
20
22
  export * from './workflow/index.js';
package/dist/index.js CHANGED
@@ -15,22 +15,24 @@
15
15
  // through this main entry or `@byline/client`.
16
16
  // ---------------------------------------------------------------------------
17
17
  export * from './@types/index.js';
18
- export { applyBeforeRead, assertActorCanPerform, COLLECTION_ABILITY_VERBS, collectionAbilityKey, registerCollectionAbilities, } from './auth/index.js';
18
+ export { applyBeforeRead, assertActorCanPerform, bindReadContextAuthority, COLLECTION_ABILITY_VERBS, collectionAbilityKey, compileBeforeReadFilters, registerCollectionAbilities, } from './auth/index.js';
19
19
  export { defineClientConfig, defineServerConfig, getClientConfig, getCollectionAdminConfig, getCollectionDefinition, getServerConfig, orderByContentLocale, resolveItemViewColumns, } from './config/config.js';
20
20
  export { resolveRoutes } from './config/routes.js';
21
21
  export { validateAdminConfigs } from './config/validate-admin-configs.js';
22
22
  export { RESERVED_FIELD_NAMES } from './config/validate-collections.js';
23
23
  export { getBylineCore, initBylineCore } from './core.js';
24
24
  export * from './defaults/default-values.js';
25
- export { BylineError, ERR_AUDIT_UNSUPPORTED, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_STORAGE, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, } from './lib/errors.js';
25
+ export { getHostRequestBridge, registerHostRequestBridge, tryGetHostRequestBridge, } from './host/host-request-bridge.js';
26
+ export { BylineError, ERR_AUDIT_UNSUPPORTED, ERR_CONFLICT, ERR_DATABASE, ERR_NOT_FOUND, ERR_READ_BUDGET_EXCEEDED, ERR_READ_RECURSION, ERR_STORAGE, ERR_TREE_HOOK_COMMITTED, ERR_UNHANDLED, ERR_VALIDATION, ErrorCodes, TREE_HOOK_COMMITTED_MARKER, TREE_PLACEMENT_STALE_MARKER, } from './lib/errors.js';
26
27
  export { generateKeyBetween, generateNKeysBetween, validateOrderKey, } from './lib/fractional-index.js';
27
28
  export { getLogger } from './lib/logger.js';
28
29
  export { AsyncRegistry, Registry } from './lib/registry.js';
29
30
  export * from './patches/index.js';
30
- export { mergePredicates, parseSort, parseWhere, } from './query/parse-where.js';
31
+ export { mergePredicates, parsePredicateFilters, parseSort, parseWhere, } from './query/parse-where.js';
31
32
  export { getCollectionSchemasForPath } from './schemas/zod/cache.js';
32
33
  export * from './services/index.js';
33
34
  export * from './storage/index.js';
35
+ export { normalizeRootRelativeRedirect } from './utils/root-relative-redirect.js';
34
36
  export { formatTextValue, looksLikeISODate, slugify, } from './utils/slugify.js';
35
37
  export { getUploadFields, hasUploadField, isUploadField } from './utils/storage-utils.js';
36
38
  export * from './workflow/index.js';
@@ -85,9 +85,15 @@ export declare const ErrorCodes: {
85
85
  readonly DATABASE: 'ERR_DATABASE';
86
86
  readonly STORAGE: 'ERR_STORAGE';
87
87
  readonly READ_BUDGET_EXCEEDED: 'ERR_READ_BUDGET_EXCEEDED';
88
+ readonly READ_RECURSION: 'ERR_READ_RECURSION';
88
89
  readonly PATH_CONFLICT: 'ERR_PATH_CONFLICT';
89
90
  readonly AUDIT_UNSUPPORTED: 'ERR_AUDIT_UNSUPPORTED';
91
+ readonly TREE_HOOK_COMMITTED: 'ERR_TREE_HOOK_COMMITTED';
90
92
  };
93
+ /** Stable message fallback for stale tree-placement `ERR_CONFLICT` errors. */
94
+ export declare const TREE_PLACEMENT_STALE_MARKER = "[ERR_CONFLICT:TREE_PLACEMENT_STALE]";
95
+ /** Stable message fallback for `ERR_TREE_HOOK_COMMITTED` errors. */
96
+ export declare const TREE_HOOK_COMMITTED_MARKER = "[ERR_TREE_HOOK_COMMITTED]";
91
97
  export declare const ERR_UNHANDLED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
92
98
  export declare const ERR_NOT_FOUND: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
93
99
  export declare const ERR_CONFLICT: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
@@ -97,6 +103,7 @@ export declare const ERR_PATCH_FAILED: (opts: BylineErrorOptions, errorConstruct
97
103
  export declare const ERR_DATABASE: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
98
104
  export declare const ERR_STORAGE: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
99
105
  export declare const ERR_READ_BUDGET_EXCEEDED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
106
+ export declare const ERR_READ_RECURSION: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
100
107
  /**
101
108
  * Thrown when a write attempts to set `path` to a value already used by
102
109
  * another document in the same `(collection, locale)` scope. Surfaces the
@@ -114,3 +121,9 @@ export declare const ERR_PATH_CONFLICT: (opts: BylineErrorOptions, errorConstruc
114
121
  * docs/03-architecture/03-transactions.md and docs/06-auth-and-security/02-auditability.md.
115
122
  */
116
123
  export declare const ERR_AUDIT_UNSUPPORTED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
124
+ /**
125
+ * A tree mutation and its audit row committed, but its post-commit
126
+ * `afterTreeChange` work failed. Callers still receive a rejection so they can
127
+ * reconcile, while transports can distinguish it from a rolled-back mutation.
128
+ */
129
+ export declare const ERR_TREE_HOOK_COMMITTED: (opts: BylineErrorOptions, errorConstructor?: any) => BylineError;
@@ -53,6 +53,7 @@ export class BylineError extends Error {
53
53
  super(message, { cause });
54
54
  }
55
55
  this.code = code;
56
+ this.name = 'BylineError';
56
57
  this.details = details;
57
58
  this.logExtra = logExtra;
58
59
  this.logLevel = logLevel;
@@ -118,9 +119,15 @@ export const ErrorCodes = {
118
119
  DATABASE: 'ERR_DATABASE',
119
120
  STORAGE: 'ERR_STORAGE',
120
121
  READ_BUDGET_EXCEEDED: 'ERR_READ_BUDGET_EXCEEDED',
122
+ READ_RECURSION: 'ERR_READ_RECURSION',
121
123
  PATH_CONFLICT: 'ERR_PATH_CONFLICT',
122
124
  AUDIT_UNSUPPORTED: 'ERR_AUDIT_UNSUPPORTED',
125
+ TREE_HOOK_COMMITTED: 'ERR_TREE_HOOK_COMMITTED',
123
126
  };
127
+ /** Stable message fallback for stale tree-placement `ERR_CONFLICT` errors. */
128
+ export const TREE_PLACEMENT_STALE_MARKER = '[ERR_CONFLICT:TREE_PLACEMENT_STALE]';
129
+ /** Stable message fallback for `ERR_TREE_HOOK_COMMITTED` errors. */
130
+ export const TREE_HOOK_COMMITTED_MARKER = '[ERR_TREE_HOOK_COMMITTED]';
124
131
  // ---------------------------------------------------------------------------
125
132
  // Pre-instantiated factories
126
133
  // ---------------------------------------------------------------------------
@@ -133,6 +140,7 @@ export const ERR_PATCH_FAILED = createErrorType(ErrorCodes.PATCH_FAILED);
133
140
  export const ERR_DATABASE = createErrorType(ErrorCodes.DATABASE);
134
141
  export const ERR_STORAGE = createErrorType(ErrorCodes.STORAGE);
135
142
  export const ERR_READ_BUDGET_EXCEEDED = createErrorType(ErrorCodes.READ_BUDGET_EXCEEDED);
143
+ export const ERR_READ_RECURSION = createErrorType(ErrorCodes.READ_RECURSION);
136
144
  /**
137
145
  * Thrown when a write attempts to set `path` to a value already used by
138
146
  * another document in the same `(collection, locale)` scope. Surfaces the
@@ -150,3 +158,9 @@ export const ERR_PATH_CONFLICT = createErrorType(ErrorCodes.PATH_CONFLICT, 'warn
150
158
  * docs/03-architecture/03-transactions.md and docs/06-auth-and-security/02-auditability.md.
151
159
  */
152
160
  export const ERR_AUDIT_UNSUPPORTED = createErrorType(ErrorCodes.AUDIT_UNSUPPORTED);
161
+ /**
162
+ * A tree mutation and its audit row committed, but its post-commit
163
+ * `afterTreeChange` work failed. Callers still receive a rejection so they can
164
+ * reconcile, while transports can distinguish it from a rolled-back mutation.
165
+ */
166
+ export const ERR_TREE_HOOK_COMMITTED = createErrorType(ErrorCodes.TREE_HOOK_COMMITTED, 'warn');
@@ -87,6 +87,15 @@ declare const DOCUMENT_SORT_COLUMNS: Record<string, string>;
87
87
  * sensible composition through a relation hop.
88
88
  */
89
89
  export declare function parseWhere(where: WhereClause | undefined, definition: CollectionDefinition, ctx?: ParseContext): Promise<ParsedWhere>;
90
+ /**
91
+ * Compile a predicate wholly to adapter `DocumentFilter`s. Unlike
92
+ * `parseWhere`, top-level `status` and `path` are emitted as document-column
93
+ * filters rather than scalar list-query options. This is the form used by
94
+ * `beforeRead` on detail, populate, count, and tree reads.
95
+ */
96
+ export declare function parsePredicateFilters(where: QueryPredicate | undefined, definition: CollectionDefinition, ctx?: ParseContext, options?: {
97
+ strict?: boolean;
98
+ }): Promise<DocumentFilter[]>;
90
99
  /**
91
100
  * Combine a `beforeRead` hook predicate with a caller-supplied where
92
101
  * clause using implicit AND. Returns whichever side is non-empty, or