ocel 0.0.1-alpha.1 → 0.0.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 (271) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +2 -0
  3. package/README.md +98 -0
  4. package/dist/binding/binding.d.ts +22 -0
  5. package/dist/binding/binding.js +60 -0
  6. package/dist/binding/unprovisioned.d.ts +17 -0
  7. package/dist/binding/unprovisioned.js +25 -0
  8. package/dist/bucket/bucket-client.d.ts +4 -0
  9. package/dist/bucket/bucket-client.js +5 -0
  10. package/dist/bucket/bucket-context.d.ts +8 -0
  11. package/dist/bucket/bucket-context.js +10 -0
  12. package/dist/bucket/bucket.d.ts +60 -0
  13. package/dist/bucket/bucket.js +91 -0
  14. package/dist/bucket/client.d.ts +49 -0
  15. package/dist/bucket/client.js +95 -0
  16. package/dist/bucket/errors.d.ts +12 -0
  17. package/dist/bucket/errors.js +20 -0
  18. package/dist/bucket/express.d.ts +10 -0
  19. package/dist/bucket/express.js +24 -0
  20. package/dist/bucket/hono.d.ts +18 -0
  21. package/dist/bucket/hono.js +13 -0
  22. package/dist/bucket/index.d.ts +8 -0
  23. package/dist/bucket/index.js +6 -0
  24. package/dist/bucket/keys.d.ts +5 -0
  25. package/dist/bucket/keys.js +24 -0
  26. package/dist/bucket/metadata.d.ts +6 -0
  27. package/dist/bucket/metadata.js +6 -0
  28. package/dist/bucket/next.d.ts +15 -0
  29. package/dist/bucket/next.js +11 -0
  30. package/dist/bucket/objects.d.ts +130 -0
  31. package/dist/bucket/objects.js +350 -0
  32. package/dist/bucket/route.d.ts +13 -0
  33. package/dist/bucket/route.js +247 -0
  34. package/dist/bucket/server-only.js +5 -0
  35. package/dist/bucket/types.d.ts +63 -0
  36. package/dist/bucket/uploader.d.ts +3 -0
  37. package/dist/bucket/uploader.js +6 -0
  38. package/dist/build-env.d.ts +17 -0
  39. package/dist/build-env.js +41 -0
  40. package/dist/config.d.ts +30 -0
  41. package/dist/config.js +9 -0
  42. package/dist/declaration/callsite.d.ts +15 -0
  43. package/dist/declaration/callsite.js +43 -0
  44. package/dist/declaration/component.d.ts +4 -0
  45. package/dist/declaration/component.js +1 -0
  46. package/dist/declaration/defer.d.ts +5 -0
  47. package/dist/declaration/defer.js +9 -0
  48. package/dist/delivery/duration.d.ts +13 -0
  49. package/dist/delivery/duration.js +36 -0
  50. package/dist/delivery/lane.d.ts +4 -0
  51. package/dist/delivery/lane.js +15 -0
  52. package/dist/delivery/payload.d.ts +6 -0
  53. package/dist/delivery/payload.js +14 -0
  54. package/dist/delivery/retry.d.ts +16 -0
  55. package/dist/delivery/retry.js +10 -0
  56. package/dist/delivery/schema.d.ts +10 -0
  57. package/dist/delivery/schema.js +20 -0
  58. package/dist/dns.d.ts +11 -0
  59. package/dist/dns.js +4 -0
  60. package/dist/edge.d.ts +4 -0
  61. package/dist/edge.js +4 -0
  62. package/dist/env/access.d.ts +22 -0
  63. package/dist/env/access.js +49 -0
  64. package/dist/env/client.d.ts +5 -0
  65. package/dist/env/client.js +10 -0
  66. package/dist/env/declare.d.ts +2 -0
  67. package/dist/env/declare.js +83 -0
  68. package/dist/env/definition.d.ts +46 -0
  69. package/dist/env/definition.js +164 -0
  70. package/dist/env/deployment.d.ts +23 -0
  71. package/dist/env/deployment.js +13 -0
  72. package/dist/env/edge.d.ts +10 -0
  73. package/dist/env/edge.js +30 -0
  74. package/dist/env/errors.d.ts +9 -0
  75. package/dist/env/errors.js +9 -0
  76. package/dist/env/file.d.ts +1 -0
  77. package/dist/env/file.js +27 -0
  78. package/dist/env/index.d.ts +10 -0
  79. package/dist/env/index.js +63 -0
  80. package/dist/env/live.d.ts +3 -0
  81. package/dist/env/live.js +12 -0
  82. package/dist/env/schema.d.ts +27 -0
  83. package/dist/env/schema.js +43 -0
  84. package/dist/env/scope.d.ts +7 -0
  85. package/dist/env/scope.js +45 -0
  86. package/dist/env/standard.d.ts +10 -0
  87. package/dist/env/standard.js +21 -0
  88. package/dist/env/value.d.ts +5 -0
  89. package/dist/env/value.js +26 -0
  90. package/dist/env-source.d.ts +17 -0
  91. package/dist/env-source.js +16 -0
  92. package/dist/gen/proto/app/bucket/v1/bucket_pb.d.ts +911 -0
  93. package/dist/gen/proto/app/bucket/v1/bucket_pb.js +253 -0
  94. package/dist/gen/proto/app/realtime/v1/realtime_pb.d.ts +50 -0
  95. package/dist/gen/proto/app/realtime/v1/realtime_pb.js +23 -0
  96. package/dist/gen/proto/app/resources/v1/resources_pb.d.ts +554 -0
  97. package/dist/gen/proto/app/resources/v1/resources_pb.js +214 -0
  98. package/dist/gen/proto/app/resources/v1/variables_pb.d.ts +231 -0
  99. package/dist/gen/proto/app/resources/v1/variables_pb.js +101 -0
  100. package/dist/gen/proto/app/task/v1/task_pb.d.ts +498 -0
  101. package/dist/gen/proto/app/task/v1/task_pb.js +151 -0
  102. package/dist/gen/proto/app/topic/v1/topic_pb.d.ts +456 -0
  103. package/dist/gen/proto/app/topic/v1/topic_pb.js +125 -0
  104. package/dist/gen/proto/buf/validate/validate_pb.d.ts +4833 -0
  105. package/dist/gen/proto/buf/validate/validate_pb.js +356 -0
  106. package/dist/gen/proto/common/bindings/v1/bindings_pb.d.ts +378 -0
  107. package/dist/gen/proto/common/bindings/v1/bindings_pb.js +140 -0
  108. package/dist/generated/config.d.ts +428 -0
  109. package/dist/generated/config.js +2 -0
  110. package/dist/kv/entries.d.ts +193 -0
  111. package/dist/kv/entries.js +325 -0
  112. package/dist/kv/errors.d.ts +12 -0
  113. package/dist/kv/errors.js +14 -0
  114. package/dist/kv/index.d.ts +6 -0
  115. package/dist/kv/index.js +3 -0
  116. package/dist/kv/ioredis.d.ts +2 -0
  117. package/dist/kv/ioredis.js +20 -0
  118. package/dist/kv/kv.d.ts +84 -0
  119. package/dist/kv/kv.js +177 -0
  120. package/dist/kv/pattern.d.ts +27 -0
  121. package/dist/kv/pattern.js +73 -0
  122. package/dist/kv/ttl.d.ts +22 -0
  123. package/dist/kv/ttl.js +21 -0
  124. package/dist/postgres/index.d.ts +21 -2
  125. package/dist/postgres/index.js +73 -2
  126. package/dist/postgres/pg.d.ts +5 -4
  127. package/dist/postgres/pg.js +16 -12
  128. package/dist/providers/aws/dns.d.ts +11 -0
  129. package/dist/providers/aws/dns.js +4 -0
  130. package/dist/providers/aws/edge.d.ts +6 -0
  131. package/dist/providers/aws/edge.js +8 -0
  132. package/dist/providers/aws/index.d.ts +4 -0
  133. package/dist/providers/aws/index.js +4 -0
  134. package/dist/providers/gcp/edge.d.ts +14 -0
  135. package/dist/providers/gcp/edge.js +14 -0
  136. package/dist/providers/gcp/index.d.ts +9 -0
  137. package/dist/providers/gcp/index.js +9 -0
  138. package/dist/providers/vps/index.d.ts +4 -0
  139. package/dist/providers/vps/index.js +4 -0
  140. package/dist/realtime/client-error.d.ts +30 -0
  141. package/dist/realtime/client-error.js +17 -0
  142. package/dist/realtime/client-event-socket.d.ts +8 -0
  143. package/dist/realtime/client-event-socket.js +169 -0
  144. package/dist/realtime/client-transport.d.ts +29 -0
  145. package/dist/realtime/client-transport.js +13 -0
  146. package/dist/realtime/client.d.ts +150 -0
  147. package/dist/realtime/client.js +345 -0
  148. package/dist/realtime/express.d.ts +18 -0
  149. package/dist/realtime/express.js +83 -0
  150. package/dist/realtime/handler.d.ts +56 -0
  151. package/dist/realtime/handler.js +287 -0
  152. package/dist/realtime/hono.d.ts +19 -0
  153. package/dist/realtime/hono.js +24 -0
  154. package/dist/realtime/index.d.ts +2 -0
  155. package/dist/realtime/index.js +2 -0
  156. package/dist/realtime/next.d.ts +26 -0
  157. package/dist/realtime/next.js +16 -0
  158. package/dist/realtime/realtime.d.ts +181 -0
  159. package/dist/realtime/realtime.js +205 -0
  160. package/dist/realtime/token.d.ts +20 -0
  161. package/dist/realtime/token.js +45 -0
  162. package/dist/realtime/transport.d.ts +11 -0
  163. package/dist/realtime/transport.js +15 -0
  164. package/dist/realtime/wire.d.ts +30 -0
  165. package/dist/realtime/wire.js +104 -0
  166. package/dist/runtime/dev-server.d.ts +8 -0
  167. package/dist/runtime/dev-server.js +8 -0
  168. package/dist/runtime/rpc.d.ts +23 -0
  169. package/dist/runtime/rpc.js +24 -0
  170. package/dist/runtime/transport.d.ts +11 -0
  171. package/dist/runtime/transport.js +35 -0
  172. package/dist/task/errors.d.ts +7 -0
  173. package/dist/task/errors.js +7 -0
  174. package/dist/task/hooks.d.ts +57 -0
  175. package/dist/task/hooks.js +1 -0
  176. package/dist/task/index.d.ts +9 -0
  177. package/dist/task/index.js +4 -0
  178. package/dist/task/runs.d.ts +74 -0
  179. package/dist/task/runs.js +82 -0
  180. package/dist/task/task.d.ts +117 -0
  181. package/dist/task/task.js +109 -0
  182. package/dist/topic/dead-letters.d.ts +53 -0
  183. package/dist/topic/dead-letters.js +62 -0
  184. package/dist/topic/index.d.ts +8 -0
  185. package/dist/topic/index.js +4 -0
  186. package/dist/topic/topic.d.ts +85 -0
  187. package/dist/topic/topic.js +117 -0
  188. package/dist/version.d.ts +2 -0
  189. package/dist/version.js +2 -0
  190. package/dist/worker/context.d.ts +36 -0
  191. package/dist/worker/context.js +1 -0
  192. package/dist/worker/deliver.d.ts +13 -0
  193. package/dist/worker/deliver.js +258 -0
  194. package/dist/worker/index.d.ts +4 -0
  195. package/dist/worker/index.js +2 -0
  196. package/dist/worker/registry.d.ts +28 -0
  197. package/dist/worker/registry.js +22 -0
  198. package/dist/worker/semaphore.d.ts +7 -0
  199. package/dist/worker/semaphore.js +36 -0
  200. package/dist/worker/worker.d.ts +21 -0
  201. package/dist/worker/worker.js +32 -0
  202. package/package.json +158 -57
  203. package/bin/run.js +0 -39
  204. package/dist/blob/bucket.d.ts +0 -55
  205. package/dist/blob/bucket.js +0 -65
  206. package/dist/blob/index.d.ts +0 -8
  207. package/dist/blob/index.js +0 -13
  208. package/dist/blob/integrations/client.d.ts +0 -25
  209. package/dist/blob/integrations/client.js +0 -71
  210. package/dist/blob/integrations/hono.d.ts +0 -31
  211. package/dist/blob/integrations/hono.js +0 -11
  212. package/dist/blob/mime-types.d.ts +0 -1
  213. package/dist/blob/parse.d.ts +0 -16
  214. package/dist/blob/parse.js +0 -69
  215. package/dist/blob/s3-listener.d.ts +0 -7
  216. package/dist/blob/s3-listener.js +0 -35
  217. package/dist/blob/types.d.ts +0 -128
  218. package/dist/blob/types.js +0 -1
  219. package/dist/blob/upload-error.d.ts +0 -35
  220. package/dist/blob/upload-error.js +0 -54
  221. package/dist/blob/uploader.d.ts +0 -67
  222. package/dist/blob/uploader.js +0 -323
  223. package/dist/internal/db/index.d.ts +0 -68
  224. package/dist/internal/db/index.js +0 -73
  225. package/dist/lambda/index.d.ts +0 -2
  226. package/dist/lambda/index.js +0 -4
  227. package/dist/lambda/integrations/express.d.ts +0 -3
  228. package/dist/lambda/integrations/express.js +0 -13
  229. package/dist/lambda/integrations/hono.d.ts +0 -3
  230. package/dist/lambda/integrations/hono.js +0 -13
  231. package/dist/lambda/lambda.d.ts +0 -43
  232. package/dist/lambda/lambda.js +0 -30
  233. package/dist/utils/component.d.ts +0 -3
  234. package/dist/utils/constants.d.ts +0 -2
  235. package/dist/utils/constants.js +0 -2
  236. package/dist/utils/nanoid.d.ts +0 -1
  237. package/dist/utils/nanoid.js +0 -6
  238. package/dist/utils/rpc.d.ts +0 -11
  239. package/dist/utils/rpc.js +0 -17
  240. package/dist/utils/stack.d.ts +0 -4
  241. package/dist/utils/stack.js +0 -23
  242. package/dist/workflow/index.d.ts +0 -3
  243. package/dist/workflow/index.js +0 -4
  244. package/dist/workflow/workflow.d.ts +0 -34
  245. package/dist/workflow/workflow.js +0 -146
  246. package/src/blob/bucket.ts +0 -92
  247. package/src/blob/index.ts +0 -34
  248. package/src/blob/integrations/client.ts +0 -127
  249. package/src/blob/integrations/hono.ts +0 -16
  250. package/src/blob/mime-types.ts +0 -68
  251. package/src/blob/parse.ts +0 -93
  252. package/src/blob/s3-listener.ts +0 -48
  253. package/src/blob/types.ts +0 -142
  254. package/src/blob/upload-error.ts +0 -67
  255. package/src/blob/uploader.ts +0 -426
  256. package/src/internal/db/index.ts +0 -78
  257. package/src/lambda/index.ts +0 -8
  258. package/src/lambda/integrations/express.ts +0 -25
  259. package/src/lambda/integrations/hono.ts +0 -25
  260. package/src/lambda/lambda.ts +0 -94
  261. package/src/postgres/index.ts +0 -5
  262. package/src/postgres/pg.ts +0 -33
  263. package/src/utils/component.ts +0 -3
  264. package/src/utils/constants.ts +0 -2
  265. package/src/utils/nanoid.ts +0 -9
  266. package/src/utils/rpc.ts +0 -28
  267. package/src/utils/stack.ts +0 -26
  268. package/src/workflow/index.ts +0 -9
  269. package/src/workflow/workflow.ts +0 -235
  270. /package/dist/{blob/mime-types.js → bucket/server-only.d.ts} +0 -0
  271. /package/dist/{utils/component.js → bucket/types.js} +0 -0
@@ -0,0 +1,350 @@
1
+ import { Code, ConnectError } from "@connectrpc/connect";
2
+ import { SignedAudience, SignedOperation, } from "../gen/proto/app/bucket/v1/bucket_pb.js";
3
+ import { ObjectNotFoundError, PreconditionFailedError } from "./errors.js";
4
+ const singleRequestCeiling = 16 * 1024 * 1024;
5
+ const partSize = 8 * 1024 * 1024;
6
+ const partsInFlight = 4;
7
+ function info(wire, key) {
8
+ return {
9
+ key: wire?.key || key,
10
+ size: Number(wire?.size ?? 0n),
11
+ etag: wire?.etag ?? "",
12
+ contentType: wire?.contentType ?? "",
13
+ uploadedAt: wire?.uploadedAt ? timestampDate(wire.uploadedAt) : undefined,
14
+ metadata: wire?.metadata ?? {},
15
+ };
16
+ }
17
+ function timestampDate(ts) {
18
+ return new Date(Number(ts.seconds) * 1000 + Math.floor(ts.nanos / 1_000_000));
19
+ }
20
+ function refused(key, status) {
21
+ if (status === 404)
22
+ return new ObjectNotFoundError(key);
23
+ if (status === 412 || status === 409)
24
+ return new PreconditionFailedError(key);
25
+ return undefined;
26
+ }
27
+ function wireRefusal(key, error) {
28
+ if (error instanceof ConnectError) {
29
+ if (error.code === Code.NotFound)
30
+ return new ObjectNotFoundError(key);
31
+ if (error.code === Code.FailedPrecondition)
32
+ return new PreconditionFailedError(key);
33
+ }
34
+ if (typeof error === "object" && error !== null && "code" in error) {
35
+ const code = error.code;
36
+ if (code === Code.NotFound)
37
+ return new ObjectNotFoundError(key);
38
+ if (code === Code.FailedPrecondition)
39
+ return new PreconditionFailedError(key);
40
+ }
41
+ return error instanceof Error ? error : new Error(String(error));
42
+ }
43
+ async function sized(body) {
44
+ if (typeof body === "string")
45
+ return { bytes: new TextEncoder().encode(body) };
46
+ if (body instanceof Uint8Array)
47
+ return { bytes: body };
48
+ if (typeof Blob !== "undefined" && body instanceof Blob) {
49
+ return body.size <= singleRequestCeiling
50
+ ? { bytes: new Uint8Array(await body.arrayBuffer()) }
51
+ : { stream: body.stream() };
52
+ }
53
+ return { stream: body };
54
+ }
55
+ async function* chunked(stream, size) {
56
+ const reader = stream.getReader();
57
+ let buffered = [];
58
+ let length = 0;
59
+ try {
60
+ for (;;) {
61
+ const { done, value } = await reader.read();
62
+ if (value) {
63
+ buffered.push(value);
64
+ length += value.byteLength;
65
+ }
66
+ while (length >= size) {
67
+ const joined = concat(buffered, length);
68
+ yield joined.subarray(0, size);
69
+ const rest = joined.subarray(size);
70
+ buffered = rest.byteLength > 0 ? [rest] : [];
71
+ length = rest.byteLength;
72
+ }
73
+ if (done)
74
+ break;
75
+ }
76
+ if (length > 0)
77
+ yield concat(buffered, length);
78
+ }
79
+ finally {
80
+ await reader.cancel().catch(() => { });
81
+ }
82
+ }
83
+ function concat(chunks, length) {
84
+ if (chunks.length === 1)
85
+ return chunks[0];
86
+ const out = new Uint8Array(length);
87
+ let at = 0;
88
+ for (const chunk of chunks) {
89
+ out.set(chunk, at);
90
+ at += chunk.byteLength;
91
+ }
92
+ return out;
93
+ }
94
+ /** Builds the object operations against a resolved runtime context. */
95
+ export function createObjects(deps) {
96
+ const send = deps.fetch ?? ((...args) => globalThis.fetch(...args));
97
+ const sign = async (key, operation, audience, constraints) => {
98
+ const { client, bucket } = deps.runtime();
99
+ const res = await client.sign({
100
+ bucket,
101
+ key,
102
+ operation,
103
+ audience,
104
+ expiresIn: constraints?.expiresIn
105
+ ? { seconds: BigInt(constraints.expiresIn), nanos: 0 }
106
+ : undefined,
107
+ constraints: {
108
+ contentType: constraints?.contentType ?? "",
109
+ maxSize: BigInt(constraints?.maxSize ?? 0),
110
+ downloadFilename: constraints?.downloadFilename ?? "",
111
+ ifNoneMatch: constraints?.ifNoneMatch ?? "",
112
+ ifMatch: constraints?.ifMatch ?? "",
113
+ cacheControl: constraints?.cacheControl ?? "",
114
+ metadata: constraints?.metadata ?? {},
115
+ },
116
+ });
117
+ const target = res.target;
118
+ if (!target)
119
+ throw new Error(`the runtime signed nothing for "${key}"`);
120
+ return target;
121
+ };
122
+ const putSingle = async (key, bytes, options) => {
123
+ const target = await sign(key, SignedOperation.PUT, SignedAudience.INTERNAL, {
124
+ contentType: options.contentType,
125
+ ifNoneMatch: options.ifNoneMatch,
126
+ ifMatch: options.ifMatch,
127
+ cacheControl: options.cacheControl,
128
+ metadata: options.metadata,
129
+ });
130
+ const headers = { ...target.headers };
131
+ if (options.contentType)
132
+ headers["content-type"] = options.contentType;
133
+ if (options.cacheControl)
134
+ headers["cache-control"] = options.cacheControl;
135
+ if (options.ifNoneMatch)
136
+ headers["if-none-match"] = options.ifNoneMatch;
137
+ if (options.ifMatch)
138
+ headers["if-match"] = options.ifMatch;
139
+ const res = await send(target.url, {
140
+ method: target.method || "PUT",
141
+ body: bytes,
142
+ headers,
143
+ signal: options.signal,
144
+ });
145
+ if (!res.ok) {
146
+ throw refused(key, res.status) ?? new Error(`writing "${key}" was refused (${res.status})`);
147
+ }
148
+ };
149
+ const putMultipart = async (key, parts, options) => {
150
+ const { client, bucket } = deps.runtime();
151
+ const { uploadId } = await client.createMultipart({
152
+ bucket,
153
+ key,
154
+ contentType: options.contentType ?? "",
155
+ cacheControl: options.cacheControl ?? "",
156
+ metadata: options.metadata ?? {},
157
+ });
158
+ const completed = [];
159
+ const inFlight = new AbortController();
160
+ const giveUp = () => inFlight.abort();
161
+ options.signal?.addEventListener("abort", giveUp);
162
+ let next = 1;
163
+ try {
164
+ for (;;) {
165
+ const batch = [];
166
+ while (batch.length < partsInFlight) {
167
+ const { done, value } = await parts.next();
168
+ if (done)
169
+ break;
170
+ batch.push({ partNumber: next++, bytes: value });
171
+ }
172
+ if (batch.length === 0)
173
+ break;
174
+ const signed = await client.signParts({
175
+ bucket,
176
+ key,
177
+ uploadId,
178
+ partNumbers: batch.map((part) => part.partNumber),
179
+ audience: SignedAudience.INTERNAL,
180
+ });
181
+ const byNumber = new Map(signed.parts.map((part) => [part.partNumber, part]));
182
+ await Promise.all(batch.map(async (part) => {
183
+ const target = byNumber.get(part.partNumber);
184
+ if (!target)
185
+ throw new Error(`the runtime signed no url for part ${part.partNumber}`);
186
+ const res = await send(target.url, {
187
+ method: "PUT",
188
+ body: part.bytes,
189
+ headers: { ...target.headers },
190
+ signal: inFlight.signal,
191
+ });
192
+ if (!res.ok) {
193
+ throw new Error(`part ${part.partNumber} of "${key}" was refused (${res.status})`);
194
+ }
195
+ completed.push({
196
+ partNumber: part.partNumber,
197
+ etag: res.headers.get("etag") ?? "",
198
+ });
199
+ }));
200
+ }
201
+ completed.sort((a, b) => a.partNumber - b.partNumber);
202
+ await client.completeMultipart({
203
+ bucket,
204
+ key,
205
+ uploadId,
206
+ parts: completed,
207
+ ifNoneMatch: options.ifNoneMatch ?? "",
208
+ ifMatch: options.ifMatch ?? "",
209
+ });
210
+ }
211
+ catch (error) {
212
+ inFlight.abort();
213
+ await parts.return(undefined).catch(() => { });
214
+ await client.abortMultipart({ bucket, key, uploadId }).catch(() => { });
215
+ throw wireRefusal(key, error);
216
+ }
217
+ finally {
218
+ options.signal?.removeEventListener("abort", giveUp);
219
+ }
220
+ };
221
+ const head = async (key) => {
222
+ const { client, bucket } = deps.runtime();
223
+ const res = await client.head({ bucket, key });
224
+ return res.object ? info(res.object, key) : null;
225
+ };
226
+ return {
227
+ async put(key, body, options = {}) {
228
+ const source = await sized(body);
229
+ if (source.bytes && source.bytes.byteLength <= singleRequestCeiling) {
230
+ await putSingle(key, source.bytes, options);
231
+ }
232
+ else {
233
+ const stream = source.stream ??
234
+ new ReadableStream({
235
+ start(controller) {
236
+ controller.enqueue(source.bytes);
237
+ controller.close();
238
+ },
239
+ });
240
+ await putMultipart(key, chunked(stream, partSize), options);
241
+ }
242
+ const stored = await head(key);
243
+ if (!stored)
244
+ throw new ObjectNotFoundError(key);
245
+ return stored;
246
+ },
247
+ async get(key, options = {}) {
248
+ const stored = await head(key);
249
+ if (!stored)
250
+ return null;
251
+ const target = await sign(key, SignedOperation.GET, SignedAudience.INTERNAL);
252
+ const headers = {};
253
+ if (options.range) {
254
+ const { offset, length } = options.range;
255
+ headers.range = `bytes=${offset}-${length === undefined ? "" : offset + length - 1}`;
256
+ }
257
+ const res = await send(target.url, { headers });
258
+ if (res.status === 404)
259
+ return null;
260
+ if (!res.ok)
261
+ throw new Error(`reading "${key}" was refused (${res.status})`);
262
+ const body = res.body;
263
+ return {
264
+ info: stored,
265
+ body,
266
+ text: () => new Response(body).text(),
267
+ json: () => new Response(body).json(),
268
+ bytes: async () => new Uint8Array(await new Response(body).arrayBuffer()),
269
+ };
270
+ },
271
+ head,
272
+ async delete(key) {
273
+ const { client, bucket } = deps.runtime();
274
+ const keys = Array.isArray(key) ? key : [key];
275
+ if (keys.length === 0)
276
+ return;
277
+ await client.delete({ bucket, keys });
278
+ },
279
+ list(options = {}) {
280
+ const page = async ({ cursor } = {}) => {
281
+ const { client, bucket } = deps.runtime();
282
+ const res = await client.list({
283
+ bucket,
284
+ prefix: options.prefix ?? "",
285
+ limit: options.limit ?? 0,
286
+ cursor: cursor ?? "",
287
+ });
288
+ return {
289
+ objects: res.objects.map((entry) => info(entry, entry.key)),
290
+ cursor: res.nextCursor,
291
+ };
292
+ };
293
+ return {
294
+ page,
295
+ async *[Symbol.asyncIterator]() {
296
+ let cursor;
297
+ for (;;) {
298
+ const listed = await page({ cursor });
299
+ yield* listed.objects;
300
+ if (!listed.cursor)
301
+ return;
302
+ cursor = listed.cursor;
303
+ }
304
+ },
305
+ };
306
+ },
307
+ async copy(source, destination) {
308
+ const { client, bucket } = deps.runtime();
309
+ try {
310
+ const res = await client.copy({ bucket, sourceKey: source, destinationKey: destination });
311
+ return info(res.object, destination);
312
+ }
313
+ catch (error) {
314
+ throw wireRefusal(source, error);
315
+ }
316
+ },
317
+ async signedUrl(key, options = {}) {
318
+ const target = await sign(key, SignedOperation.GET, SignedAudience.EXTERNAL, {
319
+ expiresIn: options.expiresIn,
320
+ downloadFilename: typeof options.download === "string"
321
+ ? options.download
322
+ : options.download
323
+ ? key.split("/").pop()
324
+ : undefined,
325
+ });
326
+ return target.url;
327
+ },
328
+ async signedUpload(key, options = {}) {
329
+ const target = await sign(key, SignedOperation.POST_UPLOAD, SignedAudience.EXTERNAL, {
330
+ expiresIn: options.expiresIn,
331
+ maxSize: options.maxSize,
332
+ contentType: options.contentType,
333
+ });
334
+ return {
335
+ url: target.url,
336
+ method: target.method || "POST",
337
+ headers: target.headers,
338
+ fields: target.fields,
339
+ };
340
+ },
341
+ publicUrl(key) {
342
+ const { publicBaseUrl } = deps.runtime();
343
+ if (!publicBaseUrl) {
344
+ throw new Error(`this bucket has no public address, so "${key}" has no public url: declare the bucket with \`public: true\` and give the project a domain to serve it from`);
345
+ }
346
+ const path = key.split("/").map(encodeURIComponent).join("/");
347
+ return `${publicBaseUrl.replace(/\/$/, "")}/${path}`;
348
+ },
349
+ };
350
+ }
@@ -0,0 +1,13 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import type { Bucket } from "./bucket.js";
3
+ import { type BucketContext } from "./bucket-context.js";
4
+ import type { UploadRequest } from "./types.js";
5
+ export type RouteRequest = UploadRequest | IncomingMessage;
6
+ export interface RouteOptions {
7
+ runtime?: BucketContext;
8
+ }
9
+ export interface RouteHandlers {
10
+ GET: (req: RouteRequest) => Promise<Response>;
11
+ POST: (req: RouteRequest, middlewareReq?: unknown) => Promise<Response>;
12
+ }
13
+ export declare function createRouteHandler(bucket: Bucket, options?: RouteOptions): RouteHandlers;
@@ -0,0 +1,247 @@
1
+ import { z } from "zod";
2
+ import { UploadState } from "../gen/proto/app/bucket/v1/bucket_pb.js";
3
+ import { resolveBucketContext } from "./bucket-context.js";
4
+ import { generateKey } from "./keys.js";
5
+ import { decodeMetadata, encodeMetadata } from "./metadata.js";
6
+ function isWebRequest(req) {
7
+ return typeof req.json === "function";
8
+ }
9
+ function headerOf(req, name) {
10
+ const headers = req.headers;
11
+ if (typeof headers.get === "function") {
12
+ return headers.get(name);
13
+ }
14
+ const value = headers[name.toLowerCase()];
15
+ if (Array.isArray(value))
16
+ return value[0] ?? null;
17
+ return value ?? null;
18
+ }
19
+ function requestUrl(req) {
20
+ const raw = req.url ?? "/";
21
+ if (/^https?:\/\//i.test(raw))
22
+ return raw;
23
+ const proto = headerOf(req, "x-forwarded-proto") ?? "http";
24
+ const host = headerOf(req, "host") ?? "localhost";
25
+ return `${proto}://${host}${raw}`;
26
+ }
27
+ async function requestJson(req) {
28
+ if (isWebRequest(req))
29
+ return req.json();
30
+ const node = req;
31
+ if (node.body !== undefined)
32
+ return node.body;
33
+ if (node.readableEnded || node.complete)
34
+ return {};
35
+ const chunks = [];
36
+ for await (const chunk of node)
37
+ chunks.push(chunk);
38
+ const text = Buffer.concat(chunks).toString("utf8");
39
+ return text ? JSON.parse(text) : {};
40
+ }
41
+ const presignBody = z.object({
42
+ uploader: z.string(),
43
+ files: z.array(z.object({
44
+ name: z.string(),
45
+ size: z.number().int().nonnegative(),
46
+ mimeType: z.string(),
47
+ })),
48
+ input: z.unknown().optional(),
49
+ });
50
+ const completeBody = z.object({
51
+ sessionId: z.string().min(1),
52
+ });
53
+ const callbackBody = z.object({
54
+ sessionId: z.string(),
55
+ signature: z.string(),
56
+ file: z.object({
57
+ key: z.string(),
58
+ name: z.string(),
59
+ size: z.number().int().nonnegative(),
60
+ mimeType: z.string(),
61
+ }),
62
+ });
63
+ function json(body, status = 200) {
64
+ return new Response(JSON.stringify(body), {
65
+ status,
66
+ headers: { "content-type": "application/json" },
67
+ });
68
+ }
69
+ function resolveLimit(value, metadata) {
70
+ if (typeof value === "function") {
71
+ return value({ metadata });
72
+ }
73
+ return value;
74
+ }
75
+ function mimeMatches(patterns, mimeType) {
76
+ return patterns.some((p) => {
77
+ if (p === "*/*" || p === "*")
78
+ return true;
79
+ if (p.endsWith("/*"))
80
+ return mimeType.startsWith(`${p.slice(0, -1)}`);
81
+ return p === mimeType;
82
+ });
83
+ }
84
+ function validateFiles(up, files, metadata) {
85
+ const maxCount = resolveLimit(up.upload.limits?.maxFileCount, metadata);
86
+ const minCount = resolveLimit(up.upload.limits?.minFileCount, metadata);
87
+ const maxSize = resolveLimit(up.upload.limits?.maxFileSize, metadata);
88
+ if (files.length === 0)
89
+ return "no files provided";
90
+ if (maxCount !== undefined && files.length > maxCount) {
91
+ return `too many files (max ${maxCount})`;
92
+ }
93
+ if (minCount !== undefined && files.length < minCount) {
94
+ return `too few files (min ${minCount})`;
95
+ }
96
+ for (const file of files) {
97
+ if (up.upload.accept && !mimeMatches(up.upload.accept, file.mimeType)) {
98
+ return `file type '${file.mimeType}' is not accepted`;
99
+ }
100
+ if (maxSize !== undefined && file.size > maxSize) {
101
+ return `file '${file.name}' exceeds max size ${maxSize}`;
102
+ }
103
+ }
104
+ return undefined;
105
+ }
106
+ function deriveCallbackBaseUrl(req) {
107
+ const u = new URL(requestUrl(req));
108
+ return `${u.origin}${u.pathname}`;
109
+ }
110
+ function opOf(req) {
111
+ return new URL(requestUrl(req)).searchParams.get("op");
112
+ }
113
+ function stateToString(state) {
114
+ switch (state) {
115
+ case UploadState.SUCCEEDED:
116
+ return "succeeded";
117
+ case UploadState.EXPIRED:
118
+ return "expired";
119
+ default:
120
+ return "pending";
121
+ }
122
+ }
123
+ async function handlePresign(bucket, ctx, req, middlewareReq) {
124
+ const parsed = presignBody.safeParse(await requestJson(req));
125
+ if (!parsed.success)
126
+ return json({ error: "invalid presign request" }, 400);
127
+ const up = bucket.uploaders[parsed.data.uploader];
128
+ if (!up) {
129
+ return json({ error: `unknown uploader '${parsed.data.uploader}'` }, 404);
130
+ }
131
+ let input;
132
+ if (up.auth.input) {
133
+ const validated = up.auth.input.safeParse(parsed.data.input);
134
+ if (!validated.success)
135
+ return json({ error: "invalid input" }, 400);
136
+ input = validated.data;
137
+ }
138
+ let metadata;
139
+ try {
140
+ metadata = await up.auth.middleware({ req: middlewareReq, input });
141
+ }
142
+ catch (err) {
143
+ return json({ error: errorMessage(err, "unauthorized") }, 401);
144
+ }
145
+ const files = parsed.data.files;
146
+ const invalid = validateFiles(up, files, metadata);
147
+ if (invalid)
148
+ return json({ error: invalid }, 400);
149
+ const presignFiles = files.map((file) => ({
150
+ key: generateKey(up.upload.path, { file, metadata }),
151
+ name: file.name,
152
+ size: BigInt(file.size),
153
+ mimeType: file.mimeType,
154
+ }));
155
+ const res = await ctx.client.presignUpload({
156
+ bucket: ctx.bucket,
157
+ files: presignFiles,
158
+ metadata: encodeMetadata({ uploader: parsed.data.uploader, metadata }),
159
+ contentDisposition: up.upload.contentDisposition ?? "",
160
+ callbackBaseUrl: deriveCallbackBaseUrl(req),
161
+ });
162
+ return json({
163
+ sessionId: res.sessionId,
164
+ files: res.files.map((t) => ({
165
+ url: t.url,
166
+ key: t.key,
167
+ name: t.name,
168
+ contentDisposition: t.contentDisposition || undefined,
169
+ headers: t.headers && Object.keys(t.headers).length > 0 ? t.headers : undefined,
170
+ method: t.method || undefined,
171
+ fields: t.fields && Object.keys(t.fields).length > 0 ? t.fields : undefined,
172
+ })),
173
+ });
174
+ }
175
+ async function handleCallback(bucket, ctx, req) {
176
+ const parsed = callbackBody.safeParse(await requestJson(req));
177
+ if (!parsed.success)
178
+ return json({ error: "invalid callback request" }, 400);
179
+ const { sessionId, signature, file } = parsed.data;
180
+ const verify = await ctx.client.verifyUploadSignature({
181
+ sessionId,
182
+ signature,
183
+ file: {
184
+ key: file.key,
185
+ name: file.name,
186
+ size: BigInt(file.size),
187
+ mimeType: file.mimeType,
188
+ },
189
+ });
190
+ if (!verify.valid)
191
+ return json({ error: "invalid signature" }, 401);
192
+ const envelope = decodeMetadata(verify.metadata);
193
+ const up = bucket.uploaders[envelope.uploader];
194
+ if (!up)
195
+ return json({ error: `unknown uploader "${envelope.uploader}"` }, 404);
196
+ const completed = {
197
+ key: file.key,
198
+ name: file.name,
199
+ size: file.size,
200
+ mimeType: file.mimeType,
201
+ path: file.key,
202
+ };
203
+ await up.upload.onUploadComplete?.({
204
+ metadata: envelope.metadata,
205
+ file: completed,
206
+ });
207
+ return json({ ok: true });
208
+ }
209
+ async function handleComplete(ctx, sessionId) {
210
+ if (!sessionId)
211
+ return json({ error: "missing sessionId" }, 400);
212
+ const res = await ctx.client.completeUpload({ sessionId });
213
+ return json({
214
+ state: stateToString(res.state),
215
+ error: res.error || undefined,
216
+ });
217
+ }
218
+ function sessionOf(req) {
219
+ return new URL(requestUrl(req)).searchParams.get("sessionId");
220
+ }
221
+ function errorMessage(err, fallback) {
222
+ return err instanceof Error && err.message ? err.message : fallback;
223
+ }
224
+ export function createRouteHandler(bucket, options = {}) {
225
+ let ctx = options.runtime;
226
+ const getCtx = () => (ctx ??= resolveBucketContext(bucket));
227
+ async function POST(req, middlewareReq) {
228
+ const op = opOf(req);
229
+ if (op === "presign") {
230
+ return handlePresign(bucket, getCtx(), req, middlewareReq ?? req);
231
+ }
232
+ if (op === "callback")
233
+ return handleCallback(bucket, getCtx(), req);
234
+ if (op === "complete") {
235
+ const body = completeBody.safeParse(await requestJson(req));
236
+ return handleComplete(getCtx(), body.success ? body.data.sessionId : sessionOf(req));
237
+ }
238
+ return json({ error: `unknown op '${op}'` }, 400);
239
+ }
240
+ async function GET(req) {
241
+ const op = opOf(req);
242
+ if (op === "poll" || op === "complete")
243
+ return handleComplete(getCtx(), sessionOf(req));
244
+ return json({ error: `unknown op '${op}'` }, 400);
245
+ }
246
+ return { GET, POST };
247
+ }
@@ -0,0 +1,5 @@
1
+ const host = globalThis;
2
+ if (host.window !== undefined && host.window.document !== undefined) {
3
+ throw new Error("ocel/bucket/next reaches this deployment's runtime with its session token, so it may only be imported by server code. Import ocel/bucket/client from a Client Component instead.");
4
+ }
5
+ export {};
@@ -0,0 +1,63 @@
1
+ import type { z } from "zod";
2
+ export type MaybePromise<T> = T | Promise<T>;
3
+ export type UploadStatusState = "pending" | "succeeded" | "expired";
4
+ export interface UploadRequest {
5
+ readonly url: string;
6
+ readonly headers: {
7
+ get(name: string): string | null;
8
+ };
9
+ json(): Promise<unknown>;
10
+ }
11
+ export interface FileInfo {
12
+ name: string;
13
+ size: number;
14
+ mimeType: string;
15
+ }
16
+ export interface CompletedFile {
17
+ key: string;
18
+ name: string;
19
+ size: number;
20
+ mimeType: string;
21
+ path: string;
22
+ }
23
+ export type ParsedInput<TInput> = TInput extends z.ZodType ? z.infer<TInput> : undefined;
24
+ export type LimitValue<TMetadata, T> = T | ((ctx: {
25
+ metadata: TMetadata;
26
+ }) => T);
27
+ export interface Limits<TMetadata> {
28
+ maxFileSize?: LimitValue<TMetadata, number>;
29
+ maxFileCount?: LimitValue<TMetadata, number>;
30
+ minFileCount?: LimitValue<TMetadata, number>;
31
+ }
32
+ export interface StructuredPath {
33
+ prefix?: string;
34
+ randomSuffix?: boolean;
35
+ }
36
+ export type PathFn<TMetadata> = (ctx: {
37
+ file: FileInfo;
38
+ metadata: TMetadata;
39
+ }) => string;
40
+ export type PathConfig<TMetadata> = StructuredPath | PathFn<TMetadata>;
41
+ export interface UploaderAuth<TReq, TInput extends z.ZodType | undefined, TMetadata> {
42
+ input?: TInput;
43
+ middleware: (ctx: {
44
+ req: TReq;
45
+ input: ParsedInput<TInput>;
46
+ }) => MaybePromise<TMetadata>;
47
+ }
48
+ export interface UploaderUpload<TMetadata> {
49
+ accept?: string[];
50
+ limits?: Limits<TMetadata>;
51
+ path?: PathConfig<TMetadata>;
52
+ contentDisposition?: string;
53
+ onUploadComplete?: (ctx: {
54
+ metadata: TMetadata;
55
+ file: CompletedFile;
56
+ }) => MaybePromise<void>;
57
+ }
58
+ export interface Uploader<TInputParsed = unknown, TMetadata = unknown, TReq = UploadRequest> {
59
+ readonly auth: UploaderAuth<TReq, z.ZodType | undefined, TMetadata>;
60
+ readonly upload: UploaderUpload<TMetadata>;
61
+ readonly __input?: TInputParsed;
62
+ }
63
+ export type AnyUploader = Uploader<any, any, any>;
@@ -0,0 +1,3 @@
1
+ import type { z } from "zod";
2
+ import type { ParsedInput, Uploader, UploaderAuth, UploaderUpload, UploadRequest } from "./types.js";
3
+ export declare function uploader<TInput extends z.ZodType | undefined = undefined, TMetadata = unknown, TReq = UploadRequest>(auth: UploaderAuth<TReq, TInput, TMetadata>, upload?: UploaderUpload<TMetadata>): Uploader<ParsedInput<TInput>, TMetadata, TReq>;
@@ -0,0 +1,6 @@
1
+ export function uploader(auth, upload = {}) {
2
+ return {
3
+ auth: auth,
4
+ upload,
5
+ };
6
+ }