@monoflake/sdk 0.0.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 (44) hide show
  1. package/LICENSE +22 -0
  2. package/dist/artifacts/src/anchors.d.ts +34 -0
  3. package/dist/artifacts/src/anchors.js +64 -0
  4. package/dist/artifacts/src/api.d.ts +14 -0
  5. package/dist/artifacts/src/api.js +20 -0
  6. package/dist/artifacts/src/batch.d.ts +105 -0
  7. package/dist/artifacts/src/batch.js +81 -0
  8. package/dist/artifacts/src/engagement.d.ts +61 -0
  9. package/dist/artifacts/src/engagement.js +67 -0
  10. package/dist/artifacts/src/feed.d.ts +42 -0
  11. package/dist/artifacts/src/feed.js +89 -0
  12. package/dist/artifacts/src/index.d.ts +4224 -0
  13. package/dist/artifacts/src/index.js +219 -0
  14. package/dist/artifacts/src/picture.d.ts +116 -0
  15. package/dist/artifacts/src/picture.js +161 -0
  16. package/dist/artifacts/src/resource.d.ts +1391 -0
  17. package/dist/artifacts/src/resource.js +477 -0
  18. package/dist/artifacts/src/schema.d.ts +5 -0
  19. package/dist/artifacts/src/schema.js +18 -0
  20. package/dist/artifacts/src/types.d.ts +396 -0
  21. package/dist/artifacts/src/types.js +0 -0
  22. package/dist/cache/src/index.d.ts +67 -0
  23. package/dist/cache/src/index.js +58 -0
  24. package/dist/imgsrc/src/index.d.ts +16 -0
  25. package/dist/imgsrc/src/index.js +89 -0
  26. package/dist/limits/src/bucket.d.ts +28 -0
  27. package/dist/limits/src/bucket.js +23 -0
  28. package/dist/limits/src/index.d.ts +29 -0
  29. package/dist/limits/src/index.js +62 -0
  30. package/dist/limits/src/key.d.ts +37 -0
  31. package/dist/limits/src/key.js +64 -0
  32. package/dist/robots/src/index.d.ts +98 -0
  33. package/dist/robots/src/index.js +196 -0
  34. package/dist/security/src/agents.d.ts +10 -0
  35. package/dist/security/src/agents.js +95 -0
  36. package/dist/security/src/index.d.ts +12 -0
  37. package/dist/security/src/index.js +37 -0
  38. package/dist/src/index.d.ts +218 -0
  39. package/dist/src/index.js +219 -0
  40. package/dist/store/src/index.d.ts +92 -0
  41. package/dist/store/src/index.js +264 -0
  42. package/dist/symlink/src/index.d.ts +24 -0
  43. package/dist/symlink/src/index.js +89 -0
  44. package/package.json +85 -0
@@ -0,0 +1,264 @@
1
+ import { recordKey, storageKey } from "../../artifacts/src/index.js";
2
+ import { errorBody } from "@canmi/response";
3
+ //#region store/src/index.ts
4
+ /**
5
+ * Reading the bytes behind a key, from whichever store this deployment has.
6
+ *
7
+ * Production reads the R2 bucket a tree under `data/bucket` mirrors; development reads that
8
+ * tree itself, handed over by `wrangler dev --assets` because it is the source of truth.
9
+ * A worker cannot open that directory itself -- workerd's `node:fs` is virtual and cannot see
10
+ * host paths (verified) -- so the runtime passes it in. Everything above this module works in
11
+ * keys and knows nothing about which one answered.
12
+ */
13
+ /**
14
+ * Origin for asset-fetcher requests. `.invalid` is reserved by RFC 2606 to never resolve,
15
+ * which is the point: the fetcher routes on the path and ignores the host, and a name that
16
+ * cannot resolve makes it impossible for this to accidentally become a real request.
17
+ */
18
+ const ASSET_ORIGIN = "https://assets.invalid";
19
+ function isUnsatisfiable(value) {
20
+ return value !== null && "unsatisfiable" in value;
21
+ }
22
+ async function read(env, key, range) {
23
+ const wanted = range ? parseRange(range) : null;
24
+ if (env.STORE) return readFromBucket(env.STORE, key, wanted);
25
+ if (env.ASSETS) return readFromAssets(env.ASSETS, key, wanted);
26
+ throw new Error("no store bound: expected STORE in production or ASSETS under wrangler dev");
27
+ }
28
+ /**
29
+ * What is known about an object under a key, without reading a byte of it.
30
+ *
31
+ * A caller that has to refuse work before paying for it needs the size first: an isolate gets
32
+ * 128 MB for its heap and its WebAssembly together, so a length that arrives alongside the bytes
33
+ * arrives too late to be a limit. Null is no such object, which makes this a presence check too.
34
+ * The date rides along because the same head already carries it, and a caller wanting both
35
+ * should not spend a second round trip on the second one.
36
+ */
37
+ async function measure(env, key) {
38
+ if (env.STORE) {
39
+ const head = await env.STORE.head(key);
40
+ return head ? {
41
+ size: head.size,
42
+ uploaded: head.uploaded ?? null
43
+ } : null;
44
+ }
45
+ if (env.ASSETS) {
46
+ const response = await env.ASSETS.fetch(`${ASSET_ORIGIN}/${key}`);
47
+ if (!response.ok) return null;
48
+ const uploaded = dateOf(response.headers.get("last-modified"));
49
+ const declared = response.headers.get("content-length");
50
+ if (declared !== null) {
51
+ await response.body?.cancel();
52
+ return {
53
+ size: Number(declared),
54
+ uploaded
55
+ };
56
+ }
57
+ return {
58
+ size: (await response.arrayBuffer()).byteLength,
59
+ uploaded
60
+ };
61
+ }
62
+ throw new Error("no store bound: expected STORE in production or ASSETS under wrangler dev");
63
+ }
64
+ /** A header date, or null for absent and for unparseable, which are one thing to a caller. */
65
+ function dateOf(header) {
66
+ if (header === null) return null;
67
+ const at = new Date(header);
68
+ return Number.isNaN(at.getTime()) ? null : at;
69
+ }
70
+ /**
71
+ * `bytes=a-b`, `bytes=a-` and `bytes=-n`, or null for anything else.
72
+ *
73
+ * Null covers a unit that is not `bytes`, a list of ranges, and a header that is simply
74
+ * malformed. All three are served whole, which is what a recipient is allowed to do and what a
75
+ * client asking for something this does not implement should get.
76
+ */
77
+ function parseRange(header) {
78
+ const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
79
+ if (!match) return null;
80
+ const [, from, to] = match;
81
+ if (from === "") return to === "" ? null : { suffix: Number(to) };
82
+ const offset = Number(from);
83
+ if (to === "") return {
84
+ offset,
85
+ end: null
86
+ };
87
+ const end = Number(to);
88
+ return end < offset ? null : {
89
+ offset,
90
+ end
91
+ };
92
+ }
93
+ /** Resolve a wanted range against the object's real size. */
94
+ function resolve(wanted, total) {
95
+ if ("suffix" in wanted) {
96
+ const length = Math.min(wanted.suffix, total);
97
+ return length === 0 ? null : {
98
+ offset: total - length,
99
+ length
100
+ };
101
+ }
102
+ if (wanted.offset >= total) return null;
103
+ const end = wanted.end === null ? total - 1 : Math.min(wanted.end, total - 1);
104
+ return {
105
+ offset: wanted.offset,
106
+ length: end - wanted.offset + 1
107
+ };
108
+ }
109
+ async function readFromBucket(bucket, key, wanted) {
110
+ if (!wanted) {
111
+ const object = await bucket.get(key);
112
+ if (!object?.body) return null;
113
+ return {
114
+ body: object.body,
115
+ contentType: object.httpMetadata?.contentType ?? contentTypeFor(key),
116
+ etag: object.httpEtag
117
+ };
118
+ }
119
+ const head = await bucket.head(key);
120
+ if (!head) return null;
121
+ const resolved = resolve(wanted, head.size);
122
+ if (!resolved) return {
123
+ unsatisfiable: true,
124
+ total: head.size
125
+ };
126
+ const object = await bucket.get(key, { range: resolved });
127
+ if (!object?.body) return null;
128
+ return {
129
+ body: object.body,
130
+ contentType: object.httpMetadata?.contentType ?? contentTypeFor(key),
131
+ etag: object.httpEtag,
132
+ partial: {
133
+ ...resolved,
134
+ total: head.size
135
+ }
136
+ };
137
+ }
138
+ async function readFromAssets(assets, key, wanted) {
139
+ const response = await assets.fetch(`${ASSET_ORIGIN}/${key}`);
140
+ if (!response.ok || !response.body) return null;
141
+ const contentType = response.headers.get("content-type") ?? contentTypeFor(key);
142
+ if (!wanted) return {
143
+ body: response.body,
144
+ contentType
145
+ };
146
+ const whole = new Uint8Array(await response.arrayBuffer());
147
+ const resolved = resolve(wanted, whole.byteLength);
148
+ if (!resolved) return {
149
+ unsatisfiable: true,
150
+ total: whole.byteLength
151
+ };
152
+ return {
153
+ body: streamOf(whole.subarray(resolved.offset, resolved.offset + resolved.length)),
154
+ contentType,
155
+ partial: {
156
+ ...resolved,
157
+ total: whole.byteLength
158
+ }
159
+ };
160
+ }
161
+ /**
162
+ * What a content id looks like.
163
+ *
164
+ * BLAKE3 truncated to 128 bits, hex encoded. Checked before a key is built from one, because an
165
+ * id becomes a path segment and an unchecked one is a way to ask the bucket for something else.
166
+ */
167
+ const CONTENT_ID = /^[0-9a-f]{32}$/;
168
+ function isContentId(value) {
169
+ return CONTENT_ID.test(value);
170
+ }
171
+ /**
172
+ * A stored object as an HTTP response, with ETag only when the store supplied one.
173
+ *
174
+ * `Accept-Ranges` on every one of them, because every object here is served through a route that
175
+ * can answer a range -- a player seeking, a `<video>` element probing for duration, a resumed
176
+ * download. The header is what tells a client it may ask; without it a browser fetches whole
177
+ * files to read a byte near the end of them.
178
+ */
179
+ function toResponse(found) {
180
+ const headers = new Headers({
181
+ "Content-Type": found.contentType,
182
+ "Accept-Ranges": "bytes"
183
+ });
184
+ if (found.etag) headers.set("ETag", found.etag);
185
+ if (!found.partial) return new Response(found.body, { headers });
186
+ const { offset, length, total } = found.partial;
187
+ headers.set("Content-Range", `bytes ${offset}-${offset + length - 1}/${total}`);
188
+ headers.set("Content-Length", String(length));
189
+ return new Response(found.body, {
190
+ status: 206,
191
+ headers
192
+ });
193
+ }
194
+ /**
195
+ * The answer to a range that names nothing inside the object.
196
+ *
197
+ * 416 carries the size so the client can ask again knowing it, which is the whole reason this is
198
+ * not a 404: the object is there, the question was wrong.
199
+ */
200
+ function unsatisfiableResponse(total) {
201
+ return Response.json(errorBody("invalid_range"), {
202
+ status: 416,
203
+ headers: {
204
+ "Content-Range": `bytes */${total}`,
205
+ "Accept-Ranges": "bytes"
206
+ }
207
+ });
208
+ }
209
+ /**
210
+ * Bytes already in hand, answered as a `Range` asked for them.
211
+ *
212
+ * For a body that was derived rather than stored: there is no object to ask the bucket for a part
213
+ * of, so the artifact is produced whole and the slice is taken here. The grammar stays in this
214
+ * module -- one parser, one set of spellings -- and a header it does not implement is served
215
+ * whole, which is what a recipient is allowed to do. See spec/architecture/delivery.md.
216
+ */
217
+ function rangedResponse(bytes, contentType, range) {
218
+ const wanted = range ? parseRange(range) : null;
219
+ if (!wanted) return toResponse({
220
+ body: streamOf(bytes),
221
+ contentType
222
+ });
223
+ const total = bytes.byteLength;
224
+ const resolved = resolve(wanted, total);
225
+ if (!resolved) return unsatisfiableResponse(total);
226
+ return toResponse({
227
+ body: streamOf(bytes.subarray(resolved.offset, resolved.offset + resolved.length)),
228
+ contentType,
229
+ partial: {
230
+ ...resolved,
231
+ total
232
+ }
233
+ });
234
+ }
235
+ /** Bytes as a body. The cast is the workers runtime's stream against the DOM's declaration. */
236
+ function streamOf(bytes) {
237
+ return new Response(bytes).body;
238
+ }
239
+ /**
240
+ * Content type from the key, for objects stored without one.
241
+ *
242
+ * R2 keeps whatever `httpMetadata` was set at upload, and rclone does set it, but an object
243
+ * put by hand through the dashboard has none. Serving those as `application/octet-stream`
244
+ * makes a browser download a favicon instead of drawing it.
245
+ */
246
+ function contentTypeFor(key) {
247
+ switch (key.split(".").pop()?.toLowerCase() ?? "") {
248
+ case "avif": return "image/avif";
249
+ case "svg": return "image/svg+xml";
250
+ case "png": return "image/png";
251
+ case "jpg":
252
+ case "jpeg": return "image/jpeg";
253
+ case "ico": return "image/x-icon";
254
+ case "mp4": return "video/mp4";
255
+ case "vtt": return "text/vtt";
256
+ case "woff2": return "font/woff2";
257
+ case "json": return "application/json";
258
+ case "txt": return "text/plain; charset=utf-8";
259
+ case "md": return "text/markdown; charset=utf-8";
260
+ default: return "application/octet-stream";
261
+ }
262
+ }
263
+ //#endregion
264
+ export { contentTypeFor, isContentId, isUnsatisfiable, measure, rangedResponse, read, recordKey, storageKey, toResponse, unsatisfiableResponse };
@@ -0,0 +1,24 @@
1
+ //#region symlink/src/index.d.ts
2
+ /**
3
+ * Ask `name` -- a `/symlink/...` address on the alias layer -- and answer with where it points:
4
+ * a `302` there while it resolves, a `404` while the corpus names nothing, a `502` otherwise.
5
+ * Each is stamped as the alias layer stamps its own.
6
+ */
7
+ export declare function followSymlink(name: string, fetcher?: typeof fetch): Promise<Response>;
8
+ /** Where a scope's file is named, `symlink` being where the alias layer answers fixed names. */
9
+ export declare function symlinkOf(symlink: string, scope: string, file: string): string;
10
+ /**
11
+ * The address `name` stands for right now, or `undefined` while it resolves to nothing. For a page
12
+ * that writes the object into its markup rather than redirecting to it; a failure is not kept.
13
+ */
14
+ export declare function resolveSymlink(name: string, fetcher?: typeof fetch): Promise<string | undefined>;
15
+ /** Each of `files` in `scope`, resolved together: what a head names its marks by. */
16
+ export declare function marksOf<File extends string>(symlink: string, scope: string, files: readonly File[], fetcher?: typeof fetch): Promise<Partial<Record<File, string>>>;
17
+ /**
18
+ * The bytes `name` stands for, answered from the asking origin rather than redirected to: for a
19
+ * file a browser uses only from the document's own origin, such as a sitemap's XSL stylesheet. A
20
+ * miss and a failure are answered as `followSymlink` answers them; the bytes are kept as the alias
21
+ * layer keeps its redirect.
22
+ */
23
+ export declare function serveSymlink(name: string, contentType: string, fetcher?: typeof fetch): Promise<Response>;
24
+ //#endregion
@@ -0,0 +1,89 @@
1
+ import { PUBLICATION_DELAY, PUBLISHED, RESOLVED } from "../../cache/src/index.js";
2
+ //#region symlink/src/index.ts
3
+ /**
4
+ * A fixed name, followed for a browser: the page asks the alias layer what the name means and
5
+ * hands the browser that object, one hop instead of two. The mapping stays the alias layer's; a
6
+ * page knows a name and nothing behind it. See spec/architecture/delivery.md, "A page follows the
7
+ * name for the browser".
8
+ */
9
+ /** An answer about this moment rather than about the corpus, so nothing may keep it. */
10
+ const NEVER = "no-store";
11
+ function answer(status, cacheControl, location) {
12
+ const headers = new Headers({ "Cache-Control": cacheControl });
13
+ if (location) headers.set("Location", location);
14
+ return new Response(null, {
15
+ status,
16
+ headers
17
+ });
18
+ }
19
+ /**
20
+ * Ask `name` -- a `/symlink/...` address on the alias layer -- and answer with where it points:
21
+ * a `302` there while it resolves, a `404` while the corpus names nothing, a `502` otherwise.
22
+ * Each is stamped as the alias layer stamps its own.
23
+ */
24
+ async function followSymlink(name, fetcher = fetch) {
25
+ let asked;
26
+ try {
27
+ asked = await fetcher(name, { redirect: "manual" });
28
+ } catch {
29
+ return answer(502, NEVER);
30
+ }
31
+ const target = asked.headers.get("Location");
32
+ if (asked.status >= 300 && asked.status < 400 && target) return answer(302, RESOLVED, new URL(target, name).href);
33
+ if (asked.status === 404) return answer(404, PUBLISHED);
34
+ return answer(502, NEVER);
35
+ }
36
+ /** Where a scope's file is named, `symlink` being where the alias layer answers fixed names. */
37
+ function symlinkOf(symlink, scope, file) {
38
+ return `${symlink}/${scope}/${file}`;
39
+ }
40
+ /** What each name last resolved to, held as long as the alias layer says its answer may be. */
41
+ const resolved = /* @__PURE__ */ new Map();
42
+ /**
43
+ * The address `name` stands for right now, or `undefined` while it resolves to nothing. For a page
44
+ * that writes the object into its markup rather than redirecting to it; a failure is not kept.
45
+ */
46
+ async function resolveSymlink(name, fetcher = fetch) {
47
+ const held = resolved.get(name);
48
+ if (held && held.until > Date.now()) return held.target;
49
+ const answered = await followSymlink(name, fetcher);
50
+ const target = answered.headers.get("Location");
51
+ if (answered.status !== 302 || !target) return void 0;
52
+ resolved.set(name, {
53
+ target,
54
+ until: Date.now() + PUBLICATION_DELAY * 1e3
55
+ });
56
+ return target;
57
+ }
58
+ /** Each of `files` in `scope`, resolved together: what a head names its marks by. */
59
+ async function marksOf(symlink, scope, files, fetcher = fetch) {
60
+ const targets = await Promise.all(files.map((file) => resolveSymlink(symlinkOf(symlink, scope, file), fetcher)));
61
+ return Object.fromEntries(files.flatMap((file, index) => targets[index] ? [[file, targets[index]]] : []));
62
+ }
63
+ /**
64
+ * The bytes `name` stands for, answered from the asking origin rather than redirected to: for a
65
+ * file a browser uses only from the document's own origin, such as a sitemap's XSL stylesheet. A
66
+ * miss and a failure are answered as `followSymlink` answers them; the bytes are kept as the alias
67
+ * layer keeps its redirect.
68
+ */
69
+ async function serveSymlink(name, contentType, fetcher = fetch) {
70
+ const followed = await followSymlink(name, fetcher);
71
+ const target = followed.headers.get("Location");
72
+ if (followed.status !== 302 || !target) return followed;
73
+ let object;
74
+ try {
75
+ object = await fetcher(target);
76
+ } catch {
77
+ return answer(502, NEVER);
78
+ }
79
+ if (!object.ok) return answer(502, NEVER);
80
+ return new Response(object.body, {
81
+ status: 200,
82
+ headers: {
83
+ "Content-Type": contentType,
84
+ "Cache-Control": RESOLVED
85
+ }
86
+ });
87
+ }
88
+ //#endregion
89
+ export { followSymlink, marksOf, resolveSymlink, serveSymlink, symlinkOf };
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@monoflake/sdk",
3
+ "version": "0.0.0",
4
+ "description": "The platform's addresses, and every client of its services and its policies behind a subpath",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/monoflake/platform.git",
9
+ "directory": "libs/sdk"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/src/index.d.ts",
18
+ "default": "./dist/src/index.js"
19
+ },
20
+ "./artifacts": {
21
+ "types": "./dist/artifacts/src/index.d.ts",
22
+ "default": "./dist/artifacts/src/index.js"
23
+ },
24
+ "./artifacts/types": {
25
+ "types": "./dist/artifacts/src/types.d.ts",
26
+ "default": "./dist/artifacts/src/types.js"
27
+ },
28
+ "./artifacts/anchors": {
29
+ "types": "./dist/artifacts/src/anchors.d.ts",
30
+ "default": "./dist/artifacts/src/anchors.js"
31
+ },
32
+ "./cache": {
33
+ "types": "./dist/cache/src/index.d.ts",
34
+ "default": "./dist/cache/src/index.js"
35
+ },
36
+ "./imgsrc": {
37
+ "types": "./dist/imgsrc/src/index.d.ts",
38
+ "default": "./dist/imgsrc/src/index.js"
39
+ },
40
+ "./limits": {
41
+ "types": "./dist/limits/src/index.d.ts",
42
+ "default": "./dist/limits/src/index.js"
43
+ },
44
+ "./robots": {
45
+ "types": "./dist/robots/src/index.d.ts",
46
+ "default": "./dist/robots/src/index.js"
47
+ },
48
+ "./security": {
49
+ "types": "./dist/security/src/index.d.ts",
50
+ "default": "./dist/security/src/index.js"
51
+ },
52
+ "./security/agents": {
53
+ "types": "./dist/security/src/agents.d.ts",
54
+ "default": "./dist/security/src/agents.js"
55
+ },
56
+ "./store": {
57
+ "types": "./dist/store/src/index.d.ts",
58
+ "default": "./dist/store/src/index.js"
59
+ },
60
+ "./symlink": {
61
+ "types": "./dist/symlink/src/index.d.ts",
62
+ "default": "./dist/symlink/src/index.js"
63
+ }
64
+ },
65
+ "dependencies": {
66
+ "@canmi/me": "^2026.1004.1",
67
+ "@canmi/response": "^2.0.0",
68
+ "@monoflake/urls": "^2026.1004.0",
69
+ "valibot": "^1.5.0"
70
+ },
71
+ "devDependencies": {
72
+ "@cloudflare/workers-types": "^5.20261004.1"
73
+ },
74
+ "peerDependencies": {
75
+ "@cloudflare/workers-types": "^5.20261004.1"
76
+ },
77
+ "peerDependenciesMeta": {
78
+ "@cloudflare/workers-types": {
79
+ "optional": true
80
+ }
81
+ },
82
+ "scripts": {
83
+ "build": "tsdown"
84
+ }
85
+ }