@oatscenter/trellis 0.100.0-rc.1

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 (264) hide show
  1. package/README.md +59 -0
  2. package/auth/authorization/client_context.d.ts +61 -0
  3. package/auth/authorization/client_context.js +266 -0
  4. package/auth/authorization/nats_registry.d.ts +41 -0
  5. package/auth/authorization/nats_registry.js +283 -0
  6. package/auth/authorization/provider_cache.d.ts +90 -0
  7. package/auth/authorization/provider_cache.js +783 -0
  8. package/auth/authorization/refresh.d.ts +46 -0
  9. package/auth/authorization/refresh.js +246 -0
  10. package/auth/authorization/types.d.ts +207 -0
  11. package/auth/authorization/types.js +60 -0
  12. package/auth/authorization_context.d.ts +11 -0
  13. package/auth/authorization_context.js +10 -0
  14. package/auth/browser/flow_types.d.ts +84 -0
  15. package/auth/browser/flow_types.js +70 -0
  16. package/auth/browser/portal.d.ts +21 -0
  17. package/auth/browser/portal.js +307 -0
  18. package/auth/browser/storage.d.ts +26 -0
  19. package/auth/browser/storage.js +224 -0
  20. package/auth/browser.d.ts +11 -0
  21. package/auth/browser.js +10 -0
  22. package/auth/browser_recovery.d.ts +13 -0
  23. package/auth/browser_recovery.js +38 -0
  24. package/auth/device_activation.d.ts +134 -0
  25. package/auth/device_activation.js +376 -0
  26. package/auth/http_error.d.ts +12 -0
  27. package/auth/http_error.js +32 -0
  28. package/auth/keys.d.ts +5 -0
  29. package/auth/keys.js +63 -0
  30. package/auth/mod.d.ts +16 -0
  31. package/auth/mod.js +16 -0
  32. package/auth/proof.d.ts +31 -0
  33. package/auth/proof.js +95 -0
  34. package/auth/protocol_wasm/trellis_protocol_wasm.d.ts +138 -0
  35. package/auth/protocol_wasm/trellis_protocol_wasm.js +605 -0
  36. package/auth/protocol_wasm/trellis_protocol_wasm_bg.wasm +0 -0
  37. package/auth/protocol_wasm/trellis_protocol_wasm_bytes.d.ts +1 -0
  38. package/auth/protocol_wasm/trellis_protocol_wasm_bytes.js +2 -0
  39. package/auth/protocol_wasm.d.ts +179 -0
  40. package/auth/protocol_wasm.js +133 -0
  41. package/auth/session_auth.d.ts +30 -0
  42. package/auth/session_auth.js +77 -0
  43. package/auth/session_proof.d.ts +53 -0
  44. package/auth/session_proof.js +43 -0
  45. package/auth/time.d.ts +6 -0
  46. package/auth/time.js +7 -0
  47. package/auth/trellis_id.d.ts +1 -0
  48. package/auth/trellis_id.js +5 -0
  49. package/auth/utils.d.ts +10 -0
  50. package/auth/utils.js +69 -0
  51. package/auth.d.ts +1 -0
  52. package/auth.js +1 -0
  53. package/caller.d.ts +111 -0
  54. package/caller.js +85 -0
  55. package/client.d.ts +14 -0
  56. package/client.js +1 -0
  57. package/client_connect.d.ts +143 -0
  58. package/client_connect.js +1097 -0
  59. package/codec.d.ts +13 -0
  60. package/codec.js +192 -0
  61. package/connection.d.ts +107 -0
  62. package/connection.js +396 -0
  63. package/device/activation.d.ts +42 -0
  64. package/device/activation.js +319 -0
  65. package/device.d.ts +80 -0
  66. package/device.js +687 -0
  67. package/env.d.ts +2 -0
  68. package/env.js +10 -0
  69. package/errors/AuthError.d.ts +30 -0
  70. package/errors/AuthError.js +34 -0
  71. package/errors/KVError.d.ts +30 -0
  72. package/errors/KVError.js +36 -0
  73. package/errors/OperationLifecycleError.d.ts +110 -0
  74. package/errors/OperationLifecycleError.js +142 -0
  75. package/errors/RemoteError.d.ts +61 -0
  76. package/errors/RemoteError.js +72 -0
  77. package/errors/SchemaValidationError.d.ts +48 -0
  78. package/errors/SchemaValidationError.js +49 -0
  79. package/errors/StoreError.d.ts +21 -0
  80. package/errors/StoreError.js +31 -0
  81. package/errors/TransferError.d.ts +21 -0
  82. package/errors/TransferError.js +31 -0
  83. package/errors/TransportError.d.ts +29 -0
  84. package/errors/TransportError.js +33 -0
  85. package/errors/TrellisError.d.ts +15 -0
  86. package/errors/TrellisError.js +15 -0
  87. package/errors/ValidationError.d.ts +50 -0
  88. package/errors/ValidationError.js +59 -0
  89. package/errors/index.d.ts +254 -0
  90. package/errors/index.js +238 -0
  91. package/generated.d.ts +4 -0
  92. package/generated.js +3 -0
  93. package/generated_support.d.ts +120 -0
  94. package/generated_support.js +359 -0
  95. package/globals.d.ts +9 -0
  96. package/globals.js +16 -0
  97. package/health_transport.d.ts +18 -0
  98. package/health_transport.js +32 -0
  99. package/helpers.d.ts +11 -0
  100. package/helpers.js +53 -0
  101. package/index.d.ts +2 -0
  102. package/index.js +2 -0
  103. package/internal_sdk/generated/apis/auth/mod.d.ts +805 -0
  104. package/internal_sdk/generated/apis/auth/mod.js +958 -0
  105. package/internal_sdk/generated/apis/core/mod.d.ts +79 -0
  106. package/internal_sdk/generated/apis/core/mod.js +93 -0
  107. package/internal_sdk/generated/apis/events/mod.d.ts +224 -0
  108. package/internal_sdk/generated/apis/events/mod.js +313 -0
  109. package/internal_sdk/generated/apis/health/mod.d.ts +106 -0
  110. package/internal_sdk/generated/apis/health/mod.js +132 -0
  111. package/internal_sdk/generated/apis/index.d.ts +7 -0
  112. package/internal_sdk/generated/apis/index.js +8 -0
  113. package/internal_sdk/generated/apis/jobs/mod.d.ts +176 -0
  114. package/internal_sdk/generated/apis/jobs/mod.js +209 -0
  115. package/internal_sdk/generated/apis/state/mod.d.ts +104 -0
  116. package/internal_sdk/generated/apis/state/mod.js +139 -0
  117. package/internal_sdk/generated/index.d.ts +4 -0
  118. package/internal_sdk/generated/index.js +5 -0
  119. package/internal_sdk/generated/package.json +18 -0
  120. package/internal_sdk/generated/participants/cli/mod.d.ts +402 -0
  121. package/internal_sdk/generated/participants/cli/mod.js +460 -0
  122. package/internal_sdk/generated/participants/cli/types.d.ts +2 -0
  123. package/internal_sdk/generated/participants/cli/types.js +3 -0
  124. package/internal_sdk/generated/participants/console/mod.d.ts +489 -0
  125. package/internal_sdk/generated/participants/console/mod.js +579 -0
  126. package/internal_sdk/generated/participants/console/types.d.ts +2 -0
  127. package/internal_sdk/generated/participants/console/types.js +3 -0
  128. package/internal_sdk/generated/participants/eventsRuntime/mod.d.ts +64 -0
  129. package/internal_sdk/generated/participants/eventsRuntime/mod.js +42 -0
  130. package/internal_sdk/generated/participants/eventsRuntime/types.d.ts +2 -0
  131. package/internal_sdk/generated/participants/eventsRuntime/types.js +3 -0
  132. package/internal_sdk/generated/participants/healthRuntime/mod.d.ts +58 -0
  133. package/internal_sdk/generated/participants/healthRuntime/mod.js +36 -0
  134. package/internal_sdk/generated/participants/healthRuntime/types.d.ts +2 -0
  135. package/internal_sdk/generated/participants/healthRuntime/types.js +3 -0
  136. package/internal_sdk/generated/participants/index.d.ts +8 -0
  137. package/internal_sdk/generated/participants/index.js +9 -0
  138. package/internal_sdk/generated/participants/jobsRuntime/mod.d.ts +64 -0
  139. package/internal_sdk/generated/participants/jobsRuntime/mod.js +42 -0
  140. package/internal_sdk/generated/participants/jobsRuntime/types.d.ts +2 -0
  141. package/internal_sdk/generated/participants/jobsRuntime/types.js +3 -0
  142. package/internal_sdk/generated/participants/platform/mod.d.ts +164 -0
  143. package/internal_sdk/generated/participants/platform/mod.js +149 -0
  144. package/internal_sdk/generated/participants/platform/types.d.ts +7 -0
  145. package/internal_sdk/generated/participants/platform/types.js +5 -0
  146. package/internal_sdk/generated/participants/portal/mod.d.ts +131 -0
  147. package/internal_sdk/generated/participants/portal/mod.js +109 -0
  148. package/internal_sdk/generated/participants/portal/types.d.ts +2 -0
  149. package/internal_sdk/generated/participants/portal/types.js +3 -0
  150. package/internal_sdk/generated/types/_internal/p0.d.ts +13511 -0
  151. package/internal_sdk/generated/types/_internal/p0.js +6105 -0
  152. package/internal_sdk/generated/types/index.d.ts +2951 -0
  153. package/internal_sdk/generated/types/index.js +1477 -0
  154. package/jobs.d.ts +309 -0
  155. package/jobs.js +389 -0
  156. package/kv.d.ts +85 -0
  157. package/kv.js +330 -0
  158. package/operations.d.ts +189 -0
  159. package/operations.js +847 -0
  160. package/package.json +73 -0
  161. package/pagination.d.ts +25 -0
  162. package/pagination.js +117 -0
  163. package/participant.d.ts +6 -0
  164. package/participant.js +6 -0
  165. package/participant_runtime/api.d.ts +131 -0
  166. package/participant_runtime/api.js +42 -0
  167. package/participant_runtime/json.d.ts +16 -0
  168. package/participant_runtime/json.js +193 -0
  169. package/participant_runtime/metadata.d.ts +34 -0
  170. package/participant_runtime/metadata.js +4 -0
  171. package/participant_runtime/participant.d.ts +200 -0
  172. package/participant_runtime/participant.js +330 -0
  173. package/participant_runtime/schemas.d.ts +347 -0
  174. package/participant_runtime/schemas.js +211 -0
  175. package/participant_runtime/surface_names.d.ts +16 -0
  176. package/participant_runtime/surface_names.js +34 -0
  177. package/portable.d.ts +30 -0
  178. package/portable.js +14 -0
  179. package/provider.d.ts +69 -0
  180. package/provider.js +68 -0
  181. package/runtime_transport.d.ts +25 -0
  182. package/runtime_transport.js +48 -0
  183. package/service/mod.d.ts +14 -0
  184. package/service/mod.js +13 -0
  185. package/service/outbox_inbox.d.ts +259 -0
  186. package/service/outbox_inbox.js +908 -0
  187. package/service/runtime/bootstrap.d.ts +56 -0
  188. package/service/runtime/bootstrap.js +226 -0
  189. package/service/runtime/core.d.ts +46 -0
  190. package/service/runtime/core.js +1747 -0
  191. package/service/runtime/health.d.ts +97 -0
  192. package/service/runtime/health.js +153 -0
  193. package/service/runtime/health_schemas.d.ts +23 -0
  194. package/service/runtime/health_schemas.js +20 -0
  195. package/service/runtime/internal_jobs/active-job.d.ts +40 -0
  196. package/service/runtime/internal_jobs/active-job.js +91 -0
  197. package/service/runtime/internal_jobs/bindings.d.ts +40 -0
  198. package/service/runtime/internal_jobs/bindings.js +1 -0
  199. package/service/runtime/internal_jobs/cancellation-registry.d.ts +13 -0
  200. package/service/runtime/internal_jobs/cancellation-registry.js +55 -0
  201. package/service/runtime/internal_jobs/heartbeat.d.ts +33 -0
  202. package/service/runtime/internal_jobs/heartbeat.js +65 -0
  203. package/service/runtime/internal_jobs/job-manager.d.ts +126 -0
  204. package/service/runtime/internal_jobs/job-manager.js +821 -0
  205. package/service/runtime/internal_jobs/key-coordinator.d.ts +280 -0
  206. package/service/runtime/internal_jobs/key-coordinator.js +747 -0
  207. package/service/runtime/internal_jobs/projection.d.ts +3 -0
  208. package/service/runtime/internal_jobs/projection.js +42 -0
  209. package/service/runtime/internal_jobs/runtime-worker.d.ts +126 -0
  210. package/service/runtime/internal_jobs/runtime-worker.js +454 -0
  211. package/service/runtime/internal_jobs/types.d.ts +246 -0
  212. package/service/runtime/internal_jobs/types.js +163 -0
  213. package/service/runtime/logger.d.ts +2 -0
  214. package/service/runtime/logger.js +7 -0
  215. package/service/runtime/runtime.d.ts +16 -0
  216. package/service/runtime/runtime.js +1 -0
  217. package/service/runtime/service.d.ts +372 -0
  218. package/service/runtime/service.js +1896 -0
  219. package/service/runtime/transfer/download.d.ts +27 -0
  220. package/service/runtime/transfer/download.js +1 -0
  221. package/service/runtime/transfer/protocol.d.ts +15 -0
  222. package/service/runtime/transfer/protocol.js +46 -0
  223. package/service/runtime/transfer/queue.d.ts +18 -0
  224. package/service/runtime/transfer/queue.js +136 -0
  225. package/service/runtime/transfer/upload.d.ts +40 -0
  226. package/service/runtime/transfer/upload.js +27 -0
  227. package/service/runtime/transfer.d.ts +62 -0
  228. package/service/runtime/transfer.js +763 -0
  229. package/session.d.ts +862 -0
  230. package/session.js +3407 -0
  231. package/store.d.ts +71 -0
  232. package/store.js +412 -0
  233. package/store_entry.d.ts +20 -0
  234. package/store_entry.js +75 -0
  235. package/tasks.d.ts +10 -0
  236. package/tasks.js +28 -0
  237. package/telemetry/carrier.d.ts +8 -0
  238. package/telemetry/carrier.js +20 -0
  239. package/telemetry/core.d.ts +7 -0
  240. package/telemetry/core.js +14 -0
  241. package/telemetry/env.d.ts +2 -0
  242. package/telemetry/env.js +10 -0
  243. package/telemetry/init.d.ts +2 -0
  244. package/telemetry/init.js +7 -0
  245. package/telemetry/metrics.d.ts +81 -0
  246. package/telemetry/metrics.js +267 -0
  247. package/telemetry/mod.d.ts +13 -0
  248. package/telemetry/mod.js +8 -0
  249. package/telemetry/nats.d.ts +6 -0
  250. package/telemetry/nats.js +6 -0
  251. package/telemetry/result.d.ts +1 -0
  252. package/telemetry/result.js +18 -0
  253. package/telemetry/runtime.d.ts +1 -0
  254. package/telemetry/runtime.js +141 -0
  255. package/telemetry/trace.d.ts +4 -0
  256. package/telemetry/trace.js +26 -0
  257. package/telemetry/trellis.d.ts +1 -0
  258. package/telemetry/trellis.js +1 -0
  259. package/telemetry.d.ts +1 -0
  260. package/telemetry.js +1 -0
  261. package/transfer.d.ts +112 -0
  262. package/transfer.js +525 -0
  263. package/transfer_protocol.d.ts +1 -0
  264. package/transfer_protocol.js +13 -0
@@ -0,0 +1,763 @@
1
+ import { AsyncResult, isErr, Result, } from "@oatscenter/result";
2
+ import { headers as natsHeaders, } from "@nats-io/nats-core";
3
+ import { ulid } from "ulid";
4
+ import { sha256 } from "@noble/hashes/sha256";
5
+ import { base64urlEncode } from "../../auth/utils.js";
6
+ import { verifyLocalAuthorization, } from "../../session.js";
7
+ import { TransferError } from "../../errors/TransferError.js";
8
+ import { TypedStore, TypedStoreEntry } from "../../store.js";
9
+ import { transferFrameProofPayload } from "../../transfer_protocol.js";
10
+ import { AsyncChunkQueue, AsyncValueBroadcaster, deferred, } from "./transfer/queue.js";
11
+ import { DEFAULT_TRANSFER_CHUNK_BYTES, DOWNLOAD_SUBJECT_PREFIX, fileInfoFromStoreInfo, parseSeq, publishError, replyError, TRANSFER_CONTROL_HEADER, TRANSFER_EOF_HEADER, TRANSFER_SEQUENCE_HEADER, UPLOAD_SUBJECT_PREFIX, } from "./transfer/protocol.js";
12
+ import { effectiveUploadMaxBytes, raceCancellation, } from "./transfer/upload.js";
13
+ import { MAX_TRANSFER_CHUNK_BYTES } from "../../transfer.js";
14
+ export class ServiceTransfer {
15
+ #name;
16
+ #nc;
17
+ #auth;
18
+ #stores;
19
+ #chunkBytes;
20
+ #uploadSessions = new Map();
21
+ #downloadSessions = new Map();
22
+ constructor(opts) {
23
+ this.#name = opts.name;
24
+ this.#nc = opts.nc;
25
+ this.#auth = opts.auth;
26
+ this.#stores = opts.stores;
27
+ this.#chunkBytes = opts.chunkBytes ?? DEFAULT_TRANSFER_CHUNK_BYTES;
28
+ if (!Number.isSafeInteger(this.#chunkBytes) || this.#chunkBytes < 1 ||
29
+ this.#chunkBytes > MAX_TRANSFER_CHUNK_BYTES) {
30
+ throw new RangeError(`transfer chunk size must be between 1 and ${MAX_TRANSFER_CHUNK_BYTES} bytes`);
31
+ }
32
+ }
33
+ async initiateUpload(args) {
34
+ const store = await this.#openStore(args.store, "initiateUpload");
35
+ const storeValue = store.take();
36
+ if (isErr(storeValue)) {
37
+ return Result.err(storeValue.error);
38
+ }
39
+ const storeStatus = await storeValue.status();
40
+ const storeStatusValue = storeStatus.take();
41
+ if (isErr(storeStatusValue)) {
42
+ return Result.err(new TransferError({
43
+ operation: "initiateUpload",
44
+ cause: storeStatusValue.error,
45
+ }));
46
+ }
47
+ const maxBytes = effectiveUploadMaxBytes(args.maxBytes, storeStatusValue.maxObjectBytes);
48
+ const transferId = ulid();
49
+ const subject = `${UPLOAD_SUBJECT_PREFIX}.${this.#auth.sessionKey.slice(0, 16)}.${transferId}`;
50
+ const expiresAtMs = Date.now() + args.expiresInMs;
51
+ const queue = new AsyncChunkQueue();
52
+ const subscription = this.#nc.subscribe(subject);
53
+ const putPromise = storeValue.put(args.key, queue, {
54
+ ...(args.contentType ? { contentType: args.contentType } : {}),
55
+ ...(args.metadata ? { metadata: args.metadata } : {}),
56
+ });
57
+ const session = {
58
+ kind: "upload",
59
+ subject,
60
+ transferId,
61
+ sessionKey: args.sessionKey,
62
+ permission: args.permission,
63
+ requiredCapabilities: args.requiredCapabilities,
64
+ expiresAtMs,
65
+ store: storeValue,
66
+ key: args.key,
67
+ ...(maxBytes !== undefined ? { maxBytes } : {}),
68
+ ...(args.contentType ? { contentType: args.contentType } : {}),
69
+ ...(args.metadata ? { metadata: args.metadata } : {}),
70
+ ...(args.onProgress ? { onProgress: args.onProgress } : {}),
71
+ ...(args.onComplete ? { onComplete: args.onComplete } : {}),
72
+ ...(args.onError ? { onError: args.onError } : {}),
73
+ ...(args.onStored ? { onStored: args.onStored } : {}),
74
+ subscription,
75
+ timeoutId: setTimeout(() => this.#expireUploadSession(subject), args.expiresInMs),
76
+ queue,
77
+ putPromise,
78
+ cancellation: new AbortController(),
79
+ committing: false,
80
+ nextSeq: 0,
81
+ receivedBytes: 0,
82
+ hasher: sha256.create(),
83
+ };
84
+ this.#uploadSessions.set(subject, session);
85
+ this.#runUploadSession(session);
86
+ if (!this.#uploadSessions.has(subject)) {
87
+ return Result.err(new TransferError({
88
+ operation: "initiateUpload",
89
+ context: { reason: "session_closed" },
90
+ }));
91
+ }
92
+ if (Date.now() >= expiresAtMs) {
93
+ const error = new TransferError({
94
+ operation: "initiateUpload",
95
+ context: { reason: "expired" },
96
+ });
97
+ this.#expireUploadSession(subject, error);
98
+ return Result.err(error);
99
+ }
100
+ return Result.ok({
101
+ type: "TransferGrant",
102
+ direction: "send",
103
+ service: this.#name,
104
+ sessionKey: args.sessionKey,
105
+ transferId,
106
+ subject,
107
+ expiresAt: new Date(expiresAtMs).toISOString(),
108
+ chunkBytes: this.#chunkBytes,
109
+ ...(maxBytes !== undefined ? { maxBytes } : {}),
110
+ ...(args.contentType ? { contentType: args.contentType } : {}),
111
+ ...(args.metadata ? { metadata: args.metadata } : {}),
112
+ });
113
+ }
114
+ createOperationUpload(args) {
115
+ return AsyncResult.from((async () => {
116
+ const updates = new AsyncValueBroadcaster();
117
+ const completed = deferred();
118
+ let settled = false;
119
+ const settle = (value) => {
120
+ if (settled) {
121
+ return;
122
+ }
123
+ settled = true;
124
+ updates.close();
125
+ completed.resolve(value);
126
+ };
127
+ const grant = await this.initiateUpload({
128
+ ...args,
129
+ onProgress: async (progress) => {
130
+ updates.push(progress);
131
+ await args.onProgress?.(progress);
132
+ },
133
+ onComplete: async (info) => {
134
+ await args.onComplete?.(info);
135
+ settle(Result.ok(info));
136
+ },
137
+ onError: async (error) => {
138
+ await args.onError?.(error);
139
+ settle(Result.err(error));
140
+ },
141
+ onStored: async (stored) => {
142
+ await args.onStored?.(stored);
143
+ },
144
+ });
145
+ const grantValue = grant.take();
146
+ if (isErr(grantValue)) {
147
+ return Result.err(grantValue.error);
148
+ }
149
+ return Result.ok({
150
+ grant: grantValue,
151
+ transfer: {
152
+ updates: () => updates.subscribe(),
153
+ completed: () => AsyncResult.from(completed.promise),
154
+ },
155
+ });
156
+ })());
157
+ }
158
+ async initiateDownload(args) {
159
+ const store = await this.#openStore(args.store, "initiateDownload");
160
+ const storeValue = store.take();
161
+ if (isErr(storeValue)) {
162
+ return Result.err(storeValue.error);
163
+ }
164
+ const entry = await storeValue.get(args.key);
165
+ const entryValue = entry.take();
166
+ if (isErr(entryValue)) {
167
+ return Result.err(new TransferError({
168
+ operation: "initiateDownload",
169
+ cause: entryValue.error,
170
+ }));
171
+ }
172
+ const info = fileInfoFromStoreInfo(entryValue.info);
173
+ if (info.digest === undefined) {
174
+ return Result.err(new TransferError({
175
+ operation: "initiateDownload",
176
+ context: { reason: "missing_digest", key: args.key },
177
+ }));
178
+ }
179
+ const downloadInfo = { ...info, digest: info.digest };
180
+ const transferId = ulid();
181
+ const subject = `${DOWNLOAD_SUBJECT_PREFIX}.${this.#auth.sessionKey.slice(0, 16)}.${transferId}`;
182
+ const expiresAtMs = Date.now() + args.expiresInMs;
183
+ const subscription = this.#nc.subscribe(subject);
184
+ const session = {
185
+ kind: "download",
186
+ subject,
187
+ transferId,
188
+ sessionKey: args.sessionKey,
189
+ permission: args.permission,
190
+ requiredCapabilities: [...(args.requiredCapabilities ?? [])],
191
+ inboxPrefix: args.inboxPrefix,
192
+ expiresAtMs,
193
+ store: storeValue,
194
+ key: args.key,
195
+ info: downloadInfo,
196
+ subscription,
197
+ pendingOffset: 0,
198
+ nextSeq: 0,
199
+ sentBytes: 0,
200
+ hasher: sha256.create(),
201
+ cancellation: new AbortController(),
202
+ timeoutId: setTimeout(() => this.#cleanupDownloadSession(subject), args.expiresInMs),
203
+ };
204
+ this.#downloadSessions.set(subject, session);
205
+ this.#runDownloadSession(session);
206
+ if (!this.#downloadSessions.has(subject)) {
207
+ return Result.err(new TransferError({
208
+ operation: "initiateDownload",
209
+ context: { reason: "session_closed" },
210
+ }));
211
+ }
212
+ if (Date.now() >= expiresAtMs) {
213
+ this.#cleanupDownloadSession(subject);
214
+ return Result.err(new TransferError({
215
+ operation: "initiateDownload",
216
+ context: { reason: "expired" },
217
+ }));
218
+ }
219
+ return Result.ok({
220
+ type: "TransferGrant",
221
+ direction: "receive",
222
+ service: this.#name,
223
+ sessionKey: args.sessionKey,
224
+ transferId,
225
+ subject,
226
+ expiresAt: new Date(expiresAtMs).toISOString(),
227
+ chunkBytes: this.#chunkBytes,
228
+ info: downloadInfo,
229
+ });
230
+ }
231
+ async stop() {
232
+ for (const subject of [...this.#uploadSessions.keys()]) {
233
+ this.#expireUploadSession(subject);
234
+ }
235
+ for (const subject of [...this.#downloadSessions.keys()]) {
236
+ this.#cleanupDownloadSession(subject);
237
+ }
238
+ }
239
+ async #openStore(alias, operation) {
240
+ const handle = this.#stores[alias];
241
+ if (!handle) {
242
+ return Result.err(new TransferError({
243
+ operation,
244
+ context: { reason: "unknown_store", store: alias },
245
+ }));
246
+ }
247
+ const store = await handle.open();
248
+ const value = store.take();
249
+ if (isErr(value)) {
250
+ return Result.err(new TransferError({
251
+ operation,
252
+ cause: value.error,
253
+ context: { store: alias },
254
+ }));
255
+ }
256
+ return Result.ok(value);
257
+ }
258
+ async #runUploadSession(session) {
259
+ let pending;
260
+ try {
261
+ for await (const msg of session.subscription) {
262
+ const cancellation = msg.headers?.get(TRANSFER_CONTROL_HEADER) ===
263
+ "cancel";
264
+ if (pending && !cancellation) {
265
+ replyError(msg, new TransferError({
266
+ operation: "put",
267
+ context: { reason: "pending_frame" },
268
+ }));
269
+ continue;
270
+ }
271
+ const handling = this.#handleUploadMessage(session, msg);
272
+ if (pending) {
273
+ await handling;
274
+ await pending.catch(() => undefined);
275
+ break;
276
+ }
277
+ let tracked;
278
+ tracked = handling.catch((cause) => {
279
+ const error = cause instanceof TransferError
280
+ ? cause
281
+ : new TransferError({ operation: "put", cause });
282
+ replyError(msg, error);
283
+ this.#expireUploadSession(session.subject, error);
284
+ }).finally(() => {
285
+ if (pending === tracked)
286
+ pending = undefined;
287
+ });
288
+ pending = tracked;
289
+ }
290
+ }
291
+ finally {
292
+ await pending?.catch(() => undefined);
293
+ this.#cleanupUploadSession(session.subject);
294
+ }
295
+ }
296
+ async #handleUploadMessage(session, msg) {
297
+ if (Date.now() >= session.expiresAtMs) {
298
+ const error = new TransferError({
299
+ operation: "put",
300
+ context: { reason: "expired" },
301
+ });
302
+ replyError(msg, error);
303
+ this.#expireUploadSession(session.subject, error);
304
+ return;
305
+ }
306
+ const seqResult = parseSeq(msg).take();
307
+ if (isErr(seqResult)) {
308
+ replyError(msg, seqResult.error);
309
+ this.#expireUploadSession(session.subject, seqResult.error);
310
+ return;
311
+ }
312
+ const control = msg.headers?.get(TRANSFER_CONTROL_HEADER) || undefined;
313
+ const authenticated = await verifyLocalAuthorization({
314
+ kind: "request",
315
+ cache: this.#auth.authorizationProviderCache,
316
+ message: msg,
317
+ permission: session.permission,
318
+ requiredCapabilities: session.requiredCapabilities,
319
+ proofPayload: transferFrameProofPayload(seqResult, control, msg.data),
320
+ });
321
+ const authenticatedValue = authenticated.take();
322
+ if (isErr(authenticatedValue) ||
323
+ authenticatedValue.sessionKey !== session.sessionKey) {
324
+ const error = new TransferError({
325
+ operation: "put",
326
+ context: { reason: "invalid_proof" },
327
+ });
328
+ replyError(msg, error);
329
+ this.#expireUploadSession(session.subject, error);
330
+ return;
331
+ }
332
+ if (msg.headers?.get(TRANSFER_EOF_HEADER) === "true") {
333
+ const error = new TransferError({
334
+ operation: "put",
335
+ context: { reason: "legacy_eof_header" },
336
+ });
337
+ replyError(msg, error);
338
+ this.#expireUploadSession(session.subject, error);
339
+ return;
340
+ }
341
+ if (control === "cancel") {
342
+ try {
343
+ if (JSON.parse(new TextDecoder().decode(msg.data))
344
+ .action !== "cancel") {
345
+ throw new Error("cancellation payload does not match its control header");
346
+ }
347
+ if (session.committing) {
348
+ replyError(msg, new TransferError({
349
+ operation: "put",
350
+ context: { reason: "cancel_after_validated_eof" },
351
+ }));
352
+ return;
353
+ }
354
+ this.#expireUploadSession(session.subject, new TransferError({
355
+ operation: "put",
356
+ context: { reason: "cancelled" },
357
+ }));
358
+ msg.respond(JSON.stringify({ status: "cancelled" }));
359
+ return;
360
+ }
361
+ catch (cause) {
362
+ const error = new TransferError({
363
+ operation: "put",
364
+ cause,
365
+ context: { reason: "invalid_control" },
366
+ });
367
+ replyError(msg, error);
368
+ this.#expireUploadSession(session.subject, error);
369
+ return;
370
+ }
371
+ }
372
+ if (control !== undefined && control !== "complete") {
373
+ const error = new TransferError({
374
+ operation: "put",
375
+ context: { reason: "invalid_control", control },
376
+ });
377
+ replyError(msg, error);
378
+ this.#expireUploadSession(session.subject, error);
379
+ return;
380
+ }
381
+ const seq = seqResult;
382
+ if (seq !== session.nextSeq) {
383
+ const error = new TransferError({
384
+ operation: "put",
385
+ context: {
386
+ reason: "out_of_order",
387
+ expected: session.nextSeq,
388
+ actual: seq,
389
+ },
390
+ });
391
+ replyError(msg, error);
392
+ this.#expireUploadSession(session.subject, error);
393
+ return;
394
+ }
395
+ const eof = control === "complete";
396
+ if (!eof && msg.data.length > 0) {
397
+ if (msg.data.length > this.#chunkBytes) {
398
+ const error = new TransferError({
399
+ operation: "put",
400
+ context: {
401
+ reason: "chunk_too_large",
402
+ maxChunkBytes: this.#chunkBytes,
403
+ },
404
+ });
405
+ replyError(msg, error);
406
+ this.#expireUploadSession(session.subject, error);
407
+ return;
408
+ }
409
+ session.receivedBytes += msg.data.length;
410
+ if (session.maxBytes !== undefined &&
411
+ session.receivedBytes > session.maxBytes) {
412
+ const error = new TransferError({
413
+ operation: "put",
414
+ context: {
415
+ reason: "max_bytes_exceeded",
416
+ maxBytes: session.maxBytes,
417
+ attemptedBytes: session.receivedBytes,
418
+ },
419
+ });
420
+ replyError(msg, error);
421
+ this.#expireUploadSession(session.subject, error);
422
+ return;
423
+ }
424
+ await raceCancellation(session.queue.push(msg.data), session.cancellation.signal);
425
+ session.hasher.update(msg.data);
426
+ await session.onProgress?.({
427
+ chunkIndex: session.nextSeq,
428
+ chunkBytes: msg.data.length,
429
+ transferredBytes: session.receivedBytes,
430
+ });
431
+ }
432
+ session.nextSeq += 1;
433
+ if (eof) {
434
+ let completion;
435
+ try {
436
+ completion = JSON.parse(new TextDecoder().decode(msg.data));
437
+ }
438
+ catch (cause) {
439
+ const error = new TransferError({
440
+ operation: "put",
441
+ cause,
442
+ context: { reason: "invalid_completion" },
443
+ });
444
+ replyError(msg, error);
445
+ this.#expireUploadSession(session.subject, error);
446
+ return;
447
+ }
448
+ const digest = `SHA-256=${base64urlEncode(session.hasher.digest())}`;
449
+ if (completion.action !== "complete" ||
450
+ completion.size !== session.receivedBytes ||
451
+ completion.digest !== digest) {
452
+ const error = new TransferError({
453
+ operation: "put",
454
+ context: {
455
+ reason: "completion_mismatch",
456
+ expectedSize: session.receivedBytes,
457
+ actualSize: completion.size,
458
+ expectedDigest: digest,
459
+ actualDigest: completion.digest,
460
+ },
461
+ });
462
+ replyError(msg, error);
463
+ this.#expireUploadSession(session.subject, error);
464
+ return;
465
+ }
466
+ session.queue.close();
467
+ session.committing = true;
468
+ clearTimeout(session.timeoutId);
469
+ const putResult = await session.putPromise;
470
+ const putValue = putResult.take();
471
+ if (isErr(putValue)) {
472
+ const error = new TransferError({
473
+ operation: "put",
474
+ cause: putValue.error,
475
+ });
476
+ replyError(msg, error);
477
+ this.#expireUploadSession(session.subject, error);
478
+ return;
479
+ }
480
+ const stored = await session.store.get(session.key);
481
+ const storedValue = stored.take();
482
+ if (isErr(storedValue)) {
483
+ const error = new TransferError({
484
+ operation: "put",
485
+ cause: storedValue.error,
486
+ });
487
+ replyError(msg, error);
488
+ this.#expireUploadSession(session.subject, error);
489
+ return;
490
+ }
491
+ const info = fileInfoFromStoreInfo(storedValue.info);
492
+ if (info.key !== session.key || info.size !== session.receivedBytes ||
493
+ info.digest === undefined ||
494
+ info.digest.replace(/=+$/, "") !== digest.replace(/=+$/, "")) {
495
+ const error = new TransferError({
496
+ operation: "put",
497
+ context: {
498
+ reason: "stored_metadata_mismatch",
499
+ expectedKey: session.key,
500
+ actualKey: info.key,
501
+ expectedSize: session.receivedBytes,
502
+ actualSize: info.size,
503
+ expectedDigest: digest,
504
+ actualDigest: info.digest,
505
+ },
506
+ });
507
+ replyError(msg, error);
508
+ this.#expireUploadSession(session.subject, error);
509
+ return;
510
+ }
511
+ msg.respond(JSON.stringify({ status: "complete", info }));
512
+ await session.onComplete?.(info);
513
+ if (session.onStored) {
514
+ void Promise.resolve(session.onStored({
515
+ transferId: session.transferId,
516
+ sessionKey: session.sessionKey,
517
+ store: session.store,
518
+ entry: storedValue,
519
+ info,
520
+ })).catch((error) => {
521
+ console.error("transfer onStored callback failed", error);
522
+ });
523
+ }
524
+ this.#cleanupUploadSession(session.subject);
525
+ return;
526
+ }
527
+ msg.respond(JSON.stringify({ status: "continue" }));
528
+ }
529
+ async #runDownloadSession(session) {
530
+ let pending;
531
+ try {
532
+ for await (const msg of session.subscription) {
533
+ const cancellation = msg.headers?.get(TRANSFER_CONTROL_HEADER) ===
534
+ "cancel";
535
+ if (pending && !cancellation) {
536
+ replyError(msg, new TransferError({
537
+ operation: "get",
538
+ context: { reason: "pending_frame" },
539
+ }));
540
+ continue;
541
+ }
542
+ const handling = this.#handleDownloadRequest(session, msg);
543
+ if (pending) {
544
+ if (await handling)
545
+ break;
546
+ continue;
547
+ }
548
+ let tracked;
549
+ tracked = handling.then((done) => {
550
+ if (done)
551
+ this.#cleanupDownloadSession(session.subject);
552
+ return done;
553
+ }).catch(() => {
554
+ this.#cleanupDownloadSession(session.subject);
555
+ return true;
556
+ }).finally(() => {
557
+ if (pending === tracked)
558
+ pending = undefined;
559
+ });
560
+ pending = tracked;
561
+ }
562
+ }
563
+ finally {
564
+ await pending?.catch(() => undefined);
565
+ this.#cleanupDownloadSession(session.subject);
566
+ }
567
+ }
568
+ async #handleDownloadRequest(session, msg) {
569
+ const reply = msg.reply;
570
+ if (!reply || !reply.startsWith(`${session.inboxPrefix}.`)) {
571
+ replyError(msg, new TransferError({
572
+ operation: "get",
573
+ context: {
574
+ reason: "reply_subject_mismatch",
575
+ expected: session.inboxPrefix,
576
+ actual: reply,
577
+ },
578
+ }));
579
+ return false;
580
+ }
581
+ if (Date.now() >= session.expiresAtMs) {
582
+ publishError(this.#nc, reply, new TransferError({ operation: "get", context: { reason: "expired" } }));
583
+ return true;
584
+ }
585
+ const parsedSeq = parseSeq(msg).take();
586
+ if (isErr(parsedSeq)) {
587
+ replyError(msg, parsedSeq.error);
588
+ return false;
589
+ }
590
+ const control = msg.headers?.get(TRANSFER_CONTROL_HEADER) || undefined;
591
+ const authenticated = await verifyLocalAuthorization({
592
+ kind: "request",
593
+ cache: this.#auth.authorizationProviderCache,
594
+ message: msg,
595
+ permission: session.permission,
596
+ requiredCapabilities: session.requiredCapabilities,
597
+ proofPayload: transferFrameProofPayload(parsedSeq, control, msg.data),
598
+ });
599
+ const authenticatedValue = authenticated.take();
600
+ if (isErr(authenticatedValue) ||
601
+ authenticatedValue.sessionKey !== session.sessionKey) {
602
+ publishError(this.#nc, reply, new TransferError({
603
+ operation: "get",
604
+ context: { reason: "invalid_proof" },
605
+ }));
606
+ return false;
607
+ }
608
+ if (control === "cancel") {
609
+ try {
610
+ if (JSON.parse(new TextDecoder().decode(msg.data))
611
+ .action !== "cancel") {
612
+ throw new Error("cancellation payload does not match its control header");
613
+ }
614
+ session.cancellation.abort(new TransferError({
615
+ operation: "get",
616
+ context: { reason: "cancelled" },
617
+ }));
618
+ await session.reader?.cancel().catch(() => undefined);
619
+ msg.respond(JSON.stringify({ status: "cancelled" }));
620
+ return true;
621
+ }
622
+ catch (cause) {
623
+ replyError(msg, new TransferError({
624
+ operation: "get",
625
+ cause,
626
+ context: { reason: "invalid_control" },
627
+ }));
628
+ return false;
629
+ }
630
+ }
631
+ if (control !== undefined ||
632
+ msg.headers?.get(TRANSFER_EOF_HEADER) === "true") {
633
+ replyError(msg, new TransferError({
634
+ operation: "get",
635
+ context: { reason: "invalid_control", control },
636
+ }));
637
+ return false;
638
+ }
639
+ if (parsedSeq !== session.nextSeq) {
640
+ replyError(msg, new TransferError({
641
+ operation: "get",
642
+ context: {
643
+ reason: "sequence",
644
+ expected: session.nextSeq,
645
+ actual: parsedSeq,
646
+ },
647
+ }));
648
+ return false;
649
+ }
650
+ if (msg.data.length !== 0) {
651
+ replyError(msg, new TransferError({
652
+ operation: "get",
653
+ context: { reason: "invalid_control" },
654
+ }));
655
+ return false;
656
+ }
657
+ try {
658
+ if (!session.reader) {
659
+ const entryValue = (await session.store.get(session.key)).take();
660
+ if (isErr(entryValue))
661
+ throw entryValue.error;
662
+ const streamValue = (await entryValue.stream()).take();
663
+ if (isErr(streamValue))
664
+ throw streamValue.error;
665
+ session.reader = streamValue.getReader();
666
+ }
667
+ while (!session.pending || session.pendingOffset >= session.pending.length) {
668
+ const next = await raceCancellation(session.reader.read(), session.cancellation.signal);
669
+ if (next.done) {
670
+ if (session.sentBytes !== session.info.size) {
671
+ throw new TransferError({
672
+ operation: "get",
673
+ context: {
674
+ reason: "size_mismatch",
675
+ expected: session.info.size,
676
+ actual: session.sentBytes,
677
+ },
678
+ });
679
+ }
680
+ const digest = `SHA-256=${base64urlEncode(session.hasher.digest())}`;
681
+ if (session.info.digest &&
682
+ session.info.digest.replace(/=+$/, "") !== digest.replace(/=+$/, "")) {
683
+ throw new TransferError({
684
+ operation: "get",
685
+ context: {
686
+ reason: "digest_mismatch",
687
+ expected: session.info.digest,
688
+ actual: digest,
689
+ },
690
+ });
691
+ }
692
+ const headers = natsHeaders();
693
+ headers.set(TRANSFER_SEQUENCE_HEADER, String(session.nextSeq));
694
+ headers.set(TRANSFER_EOF_HEADER, "true");
695
+ msg.respond(new Uint8Array(), { headers });
696
+ return true;
697
+ }
698
+ session.pending = next.value;
699
+ session.pendingOffset = 0;
700
+ }
701
+ const chunk = session.pending.slice(session.pendingOffset, session.pendingOffset + this.#chunkBytes);
702
+ session.pendingOffset += chunk.length;
703
+ session.sentBytes += chunk.length;
704
+ if (session.sentBytes > session.info.size) {
705
+ throw new TransferError({
706
+ operation: "get",
707
+ context: {
708
+ reason: "size_mismatch",
709
+ expected: session.info.size,
710
+ actual: session.sentBytes,
711
+ },
712
+ });
713
+ }
714
+ session.hasher.update(chunk);
715
+ const headers = natsHeaders();
716
+ headers.set(TRANSFER_SEQUENCE_HEADER, String(session.nextSeq));
717
+ msg.respond(chunk, { headers });
718
+ session.nextSeq += 1;
719
+ return false;
720
+ }
721
+ catch (cause) {
722
+ replyError(msg, cause instanceof TransferError
723
+ ? cause
724
+ : new TransferError({ operation: "get", cause }));
725
+ return true;
726
+ }
727
+ }
728
+ #expireUploadSession(subject, error = new TransferError({
729
+ operation: "put",
730
+ context: { reason: "expired" },
731
+ })) {
732
+ const session = this.#uploadSessions.get(subject);
733
+ if (!session) {
734
+ return;
735
+ }
736
+ session.cancellation.abort(error);
737
+ session.queue.fail(error);
738
+ void session.onError?.(error);
739
+ void session.putPromise;
740
+ this.#cleanupUploadSession(subject);
741
+ }
742
+ #cleanupUploadSession(subject) {
743
+ const session = this.#uploadSessions.get(subject);
744
+ if (!session) {
745
+ return;
746
+ }
747
+ clearTimeout(session.timeoutId);
748
+ session.cancellation.abort();
749
+ session.subscription.unsubscribe();
750
+ this.#uploadSessions.delete(subject);
751
+ }
752
+ #cleanupDownloadSession(subject) {
753
+ const session = this.#downloadSessions.get(subject);
754
+ if (!session) {
755
+ return;
756
+ }
757
+ clearTimeout(session.timeoutId);
758
+ session.cancellation.abort();
759
+ session.subscription.unsubscribe();
760
+ void session.reader?.cancel().catch(() => undefined);
761
+ this.#downloadSessions.delete(subject);
762
+ }
763
+ }