@forge-cms/runtime 0.5.0 → 0.6.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 (63) hide show
  1. package/dist/auth-managed.d.ts +19 -0
  2. package/dist/auth-managed.d.ts.map +1 -0
  3. package/dist/auth-managed.js +24 -0
  4. package/dist/auth-managed.js.map +1 -0
  5. package/dist/concurrency.d.ts +8 -0
  6. package/dist/concurrency.d.ts.map +1 -0
  7. package/dist/concurrency.js +15 -0
  8. package/dist/concurrency.js.map +1 -0
  9. package/dist/context.d.ts +5 -0
  10. package/dist/context.d.ts.map +1 -1
  11. package/dist/errors.d.ts +29 -1
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +38 -0
  14. package/dist/errors.js.map +1 -1
  15. package/dist/files.d.ts +15 -1
  16. package/dist/files.d.ts.map +1 -1
  17. package/dist/files.js +55 -14
  18. package/dist/files.js.map +1 -1
  19. package/dist/globals.d.ts +21 -3
  20. package/dist/globals.d.ts.map +1 -1
  21. package/dist/globals.js +216 -28
  22. package/dist/globals.js.map +1 -1
  23. package/dist/handlers.d.ts +3 -2
  24. package/dist/handlers.d.ts.map +1 -1
  25. package/dist/handlers.js +36 -47
  26. package/dist/handlers.js.map +1 -1
  27. package/dist/index.d.ts +3 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/localization.d.ts +17 -0
  32. package/dist/localization.d.ts.map +1 -1
  33. package/dist/localization.js +56 -0
  34. package/dist/localization.js.map +1 -1
  35. package/dist/operations.d.ts +51 -12
  36. package/dist/operations.d.ts.map +1 -1
  37. package/dist/operations.js +728 -125
  38. package/dist/operations.js.map +1 -1
  39. package/dist/relation-integrity.d.ts +31 -13
  40. package/dist/relation-integrity.d.ts.map +1 -1
  41. package/dist/relation-integrity.js +62 -6
  42. package/dist/relation-integrity.js.map +1 -1
  43. package/dist/relation-lifecycle.d.ts +95 -0
  44. package/dist/relation-lifecycle.d.ts.map +1 -0
  45. package/dist/relation-lifecycle.js +424 -0
  46. package/dist/relation-lifecycle.js.map +1 -0
  47. package/dist/runtime.d.ts +21 -0
  48. package/dist/runtime.d.ts.map +1 -1
  49. package/dist/runtime.js +112 -20
  50. package/dist/runtime.js.map +1 -1
  51. package/dist/storage-intents.d.ts +96 -0
  52. package/dist/storage-intents.d.ts.map +1 -0
  53. package/dist/storage-intents.js +217 -0
  54. package/dist/storage-intents.js.map +1 -0
  55. package/dist/system-fields.d.ts +27 -0
  56. package/dist/system-fields.d.ts.map +1 -0
  57. package/dist/system-fields.js +74 -0
  58. package/dist/system-fields.js.map +1 -0
  59. package/dist/versions.d.ts +73 -1
  60. package/dist/versions.d.ts.map +1 -1
  61. package/dist/versions.js +204 -17
  62. package/dist/versions.js.map +1 -1
  63. package/package.json +9 -7
@@ -0,0 +1,19 @@
1
+ import type { OperationContext } from './context.js';
2
+ /**
3
+ * Is `slug` a collection whose lifecycle the configured `AuthAdapter` owns (spec 061)? Asked of the
4
+ * adapter through its optional `managesCollection` — the runtime never inspects a concrete adapter or a
5
+ * slug convention, so an adapter that omits the method (or a composite of such adapters) restricts
6
+ * nothing.
7
+ */
8
+ export declare function isAuthManagedCollection(ctx: OperationContext, slug: string): boolean;
9
+ /**
10
+ * The generic-mutation boundary. Called first thing by `create`/`update`/`deleteDocumentInternal`/
11
+ * `restoreVersion` — before hooks, access checks and any read or write — so the answer is the same for
12
+ * every caller (`overrideAccess: true` bypasses *authorization*, never the auth subsystem's
13
+ * data-integrity invariants) and for a document that does not exist, and a refusal has no side effects.
14
+ * The message deliberately does not distinguish create/update/delete — the boundary is the same for all
15
+ * three, and the caller's fix (use the auth adapter's own operations) does not depend on which one this
16
+ * was.
17
+ */
18
+ export declare function assertNotAuthManaged(ctx: OperationContext, slug: string): void;
19
+ //# sourceMappingURL=auth-managed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-managed.d.ts","sourceRoot":"","sources":["../src/auth-managed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAGrD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAEpF;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9E"}
@@ -0,0 +1,24 @@
1
+ import { AuthManagedCollectionError } from './errors.js';
2
+ /**
3
+ * Is `slug` a collection whose lifecycle the configured `AuthAdapter` owns (spec 061)? Asked of the
4
+ * adapter through its optional `managesCollection` — the runtime never inspects a concrete adapter or a
5
+ * slug convention, so an adapter that omits the method (or a composite of such adapters) restricts
6
+ * nothing.
7
+ */
8
+ export function isAuthManagedCollection(ctx, slug) {
9
+ return ctx.adapters.auth.managesCollection?.(slug) === true;
10
+ }
11
+ /**
12
+ * The generic-mutation boundary. Called first thing by `create`/`update`/`deleteDocumentInternal`/
13
+ * `restoreVersion` — before hooks, access checks and any read or write — so the answer is the same for
14
+ * every caller (`overrideAccess: true` bypasses *authorization*, never the auth subsystem's
15
+ * data-integrity invariants) and for a document that does not exist, and a refusal has no side effects.
16
+ * The message deliberately does not distinguish create/update/delete — the boundary is the same for all
17
+ * three, and the caller's fix (use the auth adapter's own operations) does not depend on which one this
18
+ * was.
19
+ */
20
+ export function assertNotAuthManaged(ctx, slug) {
21
+ if (isAuthManagedCollection(ctx, slug))
22
+ throw new AuthManagedCollectionError(slug);
23
+ }
24
+ //# sourceMappingURL=auth-managed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-managed.js","sourceRoot":"","sources":["../src/auth-managed.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,GAAqB,EAAE,IAAY;IACzE,OAAO,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC;AAC9D,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAqB,EAAE,IAAY;IACtE,IAAI,uBAAuB,CAAC,GAAG,EAAE,IAAI,CAAC;QAAE,MAAM,IAAI,0BAA0B,CAAC,IAAI,CAAC,CAAC;AACrF,CAAC"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A compare-and-set on `updated_at` is only as fine as the millisecond the adapters stamp. Before a CAS
3
+ * write, wait until the clock has passed the stamp that was read, so this write's own stamp is strictly
4
+ * later: a concurrent CAS writer that read the same row then cannot match it (spec 066 review). Bounded:
5
+ * at most a few milliseconds. Clock skew *between* processes is outside what this can see.
6
+ */
7
+ export declare function afterStamp(stamp: unknown): Promise<void>;
8
+ //# sourceMappingURL=concurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.d.ts","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAM9D"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * A compare-and-set on `updated_at` is only as fine as the millisecond the adapters stamp. Before a CAS
3
+ * write, wait until the clock has passed the stamp that was read, so this write's own stamp is strictly
4
+ * later: a concurrent CAS writer that read the same row then cannot match it (spec 066 review). Bounded:
5
+ * at most a few milliseconds. Clock skew *between* processes is outside what this can see.
6
+ */
7
+ export async function afterStamp(stamp) {
8
+ const seen = typeof stamp === 'string' ? Date.parse(stamp) : NaN;
9
+ if (Number.isNaN(seen))
10
+ return;
11
+ for (let i = 0; i < 20 && Date.now() <= seen; i++) {
12
+ await new Promise((resolve) => setTimeout(resolve, 1));
13
+ }
14
+ }
15
+ //# sourceMappingURL=concurrency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,KAAc;IAC7C,MAAM,IAAI,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IACjE,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO;IAC/B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC;QAClD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IACzD,CAAC;AACH,CAAC"}
package/dist/context.d.ts CHANGED
@@ -12,5 +12,10 @@ export interface OperationContext {
12
12
  getCollection(slug: string): CollectionDefinition | undefined;
13
13
  getGlobal(slug: string): GlobalDefinition | undefined;
14
14
  getCollections(): readonly CollectionDefinition[];
15
+ /**
16
+ * Every registered global. Optional so a structural implementation written before spec 064 still
17
+ * type-checks; without it, relation integrity cannot see references held by globals.
18
+ */
19
+ getGlobals?(): readonly GlobalDefinition[];
15
20
  }
16
21
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAAC;IAC9D,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACtD,cAAc,IAAI,SAAS,oBAAoB,EAAE,CAAC;CACnD"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC;IAC9B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAAC;IAC9D,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAAC;IACtD,cAAc,IAAI,SAAS,oBAAoB,EAAE,CAAC;IAClD;;;OAGG;IACH,UAAU,CAAC,IAAI,SAAS,gBAAgB,EAAE,CAAC;CAC5C"}
package/dist/errors.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ValidationError } from '@forge-cms/core';
2
- export type ForgeErrorCode = 'NOT_FOUND' | 'INVALID_INPUT' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'UNKNOWN_FIELD' | 'INVALID_QUERY' | 'UNIQUE_CONSTRAINT' | 'INTERNAL_ERROR';
2
+ export type ForgeErrorCode = 'NOT_FOUND' | 'INVALID_INPUT' | 'VALIDATION_ERROR' | 'UNAUTHORIZED' | 'FORBIDDEN' | 'UNKNOWN_FIELD' | 'INVALID_QUERY' | 'UNIQUE_CONSTRAINT' | 'AUTH_MANAGED_COLLECTION' | 'CONCURRENT_MODIFICATION' | 'INTERNAL_ERROR';
3
3
  export declare class ForgeError extends Error {
4
4
  readonly status: number;
5
5
  readonly code: ForgeErrorCode;
@@ -47,6 +47,34 @@ export declare class UniqueConstraintError extends ForgeError {
47
47
  readonly fields: string[];
48
48
  constructor(collection: string, fields: string[]);
49
49
  }
50
+ /**
51
+ * A generic `create`/`update`/`delete` targeted a collection whose lifecycle the configured
52
+ * `AuthAdapter` owns (`AuthAdapter.managesCollection`, spec 061). Those mutations would bypass the
53
+ * adapter's invariants — first-admin provisioning, last-admin protection, password hashing, email
54
+ * normalisation, session versioning — so they are refused for every caller, trusted (`overrideAccess:
55
+ * true`) or not. Deliberately says nothing about which adapter or how; the way out is the auth
56
+ * adapter's own user-management operations.
57
+ */
58
+ export declare class AuthManagedCollectionError extends ForgeError {
59
+ readonly collection: string;
60
+ constructor(collection: string);
61
+ }
62
+ /**
63
+ * A versioned document mutation lost a race (spec 062): another write committed a new version of the
64
+ * same document after this one read it, so this one's document write and snapshot were rolled back
65
+ * together — nothing was written. Forge never retries it automatically (`before*` hooks may already
66
+ * have run); re-read the document and submit again. Deliberately names the public collection slug,
67
+ * never the internal `_versions_*` table whose unique index detected the conflict.
68
+ *
69
+ * Spec 064 reuses it for relation lifecycle conflicts — a referenced document deleted while a write was
70
+ * in progress, or a relation delete whose graph changed before it committed — with a `message` saying
71
+ * so. The meaning is the same: another request changed what this one depended on; nothing was written.
72
+ */
73
+ export declare class ConcurrentModificationError extends ForgeError {
74
+ readonly collection: string;
75
+ readonly id: string;
76
+ constructor(collection: string, id: string, message?: string);
77
+ }
50
78
  export declare function isForgeError(err: unknown): err is ForgeError;
51
79
  export interface ForgeApiErrorBody {
52
80
  error: {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,eAAe,GACf,kBAAkB,GAClB,cAAc,GACd,WAAW,GACX,eAAe,GACf,eAAe,GACf,mBAAmB,GACnB,gBAAgB,CAAC;AAErB,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAElB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc;CAMlE;AAED,qBAAa,aAAc,SAAQ,UAAU;gBAC/B,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,qBAAsB,SAAQ,UAAU;IACnD,QAAQ,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC;gBAExB,OAAO,EAAE,eAAe,EAAE;CAIvC;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,SAAiB;CAGrC;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,SAAc;CAGlC;AAED;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,UAAU;gBAC3B,OAAO,SAAgC;CAGpD;AAED;;;;;GAKG;AACH,qBAAa,qBAAsB,SAAQ,UAAU;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;gBAEd,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;CAWjD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,UAAU,CAE5D;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE;QACL,IAAI,EAAE,cAAc,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,iBAAiB,CAyBjE"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,eAAe,GACf,kBAAkB,GAClB,cAAc,GACd,WAAW,GACX,eAAe,GACf,eAAe,GACf,mBAAmB,GACnB,yBAAyB,GACzB,yBAAyB,GACzB,gBAAgB,CAAC;AAErB,qBAAa,UAAW,SAAQ,KAAK;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;gBAElB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc;CAMlE;AAED,qBAAa,aAAc,SAAQ,UAAU;gBAC/B,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,EAAE,MAAM;CAG5B;AAED,qBAAa,qBAAsB,SAAQ,UAAU;IACnD,QAAQ,CAAC,OAAO,EAAE,eAAe,EAAE,CAAC;gBAExB,OAAO,EAAE,eAAe,EAAE;CAIvC;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,SAAiB;CAGrC;AAED,qBAAa,iBAAkB,SAAQ,UAAU;gBACnC,OAAO,SAAc;CAGlC;AAED;;;;;GAKG;AACH,qBAAa,SAAU,SAAQ,UAAU;gBAC3B,OAAO,SAAgC;CAGpD;AAED;;;;;GAKG;AACH,qBAAa,qBAAsB,SAAQ,UAAU;IACnD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;gBAEd,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;CAWjD;AAED;;;;;;;GAOG;AACH,qBAAa,0BAA2B,SAAQ,UAAU;IACxD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,UAAU,EAAE,MAAM;CAS/B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,2BAA4B,SAAQ,UAAU;IACzD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;gBAER,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM;CAW7D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,UAAU,CAE5D;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE;QACL,IAAI,EAAE,cAAc,CAAC;QACrB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,iBAAiB,CAyBjE"}
package/dist/errors.js CHANGED
@@ -73,6 +73,44 @@ export class UniqueConstraintError extends ForgeError {
73
73
  this.fields = fields;
74
74
  }
75
75
  }
76
+ /**
77
+ * A generic `create`/`update`/`delete` targeted a collection whose lifecycle the configured
78
+ * `AuthAdapter` owns (`AuthAdapter.managesCollection`, spec 061). Those mutations would bypass the
79
+ * adapter's invariants — first-admin provisioning, last-admin protection, password hashing, email
80
+ * normalisation, session versioning — so they are refused for every caller, trusted (`overrideAccess:
81
+ * true`) or not. Deliberately says nothing about which adapter or how; the way out is the auth
82
+ * adapter's own user-management operations.
83
+ */
84
+ export class AuthManagedCollectionError extends ForgeError {
85
+ collection;
86
+ constructor(collection) {
87
+ super(`Collection '${collection}' is managed by the configured auth adapter and cannot be mutated ` +
88
+ `through generic collection CRUD. Use the auth adapter's user-management operations instead.`, 403, 'AUTH_MANAGED_COLLECTION');
89
+ this.collection = collection;
90
+ }
91
+ }
92
+ /**
93
+ * A versioned document mutation lost a race (spec 062): another write committed a new version of the
94
+ * same document after this one read it, so this one's document write and snapshot were rolled back
95
+ * together — nothing was written. Forge never retries it automatically (`before*` hooks may already
96
+ * have run); re-read the document and submit again. Deliberately names the public collection slug,
97
+ * never the internal `_versions_*` table whose unique index detected the conflict.
98
+ *
99
+ * Spec 064 reuses it for relation lifecycle conflicts — a referenced document deleted while a write was
100
+ * in progress, or a relation delete whose graph changed before it committed — with a `message` saying
101
+ * so. The meaning is the same: another request changed what this one depended on; nothing was written.
102
+ */
103
+ export class ConcurrentModificationError extends ForgeError {
104
+ collection;
105
+ id;
106
+ constructor(collection, id, message) {
107
+ super(message ??
108
+ `Document '${id}' in '${collection}' was changed by another request while this one was in ` +
109
+ `progress; nothing was written. Reload it and try again.`, 409, 'CONCURRENT_MODIFICATION');
110
+ this.collection = collection;
111
+ this.id = id;
112
+ }
113
+ }
76
114
  export function isForgeError(err) {
77
115
  return err instanceof ForgeError;
78
116
  }
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
package/dist/files.d.ts CHANGED
@@ -2,7 +2,11 @@ import type { ApiContext } from '@forge-cms/api';
2
2
  import type { AnyForgeCmsRuntime } from './runtime.js';
3
3
  export interface FileHandlerOptions<TEnv = unknown> {
4
4
  runtime: AnyForgeCmsRuntime<TEnv>;
5
- /** `cache-control` for a hit. Defaults to a conservative minute. */
5
+ /**
6
+ * `cache-control` for a hit served to an **anonymous** caller. Defaults to a conservative minute. A
7
+ * hit for an authenticated caller is always `private, no-store` (spec 067): shared caches must not
8
+ * hand a file one user may read to another who may not.
9
+ */
6
10
  cacheControl?: string;
7
11
  }
8
12
  /**
@@ -19,6 +23,16 @@ export interface FileHandlerOptions<TEnv = unknown> {
19
23
  * handleFile({ request: toWebRequest(event), params: { key: getRouterParam(event, 'key') ?? '' } }, { runtime })
20
24
  * );
21
25
  * ```
26
+ *
27
+ * **Access (spec 067).** A file is served only as part of the upload document that owns it: the document
28
+ * of the key's collection whose Forge-recorded `_storageKey` is this key. That document must be readable
29
+ * by the caller through the normal read pipeline (collection and row access, draft visibility), exactly
30
+ * as the Local API `findByID` with `overrideAccess: false` allows. It applies the collection's own
31
+ * `access.read`, **not** route-level options such as `requireAuth`/`allowedRoles` — a collection
32
+ * whose reads are gated only at its route must declare `access.read` to gate its files too. A key no
33
+ * document owns, or one whose document the
34
+ * caller cannot read, is the same `404` — it confirms nothing. A public bucket or CDN URL is outside
35
+ * this handler and outside Forge's access control.
22
36
  */
23
37
  export declare function handleFile<TEnv = unknown>(context: ApiContext<TEnv>, options: FileHandlerOptions<TEnv>): Promise<Response>;
24
38
  //# sourceMappingURL=files.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,MAAM,WAAW,kBAAkB,CAAC,IAAI,GAAG,OAAO;IAChD,OAAO,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAClC,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,UAAU,CAAC,IAAI,GAAG,OAAO,EAC7C,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,EACzB,OAAO,EAAE,kBAAkB,CAAC,IAAI,CAAC,GAChC,OAAO,CAAC,QAAQ,CAAC,CAkCnB"}
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAKvD,MAAM,WAAW,kBAAkB,CAAC,IAAI,GAAG,OAAO;IAChD,OAAO,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC;IAClC;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,UAAU,CAAC,IAAI,GAAG,OAAO,EAC7C,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,EACzB,OAAO,EAAE,kBAAkB,CAAC,IAAI,CAAC,GAChC,OAAO,CAAC,QAAQ,CAAC,CA8CnB"}
package/dist/files.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { AccessDeniedError, NotFoundError } from './errors.js';
2
+ import { resolveOptionalUser } from './handlers.js';
3
+ import { findStorageOwner } from './storage-intents.js';
1
4
  /**
2
5
  * Serves a stored file back over HTTP.
3
6
  *
@@ -12,35 +15,73 @@
12
15
  * handleFile({ request: toWebRequest(event), params: { key: getRouterParam(event, 'key') ?? '' } }, { runtime })
13
16
  * );
14
17
  * ```
18
+ *
19
+ * **Access (spec 067).** A file is served only as part of the upload document that owns it: the document
20
+ * of the key's collection whose Forge-recorded `_storageKey` is this key. That document must be readable
21
+ * by the caller through the normal read pipeline (collection and row access, draft visibility), exactly
22
+ * as the Local API `findByID` with `overrideAccess: false` allows. It applies the collection's own
23
+ * `access.read`, **not** route-level options such as `requireAuth`/`allowedRoles` — a collection
24
+ * whose reads are gated only at its route must declare `access.read` to gate its files too. A key no
25
+ * document owns, or one whose document the
26
+ * caller cannot read, is the same `404` — it confirms nothing. A public bucket or CDN URL is outside
27
+ * this handler and outside Forge's access control.
15
28
  */
16
29
  export async function handleFile(context, options) {
17
30
  const raw = context.params?.['key'];
18
- if (!raw) {
19
- return new Response(JSON.stringify({ error: 'Missing file key' }), {
20
- status: 400,
21
- headers: { 'content-type': 'application/json' }
22
- });
31
+ if (!raw)
32
+ return json({ error: 'Missing file key' }, 400);
33
+ let key;
34
+ try {
35
+ key = decodeURIComponent(raw);
36
+ }
37
+ catch {
38
+ return json({ error: 'Malformed file key' }, 400);
23
39
  }
24
- const key = decodeURIComponent(raw);
40
+ const { runtime } = options;
25
41
  try {
26
- const object = await options.runtime.adapters.storage.get(key);
27
- if (!object?.body) {
28
- return new Response(JSON.stringify({ error: `File '${key}' not found` }), {
29
- status: 404,
30
- headers: { 'content-type': 'application/json' }
42
+ const owner = await findStorageOwner(runtime, key);
43
+ if (!owner)
44
+ return notFound(key);
45
+ const user = await resolveOptionalUser(context, runtime);
46
+ try {
47
+ await runtime.findByID({
48
+ collection: owner.collection,
49
+ id: owner.id,
50
+ user,
51
+ overrideAccess: false,
52
+ depth: 0
31
53
  });
32
54
  }
55
+ catch (err) {
56
+ if (err instanceof NotFoundError || err instanceof AccessDeniedError)
57
+ return notFound(key);
58
+ throw err;
59
+ }
60
+ const object = await runtime.adapters.storage.get(key);
61
+ if (!object?.body)
62
+ return notFound(key);
33
63
  return new Response(object.body, {
34
64
  status: 200,
35
65
  headers: {
36
66
  'content-type': object.contentType ?? 'application/octet-stream',
37
- 'cache-control': options.cacheControl ?? 'public, max-age=60',
67
+ 'cache-control': user === null ? (options.cacheControl ?? 'public, max-age=60') : 'private, no-store',
38
68
  ...(object.size !== undefined && { 'content-length': String(object.size) })
39
69
  }
40
70
  });
41
71
  }
42
- catch (err) {
43
- return new Response(JSON.stringify({ error: err instanceof Error ? err.message : 'Failed to read file' }), { status: 500, headers: { 'content-type': 'application/json' } });
72
+ catch {
73
+ // No driver or storage message reaches the client (it can name buckets, tables or credentials).
74
+ return json({ error: 'Failed to read file' }, 500);
44
75
  }
45
76
  }
77
+ function notFound(key) {
78
+ return json({ error: `File '${key}' not found` }, 404);
79
+ }
80
+ /** Error bodies are never cached: an anonymous `404` must not be served to the owner who may read it. */
81
+ function json(body, status) {
82
+ return new Response(JSON.stringify(body), {
83
+ status,
84
+ headers: { 'content-type': 'application/json', 'cache-control': 'no-store' }
85
+ });
86
+ }
46
87
  //# sourceMappingURL=files.js.map
package/dist/files.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"AASA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAyB,EACzB,OAAiC;IAEjC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC,EAAE;YACjE,MAAM,EAAE,GAAG;YACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;SAChD,CAAC,CAAC;IACL,CAAC;IAED,MAAM,GAAG,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAEpC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/D,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;YAClB,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,SAAS,GAAG,aAAa,EAAE,CAAC,EAAE;gBACxE,MAAM,EAAE,GAAG;gBACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;aAChD,CAAC,CAAC;QACL,CAAC;QAED,OAAO,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE;YAC/B,MAAM,EAAE,GAAG;YACX,OAAO,EAAE;gBACP,cAAc,EAAE,MAAM,CAAC,WAAW,IAAI,0BAA0B;gBAChE,eAAe,EAAE,OAAO,CAAC,YAAY,IAAI,oBAAoB;gBAC7D,GAAG,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;aAC5E;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,IAAI,QAAQ,CACjB,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB,EAAE,CAAC,EACrF,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,CACjE,CAAC;IACJ,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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"}
package/dist/globals.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CmsUser } from '@forge-cms/core';
1
+ import type { CmsUser, GlobalDefinition } from '@forge-cms/core';
2
2
  import type { DatabaseRecord } from '@forge-cms/db';
3
3
  import type { OperationContext } from './context.js';
4
4
  export interface GlobalBaseArgs {
@@ -8,17 +8,35 @@ export interface GlobalBaseArgs {
8
8
  }
9
9
  export interface GetGlobalArgs extends GlobalBaseArgs {
10
10
  depth?: 0 | 1;
11
+ /** Resolve `localized` fields to this locale (with fallback), for a global declaring `locales` (spec 066). */
12
+ locale?: string;
11
13
  }
12
14
  export interface UpdateGlobalArgs extends GlobalBaseArgs {
13
15
  data: Record<string, unknown>;
16
+ /** Write `localized` fields into this locale only, keeping the others (spec 066). Must be one of the global's `locales`. */
17
+ locale?: string;
14
18
  }
19
+ /**
20
+ * The global options that exist on the shared collection types but can never apply to a singleton that
21
+ * is never created through `create` nor deleted (spec 066): delete hooks, and `create`/`delete` access
22
+ * rules. Refused at startup instead of being accepted and silently ignored. One message per problem.
23
+ * (Localized fields of globals are checked with collections', by `validateLocalizationSchema`.)
24
+ */
25
+ export declare function validateGlobalSchema(globals: readonly GlobalDefinition[]): string[];
15
26
  /**
16
27
  * Reads the singleton global document. Returns `null` if the global has never been written.
17
28
  */
18
29
  export declare function getGlobal(ctx: OperationContext, args: GetGlobalArgs): Promise<DatabaseRecord | null>;
19
30
  /**
20
- * Creates or updates the singleton global document. Unlike collections, globals always have
21
- * exactly one document — the first write creates it, subsequent writes update it.
31
+ * Creates or updates the singleton global document. Unlike collections, globals always have exactly one
32
+ * document — the first write creates it, later writes update it.
33
+ *
34
+ * Later writes are **partial**, exactly like a collection `update()` (spec 066): omitted fields keep
35
+ * their stored values (defaults and the draft status apply to the first write only), validation runs on
36
+ * the merged document, and a `locale` write changes that locale of each `localized` field and keeps the
37
+ * others. A query-returning `access.update` rule must match the stored row, and cannot authorize the
38
+ * first write (there is no row to match). Two simultaneous first writes: one commits, the other gets
39
+ * `409 CONCURRENT_MODIFICATION` with nothing written.
22
40
  */
23
41
  export declare function updateGlobal(ctx: OperationContext, args: UpdateGlobalArgs): Promise<DatabaseRecord>;
24
42
  //# sourceMappingURL=globals.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"globals.d.ts","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAoB,MAAM,iBAAiB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AA0BrD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;CACf;AAED,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AA6ED;;GAEG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAmDhC;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,cAAc,CAAC,CAsGzB"}
1
+ {"version":3,"file":"globals.d.ts","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAwB,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACvF,OAAO,KAAK,EAAwB,cAAc,EAAiB,MAAM,eAAe,CAAC;AAKzF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAuCrD,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAc,SAAQ,cAAc;IACnD,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IACd,8GAA8G;IAC9G,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,4HAA4H;IAC5H,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GAAG,MAAM,EAAE,CAsBnF;AAiGD;;GAEG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CA+DhC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,gBAAgB,EACrB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,cAAc,CAAC,CAyJzB"}