@haverstack/core 0.41.0 → 0.43.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 (317) hide show
  1. package/README.md +124 -64
  2. package/dist/access.d.ts +43 -47
  3. package/dist/access.d.ts.map +1 -1
  4. package/dist/access.js +66 -101
  5. package/dist/access.js.map +1 -1
  6. package/dist/{combine.d.ts → adapter/combine.d.ts} +8 -1
  7. package/dist/adapter/combine.d.ts.map +1 -0
  8. package/dist/{combine.js → adapter/combine.js} +18 -3
  9. package/dist/adapter/combine.js.map +1 -0
  10. package/dist/adapter/index.d.ts +19 -0
  11. package/dist/adapter/index.d.ts.map +1 -0
  12. package/dist/adapter/index.js +16 -0
  13. package/dist/adapter/index.js.map +1 -0
  14. package/dist/adapter/reach.d.ts +14 -0
  15. package/dist/adapter/reach.d.ts.map +1 -0
  16. package/dist/adapter/reach.js +31 -0
  17. package/dist/adapter/reach.js.map +1 -0
  18. package/dist/{sort.d.ts → adapter/sort.d.ts} +20 -20
  19. package/dist/adapter/sort.d.ts.map +1 -0
  20. package/dist/{sort.js → adapter/sort.js} +37 -27
  21. package/dist/adapter/sort.js.map +1 -0
  22. package/dist/associations/identity.d.ts +69 -0
  23. package/dist/associations/identity.d.ts.map +1 -0
  24. package/dist/associations/identity.js +174 -0
  25. package/dist/associations/identity.js.map +1 -0
  26. package/dist/associations/validation.d.ts +83 -0
  27. package/dist/associations/validation.d.ts.map +1 -0
  28. package/dist/associations/validation.js +312 -0
  29. package/dist/associations/validation.js.map +1 -0
  30. package/dist/attachment-records.d.ts +48 -0
  31. package/dist/attachment-records.d.ts.map +1 -0
  32. package/dist/attachment-records.js +52 -0
  33. package/dist/attachment-records.js.map +1 -0
  34. package/dist/collection.d.ts +178 -0
  35. package/dist/collection.d.ts.map +1 -0
  36. package/dist/collection.js +397 -0
  37. package/dist/collection.js.map +1 -0
  38. package/dist/content-match.d.ts +25 -0
  39. package/dist/content-match.d.ts.map +1 -0
  40. package/dist/content-match.js +51 -0
  41. package/dist/content-match.js.map +1 -0
  42. package/dist/content-translation.d.ts +42 -0
  43. package/dist/content-translation.d.ts.map +1 -0
  44. package/dist/content-translation.js +108 -0
  45. package/dist/content-translation.js.map +1 -0
  46. package/dist/did/did.d.ts.map +1 -0
  47. package/dist/{did.js → did/did.js} +3 -4
  48. package/dist/did/did.js.map +1 -0
  49. package/dist/{did-entry.d.ts → did/index.d.ts} +1 -1
  50. package/dist/did/index.d.ts.map +1 -0
  51. package/dist/{did-entry.js → did/index.js} +3 -3
  52. package/dist/did/index.js.map +1 -0
  53. package/dist/errors.d.ts +54 -66
  54. package/dist/errors.d.ts.map +1 -1
  55. package/dist/errors.js +58 -62
  56. package/dist/errors.js.map +1 -1
  57. package/dist/grants.d.ts +20 -44
  58. package/dist/grants.d.ts.map +1 -1
  59. package/dist/grants.js +69 -151
  60. package/dist/grants.js.map +1 -1
  61. package/dist/id.d.ts +13 -31
  62. package/dist/id.d.ts.map +1 -1
  63. package/dist/id.js +13 -31
  64. package/dist/id.js.map +1 -1
  65. package/dist/index.d.ts +13 -9
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +9 -6
  68. package/dist/index.js.map +1 -1
  69. package/dist/install.d.ts +3 -6
  70. package/dist/install.d.ts.map +1 -1
  71. package/dist/install.js +6 -10
  72. package/dist/install.js.map +1 -1
  73. package/dist/lazy.d.ts +13 -0
  74. package/dist/lazy.d.ts.map +1 -0
  75. package/dist/lazy.js +27 -0
  76. package/dist/lazy.js.map +1 -0
  77. package/dist/merge.d.ts +1 -1
  78. package/dist/merge.d.ts.map +1 -1
  79. package/dist/merge.js +6 -12
  80. package/dist/merge.js.map +1 -1
  81. package/dist/migration-steps.d.ts +102 -0
  82. package/dist/migration-steps.d.ts.map +1 -0
  83. package/dist/migration-steps.js +293 -0
  84. package/dist/migration-steps.js.map +1 -0
  85. package/dist/query-validation.d.ts +68 -121
  86. package/dist/query-validation.d.ts.map +1 -1
  87. package/dist/query-validation.js +214 -380
  88. package/dist/query-validation.js.map +1 -1
  89. package/dist/record-changes.d.ts +11 -150
  90. package/dist/record-changes.d.ts.map +1 -1
  91. package/dist/record-changes.js +14 -305
  92. package/dist/record-changes.js.map +1 -1
  93. package/dist/schema.d.ts +18 -7
  94. package/dist/schema.d.ts.map +1 -1
  95. package/dist/schema.js +59 -30
  96. package/dist/schema.js.map +1 -1
  97. package/dist/scoped-stack/authority-snapshot.d.ts +37 -0
  98. package/dist/scoped-stack/authority-snapshot.d.ts.map +1 -0
  99. package/dist/scoped-stack/authority-snapshot.js +49 -0
  100. package/dist/scoped-stack/authority-snapshot.js.map +1 -0
  101. package/dist/scoped-stack/authority.d.ts +169 -0
  102. package/dist/scoped-stack/authority.d.ts.map +1 -0
  103. package/dist/scoped-stack/authority.js +408 -0
  104. package/dist/scoped-stack/authority.js.map +1 -0
  105. package/dist/scoped-stack/family-policy.d.ts +59 -0
  106. package/dist/scoped-stack/family-policy.d.ts.map +1 -0
  107. package/dist/scoped-stack/family-policy.js +68 -0
  108. package/dist/scoped-stack/family-policy.js.map +1 -0
  109. package/dist/scoped-stack/scoped-stack.d.ts +318 -0
  110. package/dist/scoped-stack/scoped-stack.d.ts.map +1 -0
  111. package/dist/scoped-stack/scoped-stack.js +695 -0
  112. package/dist/scoped-stack/scoped-stack.js.map +1 -0
  113. package/dist/scoped-stack/subscription.d.ts +36 -0
  114. package/dist/scoped-stack/subscription.d.ts.map +1 -0
  115. package/dist/scoped-stack/subscription.js +69 -0
  116. package/dist/scoped-stack/subscription.js.map +1 -0
  117. package/dist/stack/arguments.d.ts +27 -0
  118. package/dist/stack/arguments.d.ts.map +1 -0
  119. package/dist/stack/arguments.js +53 -0
  120. package/dist/stack/arguments.js.map +1 -0
  121. package/dist/stack/attachments.d.ts +24 -0
  122. package/dist/stack/attachments.d.ts.map +1 -0
  123. package/dist/stack/attachments.js +113 -0
  124. package/dist/stack/attachments.js.map +1 -0
  125. package/dist/{changes.d.ts → stack/changes.d.ts} +30 -72
  126. package/dist/stack/changes.d.ts.map +1 -0
  127. package/dist/{changes.js → stack/changes.js} +40 -74
  128. package/dist/stack/changes.js.map +1 -0
  129. package/dist/stack/client.d.ts +247 -0
  130. package/dist/stack/client.d.ts.map +1 -0
  131. package/dist/stack/client.js +9 -0
  132. package/dist/stack/client.js.map +1 -0
  133. package/dist/stack/identity-bindings.d.ts.map +1 -0
  134. package/dist/{identity-bindings.js → stack/identity-bindings.js} +5 -14
  135. package/dist/stack/identity-bindings.js.map +1 -0
  136. package/dist/stack/install-app.d.ts +22 -0
  137. package/dist/stack/install-app.d.ts.map +1 -0
  138. package/dist/stack/install-app.js +262 -0
  139. package/dist/stack/install-app.js.map +1 -0
  140. package/dist/stack/integrity.d.ts +94 -0
  141. package/dist/stack/integrity.d.ts.map +1 -0
  142. package/dist/stack/integrity.js +268 -0
  143. package/dist/stack/integrity.js.map +1 -0
  144. package/dist/{limits.d.ts → stack/limits.d.ts} +2 -5
  145. package/dist/stack/limits.d.ts.map +1 -0
  146. package/dist/{limits.js → stack/limits.js} +3 -6
  147. package/dist/stack/limits.js.map +1 -0
  148. package/dist/stack/migrations.d.ts +59 -0
  149. package/dist/stack/migrations.d.ts.map +1 -0
  150. package/dist/stack/migrations.js +141 -0
  151. package/dist/stack/migrations.js.map +1 -0
  152. package/dist/stack/reads.d.ts +69 -0
  153. package/dist/stack/reads.d.ts.map +1 -0
  154. package/dist/stack/reads.js +141 -0
  155. package/dist/stack/reads.js.map +1 -0
  156. package/dist/stack/record-id.d.ts +35 -0
  157. package/dist/stack/record-id.d.ts.map +1 -0
  158. package/dist/stack/record-id.js +82 -0
  159. package/dist/stack/record-id.js.map +1 -0
  160. package/dist/stack/stack.d.ts +444 -0
  161. package/dist/stack/stack.d.ts.map +1 -0
  162. package/dist/stack/stack.js +1339 -0
  163. package/dist/stack/stack.js.map +1 -0
  164. package/dist/stack/system-types.d.ts +16 -0
  165. package/dist/stack/system-types.d.ts.map +1 -0
  166. package/dist/stack/system-types.js +111 -0
  167. package/dist/stack/system-types.js.map +1 -0
  168. package/dist/stack/type-grants.d.ts +20 -0
  169. package/dist/stack/type-grants.d.ts.map +1 -0
  170. package/dist/stack/type-grants.js +69 -0
  171. package/dist/stack/type-grants.js.map +1 -0
  172. package/dist/stack/write-expectation.d.ts +79 -0
  173. package/dist/stack/write-expectation.d.ts.map +1 -0
  174. package/dist/stack/write-expectation.js +71 -0
  175. package/dist/stack/write-expectation.js.map +1 -0
  176. package/dist/stack/write-gate.d.ts +23 -0
  177. package/dist/stack/write-gate.d.ts.map +1 -0
  178. package/dist/stack/write-gate.js +17 -0
  179. package/dist/stack/write-gate.js.map +1 -0
  180. package/dist/testing.d.ts +44 -33
  181. package/dist/testing.d.ts.map +1 -1
  182. package/dist/testing.js +180 -176
  183. package/dist/testing.js.map +1 -1
  184. package/dist/type-handle.d.ts +67 -78
  185. package/dist/type-handle.d.ts.map +1 -1
  186. package/dist/type-handle.js +44 -111
  187. package/dist/type-handle.js.map +1 -1
  188. package/dist/types/adapter.d.ts +282 -0
  189. package/dist/types/adapter.d.ts.map +1 -0
  190. package/dist/types/adapter.js +5 -0
  191. package/dist/types/adapter.js.map +1 -0
  192. package/dist/types/associations.d.ts +140 -0
  193. package/dist/types/associations.d.ts.map +1 -0
  194. package/dist/types/associations.js +6 -0
  195. package/dist/types/associations.js.map +1 -0
  196. package/dist/types/capabilities.d.ts +89 -0
  197. package/dist/types/capabilities.d.ts.map +1 -0
  198. package/dist/types/capabilities.js +5 -0
  199. package/dist/types/capabilities.js.map +1 -0
  200. package/dist/types/events.d.ts +150 -0
  201. package/dist/types/events.d.ts.map +1 -0
  202. package/dist/types/events.js +5 -0
  203. package/dist/types/events.js.map +1 -0
  204. package/dist/types/ids.d.ts +43 -0
  205. package/dist/types/ids.d.ts.map +1 -0
  206. package/dist/types/ids.js +5 -0
  207. package/dist/types/ids.js.map +1 -0
  208. package/dist/types/index.d.ts +19 -0
  209. package/dist/types/index.d.ts.map +1 -0
  210. package/dist/types/index.js +19 -0
  211. package/dist/types/index.js.map +1 -0
  212. package/dist/types/journal.d.ts +47 -0
  213. package/dist/types/journal.d.ts.map +1 -0
  214. package/dist/types/journal.js +5 -0
  215. package/dist/types/journal.js.map +1 -0
  216. package/dist/types/query.d.ts +185 -0
  217. package/dist/types/query.d.ts.map +1 -0
  218. package/dist/types/query.js +10 -0
  219. package/dist/types/query.js.map +1 -0
  220. package/dist/types/records.d.ts +76 -0
  221. package/dist/types/records.d.ts.map +1 -0
  222. package/dist/types/records.js +5 -0
  223. package/dist/types/records.js.map +1 -0
  224. package/dist/types/schema.d.ts +74 -0
  225. package/dist/types/schema.d.ts.map +1 -0
  226. package/dist/types/schema.js +5 -0
  227. package/dist/types/schema.js.map +1 -0
  228. package/dist/types/system.d.ts +161 -0
  229. package/dist/types/system.d.ts.map +1 -0
  230. package/dist/types/system.js +32 -0
  231. package/dist/types/system.js.map +1 -0
  232. package/dist/types/tokens.d.ts +39 -0
  233. package/dist/types/tokens.d.ts.map +1 -0
  234. package/dist/types/tokens.js +5 -0
  235. package/dist/types/tokens.js.map +1 -0
  236. package/dist/validate.d.ts +31 -70
  237. package/dist/validate.d.ts.map +1 -1
  238. package/dist/validate.js +52 -92
  239. package/dist/validate.js.map +1 -1
  240. package/dist/{attachment-download.d.ts → wire/attachment-download.d.ts} +0 -57
  241. package/dist/wire/attachment-download.d.ts.map +1 -0
  242. package/dist/{attachment-download.js → wire/attachment-download.js} +0 -61
  243. package/dist/wire/attachment-download.js.map +1 -0
  244. package/dist/{auth.d.ts → wire/auth.d.ts} +5 -8
  245. package/dist/wire/auth.d.ts.map +1 -0
  246. package/dist/{auth.js → wire/auth.js} +5 -8
  247. package/dist/wire/auth.js.map +1 -0
  248. package/dist/{wire-body.d.ts → wire/body.d.ts} +35 -12
  249. package/dist/wire/body.d.ts.map +1 -0
  250. package/dist/{wire-body.js → wire/body.js} +74 -13
  251. package/dist/wire/body.js.map +1 -0
  252. package/dist/{wire-entry.d.ts → wire/index.d.ts} +12 -12
  253. package/dist/wire/index.d.ts.map +1 -0
  254. package/dist/{wire-entry.js → wire/index.js} +13 -17
  255. package/dist/wire/index.js.map +1 -0
  256. package/dist/wire/record.d.ts +31 -0
  257. package/dist/wire/record.d.ts.map +1 -0
  258. package/dist/{wire-record.js → wire/record.js} +7 -67
  259. package/dist/wire/record.js.map +1 -0
  260. package/dist/{wire-request.d.ts → wire/request.d.ts} +32 -49
  261. package/dist/wire/request.d.ts.map +1 -0
  262. package/dist/{wire-request.js → wire/request.js} +113 -83
  263. package/dist/wire/request.js.map +1 -0
  264. package/package.json +7 -7
  265. package/dist/adapter-entry.d.ts +0 -16
  266. package/dist/adapter-entry.d.ts.map +0 -1
  267. package/dist/adapter-entry.js +0 -14
  268. package/dist/adapter-entry.js.map +0 -1
  269. package/dist/attachment-download.d.ts.map +0 -1
  270. package/dist/attachment-download.js.map +0 -1
  271. package/dist/auth.d.ts.map +0 -1
  272. package/dist/auth.js.map +0 -1
  273. package/dist/changes.d.ts.map +0 -1
  274. package/dist/changes.js.map +0 -1
  275. package/dist/combine.d.ts.map +0 -1
  276. package/dist/combine.js.map +0 -1
  277. package/dist/did-entry.d.ts.map +0 -1
  278. package/dist/did-entry.js.map +0 -1
  279. package/dist/did.d.ts.map +0 -1
  280. package/dist/did.js.map +0 -1
  281. package/dist/identity-bindings.d.ts.map +0 -1
  282. package/dist/identity-bindings.js.map +0 -1
  283. package/dist/limits.d.ts.map +0 -1
  284. package/dist/limits.js.map +0 -1
  285. package/dist/record-id.d.ts +0 -70
  286. package/dist/record-id.d.ts.map +0 -1
  287. package/dist/record-id.js +0 -127
  288. package/dist/record-id.js.map +0 -1
  289. package/dist/scoped-stack.d.ts +0 -549
  290. package/dist/scoped-stack.d.ts.map +0 -1
  291. package/dist/scoped-stack.js +0 -1301
  292. package/dist/scoped-stack.js.map +0 -1
  293. package/dist/sort.d.ts.map +0 -1
  294. package/dist/sort.js.map +0 -1
  295. package/dist/stack-reads.d.ts +0 -38
  296. package/dist/stack-reads.d.ts.map +0 -1
  297. package/dist/stack-reads.js +0 -79
  298. package/dist/stack-reads.js.map +0 -1
  299. package/dist/stack.d.ts +0 -1032
  300. package/dist/stack.d.ts.map +0 -1
  301. package/dist/stack.js +0 -2337
  302. package/dist/stack.js.map +0 -1
  303. package/dist/types.d.ts +0 -1278
  304. package/dist/types.d.ts.map +0 -1
  305. package/dist/types.js +0 -51
  306. package/dist/types.js.map +0 -1
  307. package/dist/wire-body.d.ts.map +0 -1
  308. package/dist/wire-body.js.map +0 -1
  309. package/dist/wire-entry.d.ts.map +0 -1
  310. package/dist/wire-entry.js.map +0 -1
  311. package/dist/wire-record.d.ts +0 -50
  312. package/dist/wire-record.d.ts.map +0 -1
  313. package/dist/wire-record.js.map +0 -1
  314. package/dist/wire-request.d.ts.map +0 -1
  315. package/dist/wire-request.js.map +0 -1
  316. /package/dist/{did.d.ts → did/did.d.ts} +0 -0
  317. /package/dist/{identity-bindings.d.ts → stack/identity-bindings.d.ts} +0 -0
@@ -0,0 +1,695 @@
1
+ /**
2
+ * ScopedStack — the permission-enforcing view
3
+ * -------------------------------------------------------
4
+ * A `Stack` answers every question; a `ScopedStack` answers only the ones a
5
+ * particular requester is entitled to. It wraps a Stack rather than an
6
+ * adapter, so every invariant the Stack layer holds is already in force by
7
+ * the time a check here runs, and the gates below only ever narrow.
8
+ *
9
+ * Two identities, one rule: **the principal governs authority, the subject
10
+ * governs attribution.** Both are DIDs and both are often equal, which is
11
+ * what makes them easy to confuse — so the split is stated on every gate
12
+ * that depends on it. Grant lookup and the privilege-bearing gates no grant
13
+ * reaches (resharing, group management, purge, widening access at create
14
+ * time) key on the principal; authorship, `-own` matching, record-level
15
+ * permissions and "files I uploaded" key on the subject. Unconditional
16
+ * owner access splits the same way: an owner subject passes every
17
+ * permission check, an owner principal is not bounded by grants, and under
18
+ * delegation both halves apply. The questions those gates ask are
19
+ * ScopeAuthority's, in authority.ts; this module turns their answers into
20
+ * refusals.
21
+ *
22
+ * Also holds the feed side of the same rule: a subscription's deliveries
23
+ * ask the same read reach a get() does, so a feed can never disagree with
24
+ * a read about what a session sees.
25
+ *
26
+ * See docs/spec/access-control.md and docs/spec/events.md § Permission scoping.
27
+ */
28
+ import { baseIdOf } from '../schema.js';
29
+ import { validatePatchValues } from '../validate.js';
30
+ import { ChangeEmitter, assertSinceUsable } from '../stack/changes.js';
31
+ import { SYSTEM_TYPES } from '../types/index.js';
32
+ import { StackConflictError, StackNotFoundError, StackPermissionError, StackValidationError, } from '../errors.js';
33
+ import { assertAssociationEdits } from '../associations/validation.js';
34
+ import { addsOf, associationEqual } from '../associations/identity.js';
35
+ import { assertQueryCapabilities, assertValidBaseIdFilter, splitQuery, filtersContent, } from '../query-validation.js';
36
+ import { checkFamily, storedVersionDiffers, WRITE_EXPECTATION, } from '../stack/write-expectation.js';
37
+ import { WRITE_GATE } from '../stack/write-gate.js';
38
+ import { assertAttachmentSize } from '../stack/limits.js';
39
+ import { validateIdTimestampSkew, validateRecordId } from '../stack/record-id.js';
40
+ import { findAppCardByDid, lookupEntityByDid } from '../stack/reads.js';
41
+ import { presentDeleted, withoutAuthorityChanges } from '../record-changes.js';
42
+ import { createCollection } from '../collection.js';
43
+ import { uploadContent } from '../stack/attachments.js';
44
+ import { assertAssociationSet, assertContentPatch, assertParentArgument, assertUnlistedArgument, } from '../stack/arguments.js';
45
+ import { ScopeAuthority } from './authority.js';
46
+ import { familyPolicy, ownerFieldRefusal } from './family-policy.js';
47
+ import { ScopedSubscription } from './subscription.js';
48
+ /**
49
+ * Held by Stack alone: the index re-exports ScopedStack but not this, so
50
+ * asEntity()/asActor() stay the only way to one, and the actor
51
+ * normalization and open-check they run can't be skipped.
52
+ */
53
+ export const scopeToken = Symbol('ScopedStack');
54
+ /**
55
+ * A permission-enforcing view of a Stack for one (principal, subject) pair,
56
+ * obtained via `stack.asEntity()` or `stack.asActor()`. A record the request
57
+ * cannot read answers exactly as a missing one does.
58
+ * See docs/spec/disclosure.md § Which refusal a Record answers with.
59
+ */
60
+ export class ScopedStack {
61
+ #authority;
62
+ stack;
63
+ idTimestampSkewMs;
64
+ adapter;
65
+ changes;
66
+ assertStackOpen;
67
+ migrations;
68
+ queryReadable;
69
+ /** Not callable from outside the package — see ScopedStackInit. */
70
+ constructor(token, init) {
71
+ if (token !== scopeToken) {
72
+ throw new TypeError('A ScopedStack is obtained through Stack.asEntity() or Stack.asActor()');
73
+ }
74
+ this.stack = init.stack;
75
+ this.#authority = new ScopeAuthority(init.stack, init.principalId, init.subjectId, init.queryReadable);
76
+ this.idTimestampSkewMs = init.idTimestampSkewMs;
77
+ this.adapter = init.adapter;
78
+ this.changes = init.changes;
79
+ this.assertStackOpen = init.assertOpen;
80
+ this.migrations = init.migrations;
81
+ this.queryReadable = init.queryReadable;
82
+ }
83
+ collection(handle) {
84
+ this.assertStackOpen();
85
+ return createCollection(this, this.migrations, handle, () => this.stack.listTypes());
86
+ }
87
+ /**
88
+ * The identity whose authority this view acts with; `null` when
89
+ * anonymous. See docs/spec/access-control.md § Delegation: principal and subject.
90
+ */
91
+ get principalId() {
92
+ return this.#authority.principalId;
93
+ }
94
+ /**
95
+ * The identity this view acts for and attributes writes to; `null` when
96
+ * anonymous. Equal to `principalId` unless an app is acting for a user.
97
+ */
98
+ get subjectId() {
99
+ return this.#authority.subjectId;
100
+ }
101
+ get capabilities() {
102
+ return this.stack.capabilities;
103
+ }
104
+ get actor() {
105
+ // The key is always present, so spreading this last overrides anything
106
+ // a caller passed: a scoped requester names itself by making the
107
+ // request, never by describing itself in the options.
108
+ return { actor: this.#authority.requester };
109
+ }
110
+ /** Refuse a create outside this request's reach — see ScopeAuthority.canCreate(). */
111
+ async requireCreateGrant(typeId, snapshot) {
112
+ if (!(await this.#authority.canCreate(typeId, snapshot))) {
113
+ throw new StackPermissionError(`No create grant for type "${typeId}"`);
114
+ }
115
+ }
116
+ /** Refuse anything but the owner acting as itself — see ScopeAuthority.ownerActingAlone. */
117
+ requireOwnerActingAlone(message) {
118
+ if (!this.#authority.ownerActingAlone)
119
+ throw new StackPermissionError(message);
120
+ }
121
+ /**
122
+ * Fetch a record the subject can reach and the principal holds `update` on
123
+ * (via permissions or an update grant), or throw. `mutating: false` marks
124
+ * the history readers, which borrow this gate without changing anything —
125
+ * the one way through it a `_grant` Record stays open to.
126
+ */
127
+ async requireUpdatable(id, opts, snapshot) {
128
+ // The `_grant` write fence applies to mutating callers only: reading a
129
+ // grant Record's history is not the escalation that fence exists to stop.
130
+ return this.requireVerb(id, 'update', { fenceGrantRecord: opts.mutating ?? true, expect: opts.expect }, snapshot);
131
+ }
132
+ /**
133
+ * The write gate both requireUpdatable() and requireDeletable() are: the
134
+ * subject can reach the record (record-level permission or a grant) and
135
+ * the principal holds the same verb, intersected — or, for a `_group`, the
136
+ * stricter management rule that no grant reaches.
137
+ * See docs/spec/access-control.md § Delegation: principal and subject.
138
+ */
139
+ async requireVerb(id, verb, opts, snapshot) {
140
+ const record = await this.stack.get(id, { includeDeleted: true });
141
+ if (!record)
142
+ throw new StackNotFoundError(`Record not found: "${id}"`);
143
+ // Ahead of the gate, so another family's record is not found whatever
144
+ // this requester may do to it — the answer a collection's get() gives.
145
+ checkFamily(record, opts.expect);
146
+ await this.gateVerb(record, verb, opts.fenceGrantRecord, snapshot);
147
+ return record;
148
+ }
149
+ /** requireVerb()'s gate, asked of a record already found in its family. */
150
+ async gateVerb(record, verb, fenceGrantRecord, snapshot) {
151
+ const { id } = record;
152
+ const { writers } = familyPolicy(record.typeId);
153
+ // The owner-alone fence is on writes only: reading how a grant came to
154
+ // be is not the escalation it exists to stop.
155
+ if (fenceGrantRecord)
156
+ await this.requireOwnerAloneWriter(record, snapshot);
157
+ if (writers === 'group-managers') {
158
+ if (!this.#authority.isGroupManager(record)) {
159
+ throw await this.#authority.denialFor(record, `Cannot change group "${id}": only its admins can manage it`, snapshot);
160
+ }
161
+ return;
162
+ }
163
+ if (await this.#authority.may(verb, record, snapshot))
164
+ return;
165
+ const what = `Cannot ${verb} "${id}" (${record.typeId})`;
166
+ throw await this.#authority.denialFor(record, (await this.#authority.subjectMay(verb, record, snapshot))
167
+ ? `${what}: the app acting for this entity holds no ${verb} grant`
168
+ : `${what}: requires ${verb}-own or ${verb}-any`, snapshot);
169
+ }
170
+ /**
171
+ * Fetch a record the subject can reach and the principal holds `delete` on
172
+ * (via permissions or a delete grant), or throw.
173
+ */
174
+ requireDeletable(id, expect, snapshot) {
175
+ // No soft-delete refusal: undelete() shares this gate, and a tombstone
176
+ // is exactly what it addresses.
177
+ return this.requireVerb(id, 'delete', { fenceGrantRecord: true, expect }, snapshot);
178
+ }
179
+ /**
180
+ * Gates file-ref content fields on file access, mirroring the
181
+ * attachment-association gate — a file-ref field conveys attachment
182
+ * access exactly like an `attachment` association. Only fields present
183
+ * in `content` are checked (a content patch is a merge patch; untouched fields
184
+ * carry no new reference).
185
+ */
186
+ async requireFileRefAccess(typeId, content, snapshot) {
187
+ for (const field of await this.#authority.fileRefFieldNames(typeId)) {
188
+ const value = content[field];
189
+ if (typeof value !== 'string')
190
+ continue;
191
+ if (!(await this.#authority.canAccessFile(value, snapshot))) {
192
+ throw new StackPermissionError(`Cannot reference file "${value}" in field "${field}": it does not exist or you cannot read it`);
193
+ }
194
+ }
195
+ }
196
+ /**
197
+ * Reference-creation gate for one association: file access for an
198
+ * `attachment`, read access for a `relationship` to a record in this
199
+ * stack. Other targets convey nothing, and a `_group` roster has the
200
+ * stricter management gate. See docs/spec/access-control.md
201
+ * § Reference-creation gating.
202
+ */
203
+ async requireAssociationAccess(typeId, association, snapshot) {
204
+ if (association.kind === 'attachment') {
205
+ if (!(await this.#authority.canAccessFile(association.fileId, snapshot))) {
206
+ throw new StackPermissionError(`Cannot reference file "${association.fileId}": it does not exist or you cannot read it`);
207
+ }
208
+ }
209
+ else if (association.kind === 'relationship' && !familyPolicy(typeId).rosterAssociations) {
210
+ const { target } = association;
211
+ // `stackUrl` is tested for a value, not for presence: absent and
212
+ // empty are one target — storage, targetEqual() and the filter all
213
+ // read them as this stack — so a check on presence alone would
214
+ // leave one spelling of a local Record ungated.
215
+ if (target.kind !== 'record' || target.stackUrl)
216
+ return;
217
+ if (!(await this.#authority.canReadReferent(target.recordId, snapshot))) {
218
+ throw new StackPermissionError(`Cannot reference record "${target.recordId}": it does not exist or you cannot read it`);
219
+ }
220
+ }
221
+ }
222
+ /**
223
+ * Create a record on behalf of the subject, stamped with the requester as
224
+ * `createdBy`. Every option that could widen reach or forge a position is
225
+ * refused rather than dropped, so an app never believes it published
226
+ * something it didn't. See docs/spec/data-model.md § Backdating on import.
227
+ */
228
+ async create(typeId, content, opts = {}) {
229
+ const principal = this.#authority.principalId;
230
+ if (!principal)
231
+ throw new StackPermissionError('Anonymous requesters cannot create records');
232
+ // A grantee is exactly the untrusted actor the `id` skew check below
233
+ // exists to stop from forging a sort position, and a delegated app
234
+ // acting for the owner inherits none of the owner's extra trust — same
235
+ // reasoning as ScopeAuthority.mayGrantAccess(). Asked value-wise, since an
236
+ // `undefined` carries no date and Stack.create() reads it as absent.
237
+ if ((opts.createdAt !== undefined || opts.updatedAt !== undefined) &&
238
+ !this.#authority.ownerActingAlone) {
239
+ throw new StackPermissionError('createdAt/updatedAt can only be set by the stack owner acting alone; a grantee or delegated create always stamps the current time.');
240
+ }
241
+ const snapshot = this.#authority.snapshot();
242
+ await this.requireCreateGrant(typeId, snapshot);
243
+ // The exemption is the owner's own, so delegation doesn't carry it: an
244
+ // owner principal acting for someone else would otherwise let that
245
+ // subject name any fileId and reach the bytes through the uploader
246
+ // clause, which matches on the subject this create stamps.
247
+ const namesFile = familyPolicy(typeId).createNamesFile;
248
+ if (!this.#authority.ownerActingAlone && namesFile !== undefined) {
249
+ const fileId = content[namesFile];
250
+ if (typeof fileId !== 'string' ||
251
+ !(await this.#authority.hasReadableReference(fileId, snapshot))) {
252
+ throw new StackPermissionError(`Cannot reference file "${String(fileId)}": it does not exist or you cannot read it`);
253
+ }
254
+ }
255
+ if (opts.permissions?.length && !this.#authority.mayGrantAccess()) {
256
+ throw new StackPermissionError('A delegated principal cannot set permissions, at create time or after');
257
+ }
258
+ if (opts.unlisted && !this.#authority.mayGrantAccess()) {
259
+ throw new StackPermissionError('A delegated principal cannot create an unlisted record, at create time or after');
260
+ }
261
+ this.requireOwnerFields(typeId, undefined, content);
262
+ await this.requireAppIdMatchesPrincipal(opts.appId);
263
+ if (opts.id !== undefined) {
264
+ validateRecordId(opts.id);
265
+ // A backdated create — the owner's alone, checked above — has its id
266
+ // checked against `createdAt` by Stack.create() instead of against now.
267
+ // See docs/spec/data-model.md § Record IDs.
268
+ if (opts.createdAt === undefined) {
269
+ validateIdTimestampSkew(opts.id, this.idTimestampSkewMs, Date.now(), 'the current time');
270
+ }
271
+ }
272
+ if (opts.parentId !== undefined &&
273
+ !(await this.#authority.canReadReferent(opts.parentId, snapshot))) {
274
+ throw new StackPermissionError(`Cannot reference record "${opts.parentId}" as parent: it does not exist or you cannot read it`);
275
+ }
276
+ for (const assoc of opts.associations ?? []) {
277
+ await this.requireAssociationAccess(typeId, assoc, snapshot);
278
+ }
279
+ await this.requireFileRefAccess(typeId, content, snapshot);
280
+ return this.stack.create(typeId, content, {
281
+ ...opts,
282
+ createdBy: this.#authority.requester,
283
+ });
284
+ }
285
+ /**
286
+ * A record this request may read, or null. Never throws
287
+ * StackPermissionError: an unreadable record answers exactly as a missing
288
+ * one does, so a caller learns only what it may read.
289
+ * See docs/spec/disclosure.md § Which refusal a Record answers with.
290
+ */
291
+ async get(id, opts = {}) {
292
+ const record = await this.stack.get(id, opts);
293
+ if (!record)
294
+ return null;
295
+ if (!(await this.#authority.canRead(record, this.#authority.snapshot())))
296
+ return null;
297
+ return presentDeleted(record);
298
+ }
299
+ /**
300
+ * Runs through this.query(), so the read reach still applies — this
301
+ * answers a question the caller asks on its own behalf. includeUnlisted
302
+ * is passed only when the request is the owner acting alone, per
303
+ * docs/spec/identity.md § DID bindings.
304
+ */
305
+ async getEntityByDid(did) {
306
+ return lookupEntityByDid((q) => this.query(q), did, filtersContent(this.stack.capabilities), this.#authority.ownerActingAlone);
307
+ }
308
+ async getOwnerEntity() {
309
+ return this.getEntityByDid(this.stack.ownerEntityId);
310
+ }
311
+ /**
312
+ * Query records, among those this request can read: the adapter matches
313
+ * the request's read reach as one more condition of the query, so a page,
314
+ * its cursor and whether one comes back are decided among readable
315
+ * Records alone. See docs/spec/disclosure.md § What a query reveals.
316
+ */
317
+ async query(query = {}) {
318
+ const split = splitQuery(query);
319
+ assertQueryCapabilities(split.query, this.stack.capabilities);
320
+ if (query.filter?.includeUnlisted && !this.#authority.ownerActingAlone) {
321
+ throw new StackPermissionError('includeUnlisted is owner-only');
322
+ }
323
+ const reach = await this.#authority.snapshot().reach('read');
324
+ const page = await this.queryReadable(split, reach);
325
+ // Projected here as well as in get(), or `includeDeleted` would be a
326
+ // strictly better read channel than the fetch-by-ID it is supposed to
327
+ // match.
328
+ return { records: page.records.map(presentDeleted), cursor: page.cursor };
329
+ }
330
+ /**
331
+ * Patch content on behalf of the subject, on the write gate, plus the
332
+ * gates a content value can carry: a card's binding fields and the files
333
+ * a `file-ref` field names. See docs/spec/access-control.md § Per-verb gates.
334
+ */
335
+ async patchContent(id, patch, opts = {}) {
336
+ // Every argument check sits ahead of the gate, here and in each write
337
+ // verb below, so a malformed call earns one refusal from every requester.
338
+ assertContentPatch(patch);
339
+ const patchErrors = validatePatchValues(patch);
340
+ if (patchErrors.length > 0)
341
+ throw new StackValidationError(patchErrors);
342
+ const snapshot = this.#authority.snapshot();
343
+ const record = await this.requireUpdatable(id, { expect: opts }, snapshot);
344
+ // A patch for another version is refused by `Stack`, which owes the
345
+ // tombstone and `ifVersion` refusals first; the content gates would read
346
+ // a stored Type it was not written for. Pinned to this read, `Stack`
347
+ // cannot find a record the patch lands on.
348
+ const mismatched = storedVersionDiffers(record, opts, true);
349
+ if (!mismatched) {
350
+ // Value-wise, not presence-wise: a client that reads a card, edits
351
+ // its `name` and sends the whole content object back is not setting
352
+ // the binding it round-trips, and `name` is writable by record-level
353
+ // permission.
354
+ const content = record.content;
355
+ this.requireOwnerFields(record.typeId, content, { ...content, ...patch });
356
+ await this.requireFileRefAccess(record.typeId, patch, snapshot);
357
+ }
358
+ const ifVersion = mismatched ? (opts.ifVersion ?? record.version) : opts.ifVersion;
359
+ return this.stack.patchContent(id, patch, { ...opts, ifVersion, ...this.actor });
360
+ }
361
+ /**
362
+ * The write gate, plus read access to the destination: a container is a
363
+ * reference the requester has to be allowed to create.
364
+ */
365
+ async setParent(id, parentId, opts = {}) {
366
+ assertParentArgument(parentId);
367
+ const snapshot = this.#authority.snapshot();
368
+ const record = await this.requireUpdatable(id, { expect: opts }, snapshot);
369
+ if (typeof parentId === 'string' &&
370
+ parentId !== (record.parentId ?? null) &&
371
+ !(await this.#authority.canReadReferent(parentId, snapshot))) {
372
+ throw new StackPermissionError(`Cannot reference record "${parentId}" as parent: it does not exist or you cannot read it`);
373
+ }
374
+ return this.stack.setParent(id, parentId, { ...opts, ...this.actor });
375
+ }
376
+ /**
377
+ * Listing decides who can enumerate a record, so it carries the reshare
378
+ * gate rather than the write bit. See docs/spec/unlisted.md.
379
+ */
380
+ async setUnlisted(id, unlisted, opts = {}) {
381
+ assertUnlistedArgument(unlisted);
382
+ await this.requireReshareable(id, opts, this.#authority.snapshot());
383
+ return this.stack.setUnlisted(id, unlisted, { ...opts, ...this.actor });
384
+ }
385
+ /**
386
+ * The write gate, plus the reference gate on each add new to the record:
387
+ * a remove, or an add of an identity already held, names a reference the
388
+ * record carries, so it is gated by the write bit alone.
389
+ */
390
+ async editAssociations(id, edits, opts = {}) {
391
+ assertAssociationEdits(edits, 'editAssociations()', 'data');
392
+ const snapshot = this.#authority.snapshot();
393
+ return this.stack.editAssociations(id, edits, {
394
+ ...opts,
395
+ ...this.actor,
396
+ [WRITE_GATE]: (record) => this.gateAssociationWrite(record, addsOf(edits), snapshot),
397
+ });
398
+ }
399
+ /** editAssociations()'s gates, with every element new to the set an add. */
400
+ async setAssociations(id, associations, opts = {}) {
401
+ assertAssociationSet(associations, 'associations');
402
+ const snapshot = this.#authority.snapshot();
403
+ return this.stack.setAssociations(id, associations, {
404
+ ...opts,
405
+ ...this.actor,
406
+ [WRITE_GATE]: (record) => this.gateAssociationWrite(record, associations, snapshot),
407
+ });
408
+ }
409
+ /**
410
+ * The write gate, then the reference gate over the elements a write adds,
411
+ * asked against the stored set so an association the record already
412
+ * carries is not re-gated. `Stack` asks it of each read it writes from,
413
+ * so a retried write is gated on the set it lands on.
414
+ */
415
+ async gateAssociationWrite(record, added, snapshot) {
416
+ await this.gateVerb(record, 'update', true, snapshot);
417
+ for (const association of added) {
418
+ if ((record.associations ?? []).some((a) => associationEqual(a, association)))
419
+ continue;
420
+ await this.requireAssociationAccess(record.typeId, association, snapshot);
421
+ }
422
+ }
423
+ /**
424
+ * Extend or withdraw who reaches a record, on the reshare gate. The write
425
+ * bit does not confer it, or a write-holder could decide who else reaches
426
+ * the record. See docs/spec/access-control.md § Record-level permissions.
427
+ */
428
+ async editAccess(id, edits, opts = {}) {
429
+ assertAssociationEdits(edits, 'editAccess()', 'authority');
430
+ const snapshot = this.#authority.snapshot();
431
+ return this.stack.editAccess(id, edits, {
432
+ ...opts,
433
+ ...this.actor,
434
+ [WRITE_GATE]: (record) => this.gateReshare(record, snapshot),
435
+ });
436
+ }
437
+ /** See editAccess() — the same gate. */
438
+ async setAccess(id, permissions, opts = {}) {
439
+ assertAssociationSet(permissions, 'permissions');
440
+ const snapshot = this.#authority.snapshot();
441
+ return this.stack.setAccess(id, permissions, {
442
+ ...opts,
443
+ ...this.actor,
444
+ [WRITE_GATE]: (record) => this.gateReshare(record, snapshot),
445
+ });
446
+ }
447
+ /**
448
+ * The record, having established that this request may decide who else
449
+ * reaches it — the owner-or-creator rule, asked of both sides of a
450
+ * delegation, or a Group's admin rule for a `_group`. The gate the access
451
+ * and listing verbs carry, which the write bit deliberately does not confer.
452
+ */
453
+ async requireReshareable(id, expect, snapshot) {
454
+ const record = await this.stack.get(id, { includeDeleted: true });
455
+ if (!record)
456
+ throw new StackNotFoundError(`Record not found: "${id}"`);
457
+ checkFamily(record, expect);
458
+ await this.gateReshare(record, snapshot);
459
+ return record;
460
+ }
461
+ /** requireReshareable()'s gate, asked of a record already found in its family. */
462
+ async gateReshare(record, snapshot) {
463
+ await this.requireOwnerAloneWriter(record, snapshot);
464
+ if (!this.#authority.canReshare(record)) {
465
+ throw await this.#authority.denialFor(record, `Cannot change permissions on "${record.id}": only its author or the stack owner can reshare it`, snapshot);
466
+ }
467
+ }
468
+ /**
469
+ * Refuse a write moving content its family reserves to the owner acting
470
+ * alone — an `_app` card's bindings, the owner's own DID on an `_entity`
471
+ * — whichever verb carries it. See docs/spec/identity.md § DID bindings.
472
+ */
473
+ requireOwnerFields(typeId, before, after) {
474
+ if (this.#authority.ownerActingAlone)
475
+ return;
476
+ const refusal = ownerFieldRefusal(typeId, before, after, this.stack.ownerEntityId);
477
+ if (refusal)
478
+ throw new StackPermissionError(refusal);
479
+ }
480
+ /**
481
+ * A delegated write's `appId` must match the `_app` card the owner
482
+ * registered for the principal's DID, if there is one: a verified
483
+ * principal may not claim a name the owner gave other software.
484
+ * See docs/spec/identity.md § Attribution and what can be trusted.
485
+ */
486
+ async requireAppIdMatchesPrincipal(appId) {
487
+ const principal = this.#authority.principalId;
488
+ if (appId === undefined || !this.#authority.delegated || !principal)
489
+ return;
490
+ const card = await findAppCardByDid(this.stack, principal);
491
+ if (card && card.content.appId !== appId) {
492
+ throw new StackPermissionError(`appId "${appId}" is not the appId registered for this principal`);
493
+ }
494
+ }
495
+ /**
496
+ * Refuse anyone but the owner acting alone a write to a family only the
497
+ * owner writes — `_grant` and `_install`, which are authority — with the
498
+ * refusal an unreadable Record answers with. Any other family passes.
499
+ * See docs/spec/access-control.md § What a grant covers.
500
+ */
501
+ async requireOwnerAloneWriter(record, snapshot) {
502
+ if (familyPolicy(record.typeId).writers !== 'owner-alone')
503
+ return;
504
+ if (this.#authority.ownerActingAlone)
505
+ return;
506
+ throw await this.#authority.denialFor(record, `Only the stack owner may write a ${baseIdOf(record.typeId)} record`, snapshot);
507
+ }
508
+ /**
509
+ * Purge is owner-only: it is irreversible and destroys version
510
+ * history, so neither the write bit nor delete-own/delete-any grants
511
+ * reach it, and delegation doesn't carry it either. Everyone else is
512
+ * limited to soft delete.
513
+ */
514
+ async delete(id, opts = {}) {
515
+ const record = await this.requireDeletable(id, opts, this.#authority.snapshot());
516
+ if (opts.purge && !this.#authority.ownerActingAlone) {
517
+ throw new StackPermissionError('Purge is owner-only');
518
+ }
519
+ // Checked against the read above, which the delete is pinned to.
520
+ // Forwarded, it would cost a purge the read `Stack` otherwise skips.
521
+ const { [WRITE_EXPECTATION]: expectation, ...rest } = opts;
522
+ const result = await this.stack.delete(id, {
523
+ ...rest,
524
+ ...(expectation && { ifVersion: opts.ifVersion ?? record.version }),
525
+ ...this.actor,
526
+ });
527
+ // A soft delete's record reaches the caller as every scoped read
528
+ // presents one; a purge is the owner's, and reports what it destroyed.
529
+ // See docs/spec/versioning.md § The tombstone is literal.
530
+ return opts.purge || !result.record
531
+ ? result
532
+ : { ...result, record: presentDeleted(result.record) };
533
+ }
534
+ /**
535
+ * Reverse a soft delete. Gated the same as delete() — undelete is the
536
+ * inverse of soft delete, so granting one direction without the other
537
+ * would be backwards. Idempotent, per Stack.undelete().
538
+ */
539
+ async undelete(id, opts = {}) {
540
+ await this.requireDeletable(id, opts, this.#authority.snapshot());
541
+ return this.stack.undelete(id, { ...opts, ...this.actor });
542
+ }
543
+ /**
544
+ * History is the mutation/recovery surface, not a read surface — gated
545
+ * like patchContent(). Reading it changes nothing, so it is the one path
546
+ * the `_grant` write fence leaves alone: seeing how a Record you can
547
+ * already read got that way is not the escalation that fence exists to
548
+ * stop. See docs/spec/versioning.md § History access.
549
+ */
550
+ async getVersions(id, query = {}) {
551
+ await this.requireUpdatable(id, { mutating: false }, this.#authority.snapshot());
552
+ return this.stack.getVersions(id, query);
553
+ }
554
+ /**
555
+ * See getVersions() — the same gate. A requester who may not reshare the
556
+ * record is served each `reshare` op without the elements beneath it.
557
+ * See docs/spec/journal.md § Reading it.
558
+ */
559
+ async getJournal(id, query = {}) {
560
+ const record = await this.requireUpdatable(id, { mutating: false }, this.#authority.snapshot());
561
+ const entries = await this.stack.getJournal(id, query);
562
+ return this.#authority.canReshare(record) ? entries : entries.map(withoutAuthorityChanges);
563
+ }
564
+ /** See getVersions() — the same mutate-surface gate. */
565
+ async getVersion(id, version) {
566
+ await this.requireUpdatable(id, { mutating: false }, this.#authority.snapshot());
567
+ return this.stack.getVersion(id, version);
568
+ }
569
+ /**
570
+ * Re-runs the reference-creation checks against the snapshot, so a
571
+ * restore can't re-convey access to a file the subject can no longer
572
+ * reach today. Only the owner acting alone is exempt: under delegation
573
+ * the checks resolve against the subject, which is whose reach the
574
+ * restore would widen. See docs/spec/versioning.md § Restore semantics.
575
+ */
576
+ async restoreVersion(id, version, opts = {}) {
577
+ const snapshot = this.#authority.snapshot();
578
+ const record = await this.requireUpdatable(id, { expect: opts }, snapshot);
579
+ if (!this.#authority.ownerActingAlone) {
580
+ const target = await this.stack.getVersion(id, version);
581
+ if (target) {
582
+ // A rollback moving a reserved value is the same trust decision a
583
+ // content patch reserves to the owner, reached by another route.
584
+ this.requireOwnerFields(record.typeId, record.content, target.content);
585
+ await this.requireFileRefAccess(target.typeId, target.content, snapshot);
586
+ // Content's file refs are the only reference a restore can put
587
+ // back: containment and associations are not on a snapshot at all,
588
+ // so a restore never introduces either — see
589
+ // docs/spec/versioning.md § Restore semantics.
590
+ }
591
+ }
592
+ return this.stack.restoreVersion(id, version, { ...opts, ...this.actor });
593
+ }
594
+ /**
595
+ * Commit a per-record migration: the owner acting alone, or an installed
596
+ * app within its own families (requireInstallMigration()). Ordinary write
597
+ * access is not consent to move a record between families.
598
+ * See docs/spec/access-control.md § What a grant covers.
599
+ */
600
+ async commitMigration(id, toTypeId, content, opts = {}) {
601
+ if (!this.#authority.ownerActingAlone) {
602
+ const snapshot = this.#authority.snapshot();
603
+ const record = await this.requireInstallMigration(id, toTypeId, snapshot);
604
+ this.requireOwnerFields(toTypeId, record.content, content);
605
+ // The new content is a fresh set of file references, so an app is held
606
+ // to the gate create() applies; otherwise a migration could point one
607
+ // of its records at any attachment and read the bytes through it.
608
+ await this.requireFileRefAccess(toTypeId, content, snapshot);
609
+ }
610
+ return this.stack.commitMigration(id, toTypeId, content, { ...opts, ...this.actor });
611
+ }
612
+ /**
613
+ * Refuse a migration this request may not commit as an installed app —
614
+ * see ScopeAuthority.installMayMigrate(). The tombstone refusal comes
615
+ * only once authority is settled, as every mutating verb asks it: the
616
+ * owner may migrate a tombstone; an app sees it without content.
617
+ * See docs/spec/apps.md § Migrating an installed app's types.
618
+ */
619
+ async requireInstallMigration(id, toTypeId, snapshot) {
620
+ const record = await this.stack.get(id, { includeDeleted: true });
621
+ if (!record || !(await this.#authority.installMayMigrate(record, toTypeId, snapshot))) {
622
+ throw new StackPermissionError('Only the stack owner, or an installed app within the type families it defines, may commit a migration');
623
+ }
624
+ if (record.deletedAt) {
625
+ throw new StackConflictError(`Record "${id}" is soft-deleted; undelete it before migrating it.`);
626
+ }
627
+ return record;
628
+ }
629
+ /**
630
+ * Store bytes and create an _attachment@1 metadata record (create grant
631
+ * on `_attachment@1` required; anonymous denied), returning that record.
632
+ * Authorship and principal are stamped exactly as create() does.
633
+ */
634
+ async putAttachment(data, opts) {
635
+ const { appId } = opts;
636
+ // The one ScopedStack path that reaches the adapter without going
637
+ // through Stack first — without this, a closed stack would still write
638
+ // bytes before the delegated create() refused.
639
+ this.assertStackOpen();
640
+ const principal = this.#authority.principalId;
641
+ if (!principal) {
642
+ throw new StackPermissionError('Anonymous requesters cannot upload attachments');
643
+ }
644
+ await this.requireCreateGrant(`${SYSTEM_TYPES.ATTACHMENT}@1`, this.#authority.snapshot());
645
+ await this.requireAppIdMatchesPrincipal(appId);
646
+ assertAttachmentSize(data.byteLength, this.capabilities.limits.attachmentBytes);
647
+ const fileId = await this.adapter.putBlob(data);
648
+ return this.stack.create(`${SYSTEM_TYPES.ATTACHMENT}@1`, uploadContent(fileId, data, opts), { createdBy: this.#authority.requester, appId });
649
+ }
650
+ /**
651
+ * Download attachment bytes. Accessible to the owner, a reader of any
652
+ * referencing record, or the uploader pre-association — the same
653
+ * predicate as the reference-creation gate (canAccessFile).
654
+ */
655
+ async getAttachment(fileId) {
656
+ if (!(await this.#authority.canAccessFile(fileId, this.#authority.snapshot()))) {
657
+ throw new StackPermissionError(`Cannot read file "${fileId}": it does not exist or you cannot read it`);
658
+ }
659
+ return this.stack.getAttachment(fileId);
660
+ }
661
+ /**
662
+ * Delete an attachment. Only the stack owner may delete attachments.
663
+ * Delegates to Stack.deleteAttachment(), which enforces the "not referenced" check.
664
+ */
665
+ async deleteAttachment(fileId) {
666
+ this.requireOwnerActingAlone('Only the stack owner can delete attachments');
667
+ return this.stack.deleteAttachment(fileId, this.actor);
668
+ }
669
+ /**
670
+ * Sweep for unreferenced attachment bytes and delete them. Only the stack
671
+ * owner may run this. Delegates to Stack.collectAttachmentGarbage().
672
+ */
673
+ async collectAttachmentGarbage(opts) {
674
+ this.requireOwnerActingAlone('Only the stack owner can collect attachment garbage');
675
+ return this.stack.collectAttachmentGarbage({ ...opts, ...this.actor });
676
+ }
677
+ /**
678
+ * Observe the changes this request may read, decided per event by the
679
+ * same read reach that get() and query() ask. An unreadable record yields
680
+ * no event at all, since a change's existence is itself a disclosure.
681
+ * See docs/spec/events.md § Permission scoping.
682
+ */
683
+ async subscribe(handler, opts = {}) {
684
+ this.assertStackOpen();
685
+ // A scoped view exists only over a stack that relays nothing, so
686
+ // `since` never has a cursor to mean anything by.
687
+ assertSinceUsable(opts.since, false);
688
+ assertValidBaseIdFilter(opts.filter);
689
+ if (opts.filter?.includeUnlisted && !this.#authority.ownerActingAlone) {
690
+ throw new StackPermissionError('includeUnlisted is owner-only');
691
+ }
692
+ return this.changes.add(new ScopedSubscription(this.#authority.snapshot(), (record, snapshot) => this.#authority.canRead(record, snapshot), handler, opts));
693
+ }
694
+ }
695
+ //# sourceMappingURL=scoped-stack.js.map