@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.
- package/LICENSE +22 -0
- package/dist/artifacts/src/anchors.d.ts +34 -0
- package/dist/artifacts/src/anchors.js +64 -0
- package/dist/artifacts/src/api.d.ts +14 -0
- package/dist/artifacts/src/api.js +20 -0
- package/dist/artifacts/src/batch.d.ts +105 -0
- package/dist/artifacts/src/batch.js +81 -0
- package/dist/artifacts/src/engagement.d.ts +61 -0
- package/dist/artifacts/src/engagement.js +67 -0
- package/dist/artifacts/src/feed.d.ts +42 -0
- package/dist/artifacts/src/feed.js +89 -0
- package/dist/artifacts/src/index.d.ts +4224 -0
- package/dist/artifacts/src/index.js +219 -0
- package/dist/artifacts/src/picture.d.ts +116 -0
- package/dist/artifacts/src/picture.js +161 -0
- package/dist/artifacts/src/resource.d.ts +1391 -0
- package/dist/artifacts/src/resource.js +477 -0
- package/dist/artifacts/src/schema.d.ts +5 -0
- package/dist/artifacts/src/schema.js +18 -0
- package/dist/artifacts/src/types.d.ts +396 -0
- package/dist/artifacts/src/types.js +0 -0
- package/dist/cache/src/index.d.ts +67 -0
- package/dist/cache/src/index.js +58 -0
- package/dist/imgsrc/src/index.d.ts +16 -0
- package/dist/imgsrc/src/index.js +89 -0
- package/dist/limits/src/bucket.d.ts +28 -0
- package/dist/limits/src/bucket.js +23 -0
- package/dist/limits/src/index.d.ts +29 -0
- package/dist/limits/src/index.js +62 -0
- package/dist/limits/src/key.d.ts +37 -0
- package/dist/limits/src/key.js +64 -0
- package/dist/robots/src/index.d.ts +98 -0
- package/dist/robots/src/index.js +196 -0
- package/dist/security/src/agents.d.ts +10 -0
- package/dist/security/src/agents.js +95 -0
- package/dist/security/src/index.d.ts +12 -0
- package/dist/security/src/index.js +37 -0
- package/dist/src/index.d.ts +218 -0
- package/dist/src/index.js +219 -0
- package/dist/store/src/index.d.ts +92 -0
- package/dist/store/src/index.js +264 -0
- package/dist/symlink/src/index.d.ts +24 -0
- package/dist/symlink/src/index.js +89 -0
- package/package.json +85 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { LocaleCode } from "@canmi/me/locales";
|
|
2
|
+
//#region artifacts/src/types.d.ts
|
|
3
|
+
/** Which of the three things a text track is. WebVTT's own vocabulary, not ours. */
|
|
4
|
+
export type CaptionKind = 'captions' | 'subtitles' | 'descriptions';
|
|
5
|
+
export type VideoRung = {
|
|
6
|
+
src: string;
|
|
7
|
+
type: string;
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
};
|
|
11
|
+
/** One published text track, described by what the record says it is rather than by a label. */
|
|
12
|
+
export type VideoTrack = {
|
|
13
|
+
src: string;
|
|
14
|
+
kind: CaptionKind;
|
|
15
|
+
language: string;
|
|
16
|
+
};
|
|
17
|
+
export type ArticleMeta = {
|
|
18
|
+
title: string;
|
|
19
|
+
subtitle: string;
|
|
20
|
+
description: string;
|
|
21
|
+
/** The source view's public BCP-47 language tag. */
|
|
22
|
+
lang: string;
|
|
23
|
+
created: string;
|
|
24
|
+
/** When the article went public, which is the date a reader is shown. The author's to edit. */
|
|
25
|
+
published: string;
|
|
26
|
+
lastmod: string;
|
|
27
|
+
/**
|
|
28
|
+
* Written but not published. Absent means published, so an article says nothing to stay
|
|
29
|
+
* ordinary and one word to be held back.
|
|
30
|
+
*
|
|
31
|
+
* A production build drops these before compiling them, so a draft has no page, no sitemap
|
|
32
|
+
* entry, no feed item and no search record. Every other build keeps them, which is what makes
|
|
33
|
+
* a draft previewable. See web's spec/drafts.md.
|
|
34
|
+
*/
|
|
35
|
+
draft?: boolean;
|
|
36
|
+
};
|
|
37
|
+
export type Block = {
|
|
38
|
+
type: 'prose';
|
|
39
|
+
html: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'heading';
|
|
42
|
+
depth: number;
|
|
43
|
+
slug: string;
|
|
44
|
+
text: string;
|
|
45
|
+
/** Note numbers written into this heading. Absent from `text`, and so from the ToC. */
|
|
46
|
+
notes?: number[];
|
|
47
|
+
} | {
|
|
48
|
+
type: 'code';
|
|
49
|
+
lang: string;
|
|
50
|
+
label?: string;
|
|
51
|
+
title?: string;
|
|
52
|
+
collapsible?: boolean;
|
|
53
|
+
default_expanded?: boolean;
|
|
54
|
+
html: string;
|
|
55
|
+
code: string;
|
|
56
|
+
} | {
|
|
57
|
+
type: 'mermaid';
|
|
58
|
+
source: string;
|
|
59
|
+
ratio?: number;
|
|
60
|
+
description?: string;
|
|
61
|
+
} | {
|
|
62
|
+
type: 'quadrant';
|
|
63
|
+
title: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
/**
|
|
66
|
+
* What the figure reads as, from `local diagram`. Absent until one has been run.
|
|
67
|
+
*
|
|
68
|
+
* Not `description`, which is one line the author wrote to sit under the title. This
|
|
69
|
+
* is the whole figure said in prose, and it is translated, which is what the
|
|
70
|
+
* assembled-from-labels fallback never was.
|
|
71
|
+
*/
|
|
72
|
+
reading?: string;
|
|
73
|
+
axes: Record<QuadrantDirection, string>;
|
|
74
|
+
items: QuadrantItem[];
|
|
75
|
+
} | {
|
|
76
|
+
type: 'svgCanvas';
|
|
77
|
+
svg: string;
|
|
78
|
+
title: string;
|
|
79
|
+
/** What the drawing says, from `local diagram`. Absent until one has been run. */
|
|
80
|
+
description?: string;
|
|
81
|
+
} | {
|
|
82
|
+
type: 'tokei';
|
|
83
|
+
source: string;
|
|
84
|
+
title: string;
|
|
85
|
+
view: TokeiView;
|
|
86
|
+
} | {
|
|
87
|
+
type: 'cargo';
|
|
88
|
+
crate: CrateRecord;
|
|
89
|
+
view: CargoView;
|
|
90
|
+
} | {
|
|
91
|
+
type: 'twitter';
|
|
92
|
+
tweet: TweetRecord;
|
|
93
|
+
} | {
|
|
94
|
+
type: 'github';
|
|
95
|
+
repo: RepoRecord;
|
|
96
|
+
git_ref?: string;
|
|
97
|
+
title?: string;
|
|
98
|
+
align: CardAlign;
|
|
99
|
+
} | {
|
|
100
|
+
type: 'linkcard';
|
|
101
|
+
src: string;
|
|
102
|
+
url: string;
|
|
103
|
+
title: string;
|
|
104
|
+
/**
|
|
105
|
+
* The site's mark, by rid, and never an address.
|
|
106
|
+
*
|
|
107
|
+
* An icon belongs to somebody else's site and is redrawn on their schedule, so what
|
|
108
|
+
* this resource currently holds is a fact about the corpus at the moment somebody
|
|
109
|
+
* asks -- not something a compiled article may carry. The whole key is absent for a
|
|
110
|
+
* site nothing has collected a mark for, and the role is spelled out so a page reads
|
|
111
|
+
* one name on every block: see `namedResources`, and spec/architecture/resource.md.
|
|
112
|
+
*/
|
|
113
|
+
resources?: {
|
|
114
|
+
icon: string;
|
|
115
|
+
};
|
|
116
|
+
/** Which of that resource's files to draw. Selected at render time, not here. */
|
|
117
|
+
tone?: 'light' | 'dark';
|
|
118
|
+
width?: number;
|
|
119
|
+
height?: number;
|
|
120
|
+
preview?: string;
|
|
121
|
+
srcset?: string;
|
|
122
|
+
/** The cover's crop, defaulted like `::image`'s. See the `image` variant below. */
|
|
123
|
+
crop?: string;
|
|
124
|
+
/** `object-position` for that crop. Absent means centred. */
|
|
125
|
+
align?: string;
|
|
126
|
+
/** What the cover shows. Offered as the link's description, never as its name. */
|
|
127
|
+
description?: string;
|
|
128
|
+
} | ({
|
|
129
|
+
type: 'article';
|
|
130
|
+
path: string;
|
|
131
|
+
} & ArticleReference) | {
|
|
132
|
+
type: 'footnotes';
|
|
133
|
+
notes: ArticleNote[];
|
|
134
|
+
} | {
|
|
135
|
+
type: 'placeholder';
|
|
136
|
+
kind: string;
|
|
137
|
+
meta: Record<string, string>;
|
|
138
|
+
/**
|
|
139
|
+
* An embed named but not yet fetched, as opposed to a stub the author wrote.
|
|
140
|
+
*
|
|
141
|
+
* The two had one shape and meant different things, which a consumer could not tell
|
|
142
|
+
* apart: `::cargo` whose record `local embed` has not filled in still says which crate
|
|
143
|
+
* the article meant, while `::placeholder` is the author asking for a gap. The feed is
|
|
144
|
+
* where it showed -- one of them belongs in a document a reader subscribes to and the
|
|
145
|
+
* other does not.
|
|
146
|
+
*/
|
|
147
|
+
pending?: true;
|
|
148
|
+
} | {
|
|
149
|
+
type: 'image';
|
|
150
|
+
/**
|
|
151
|
+
* The picture, by rid, and nothing derived from what it currently holds.
|
|
152
|
+
*
|
|
153
|
+
* Its ladder, its placeholder and its intrinsic box all move when the picture is
|
|
154
|
+
* encoded again, on nobody's schedule but the corpus's, so they are resolved per
|
|
155
|
+
* render and this names what they are resolved from. What is left is what the
|
|
156
|
+
* article itself decided. See spec/architecture/resource.md, "A rid is resolved
|
|
157
|
+
* three times".
|
|
158
|
+
*/
|
|
159
|
+
resources: {
|
|
160
|
+
picture: string;
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* What the picture is called here, which is the article's to say.
|
|
164
|
+
*
|
|
165
|
+
* Baked, unlike the ladder above, because it changes on **this** repository's
|
|
166
|
+
* schedule: `local alt` writes it into a file beside the article, per locale, and a
|
|
167
|
+
* view carries the one it is written in rather than nine it is not.
|
|
168
|
+
*/
|
|
169
|
+
alt: string;
|
|
170
|
+
/**
|
|
171
|
+
* A ratio to crop the displayed image to, as `16 / 9` ready for CSS.
|
|
172
|
+
*
|
|
173
|
+
* Cropping is presentation, so it is done by the browser with `object-fit` rather
|
|
174
|
+
* than by producing another object. A stored variant per ratio and alignment would
|
|
175
|
+
* multiply the bucket and, worse, make a content id mean "this image as shown here"
|
|
176
|
+
* instead of "this image".
|
|
177
|
+
*/
|
|
178
|
+
crop?: string;
|
|
179
|
+
/** `object-position` for that crop. Absent means centred. */
|
|
180
|
+
align?: string;
|
|
181
|
+
} | {
|
|
182
|
+
type: 'video';
|
|
183
|
+
/**
|
|
184
|
+
* The reference the article wrote, extension and all.
|
|
185
|
+
*
|
|
186
|
+
* Kept beside the resolved fields for the reason an image keeps it: an article can name
|
|
187
|
+
* a clip nothing has imported yet, and that should cost a fallback rather than a build.
|
|
188
|
+
*/
|
|
189
|
+
src: string;
|
|
190
|
+
/** Every published rung, smallest first. Absent for a reference nothing resolved. */
|
|
191
|
+
rungs?: VideoRung[];
|
|
192
|
+
/** The original's dimensions, which reserve the box before anything is fetched. */
|
|
193
|
+
width?: number;
|
|
194
|
+
height?: number;
|
|
195
|
+
/** The poster image asset's own rendition, which is what `<video poster>` names. */
|
|
196
|
+
poster?: string;
|
|
197
|
+
/**
|
|
198
|
+
* What every sample of this clip is multiplied by, so two clips play at one level.
|
|
199
|
+
*
|
|
200
|
+
* Computed in the build from the loudness and true peak services/apps/local measured. `1` for
|
|
201
|
+
* a clip nothing has measured, which plays as it always did. See `assets.ts`.
|
|
202
|
+
*/
|
|
203
|
+
gain?: number;
|
|
204
|
+
/** The poster's placeholder, painted under it while it arrives. */
|
|
205
|
+
preview?: string;
|
|
206
|
+
captions?: VideoTrack[];
|
|
207
|
+
/** What the clip shows. Offered as a description, never as the element's name. */
|
|
208
|
+
description?: string;
|
|
209
|
+
/** Where the clip came from, which is what the unsupported-format notice links to. */
|
|
210
|
+
source?: {
|
|
211
|
+
url: string;
|
|
212
|
+
label?: string;
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* What an `::article` card shows of the article it points at, in the view's own locale.
|
|
217
|
+
*
|
|
218
|
+
* The card reads these off the target rather than off the directive, so retitling an article
|
|
219
|
+
* retitles every card naming it and each translated view names it in its own language.
|
|
220
|
+
*/
|
|
221
|
+
export type ArticleReference = {
|
|
222
|
+
title: string;
|
|
223
|
+
subtitle: string;
|
|
224
|
+
/**
|
|
225
|
+
* The date the card draws, which is when the target went public and not when it was written.
|
|
226
|
+
*
|
|
227
|
+
* Named for what it holds rather than kept as `created` beside a second key: this is what a
|
|
228
|
+
* card shows, not what an article is, and a card shows one date. A record carries every fact
|
|
229
|
+
* it knows -- see `ArticleMeta` -- and a projection carries the one that is drawn.
|
|
230
|
+
*/
|
|
231
|
+
published: string;
|
|
232
|
+
/** What a phone card shows instead, where the row clips. Falls back to the full form for a
|
|
233
|
+
* view `local` has not written one for. See web's spec/i18n/prose.md. */
|
|
234
|
+
short_title: string;
|
|
235
|
+
short_subtitle: string;
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* One `:fn` note: the words it explains, the number it was given, and what it says.
|
|
239
|
+
*
|
|
240
|
+
* The phrase is carried so the collected note can name what it is about instead of asking a
|
|
241
|
+
* reader to hold the sentence they left in their head while they read it.
|
|
242
|
+
*
|
|
243
|
+
* Two notes with the same words are two notes. There is no label to say otherwise, and a reader
|
|
244
|
+
* who meets the same explanation twice was told it twice on purpose.
|
|
245
|
+
*/
|
|
246
|
+
export type ArticleNote = {
|
|
247
|
+
number: number;
|
|
248
|
+
phrase: string;
|
|
249
|
+
text: string;
|
|
250
|
+
};
|
|
251
|
+
export type TocEntry = {
|
|
252
|
+
slug: string;
|
|
253
|
+
text: string;
|
|
254
|
+
depth: number;
|
|
255
|
+
};
|
|
256
|
+
export type CardAlign = 'left' | 'center' | 'right';
|
|
257
|
+
export type CargoView = 'treemap' | 'table';
|
|
258
|
+
export type TokeiView = 'treemap' | 'bar' | 'table';
|
|
259
|
+
export type QuadrantDirection = 'top' | 'right' | 'bottom' | 'left';
|
|
260
|
+
export type QuadrantPosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
261
|
+
export type QuadrantItem = {
|
|
262
|
+
at: QuadrantPosition;
|
|
263
|
+
title: string;
|
|
264
|
+
note?: string;
|
|
265
|
+
};
|
|
266
|
+
export type Compiled = {
|
|
267
|
+
meta: ArticleMeta;
|
|
268
|
+
toc: TocEntry[];
|
|
269
|
+
blocks: Block[];
|
|
270
|
+
markdown: string;
|
|
271
|
+
text: string;
|
|
272
|
+
};
|
|
273
|
+
export type ArticleView = Pick<Compiled, 'meta' | 'toc' | 'blocks' | 'text'> & {
|
|
274
|
+
code: LocaleCode;
|
|
275
|
+
/**
|
|
276
|
+
* How long the article is in this view, in words.
|
|
277
|
+
*
|
|
278
|
+
* Body prose and what is inside it. Not `text`, which is every readable string on the page --
|
|
279
|
+
* a picture's description, a linkcard's title, a diagram's caption, an embedded post -- and
|
|
280
|
+
* which this used to be measured from, in characters, making a 9,102-character article read
|
|
281
|
+
* as 14,870. Those are components; a reader asking how long an article is does not mean them.
|
|
282
|
+
*/
|
|
283
|
+
words: number;
|
|
284
|
+
language_tag: string;
|
|
285
|
+
canonical: string;
|
|
286
|
+
/** False when this locale is showing the complete source article as a safe fallback. */
|
|
287
|
+
translation_available: boolean;
|
|
288
|
+
/** The title and subtitle a phone card shows instead of `meta`'s, where the row clips. Falls
|
|
289
|
+
* back to the full form for a view `local` has not written one for. See web's spec/i18n/prose.md.
|
|
290
|
+
* */
|
|
291
|
+
short: {
|
|
292
|
+
title: string;
|
|
293
|
+
subtitle: string;
|
|
294
|
+
};
|
|
295
|
+
/**
|
|
296
|
+
* The title the article page shows on a phone: `meta.title` where it fits the column, and
|
|
297
|
+
* `short.title` where it does not. Decided here rather than in the browser -- the answer is a
|
|
298
|
+
* property of the string, so it cannot change between renders, and computing it at runtime
|
|
299
|
+
* would mean the first frame guessing. See web's spec/styling/phone.md.
|
|
300
|
+
*/
|
|
301
|
+
phone_title: string;
|
|
302
|
+
/**
|
|
303
|
+
* What the article is about, withholding what it concludes. Written by `local summary` into a
|
|
304
|
+
* sidecar rather than into the article, so it is absent until that has been run.
|
|
305
|
+
*/
|
|
306
|
+
summary?: ArticleSummary;
|
|
307
|
+
};
|
|
308
|
+
export type ArticleSummary = {
|
|
309
|
+
text: string;
|
|
310
|
+
provider: string;
|
|
311
|
+
};
|
|
312
|
+
export type CrateDep = {
|
|
313
|
+
name: string;
|
|
314
|
+
version: string;
|
|
315
|
+
kind: string;
|
|
316
|
+
optional: boolean;
|
|
317
|
+
target: string | null;
|
|
318
|
+
features: string[];
|
|
319
|
+
size: number | null;
|
|
320
|
+
depth: number;
|
|
321
|
+
};
|
|
322
|
+
export type CrateRecord = {
|
|
323
|
+
name: string;
|
|
324
|
+
version: string;
|
|
325
|
+
rust_version: string | null;
|
|
326
|
+
features: Record<string, string[]>;
|
|
327
|
+
deps: CrateDep[];
|
|
328
|
+
total_dep_size: number;
|
|
329
|
+
};
|
|
330
|
+
export type RepoRecord = {
|
|
331
|
+
full_name: string;
|
|
332
|
+
description: string | null;
|
|
333
|
+
language: string | null;
|
|
334
|
+
stars: number;
|
|
335
|
+
forks: number;
|
|
336
|
+
open_issues: number;
|
|
337
|
+
license: string | null;
|
|
338
|
+
pushed_at: string | null;
|
|
339
|
+
};
|
|
340
|
+
export type TweetRecord = {
|
|
341
|
+
id: string;
|
|
342
|
+
author: string;
|
|
343
|
+
text: string;
|
|
344
|
+
created: string;
|
|
345
|
+
likes: number;
|
|
346
|
+
reposts: number;
|
|
347
|
+
replies: number;
|
|
348
|
+
};
|
|
349
|
+
export type Alternate = {
|
|
350
|
+
code: Exclude<LocaleCode, 'mw'> | 'x-default';
|
|
351
|
+
language_tag: string;
|
|
352
|
+
href: string;
|
|
353
|
+
};
|
|
354
|
+
export type Article = Compiled & {
|
|
355
|
+
/** The identity: the last segment of the path, unique across the corpus. See build/slugs.ts. */
|
|
356
|
+
slug: string;
|
|
357
|
+
/** The address: the directory it currently sits in, plus that identity. */
|
|
358
|
+
path: string;
|
|
359
|
+
url: string;
|
|
360
|
+
views: Record<LocaleCode, ArticleView>;
|
|
361
|
+
canonical_urls: string[];
|
|
362
|
+
alternates: Alternate[];
|
|
363
|
+
};
|
|
364
|
+
export type InlineSegment = {
|
|
365
|
+
type: 'html';
|
|
366
|
+
html: string;
|
|
367
|
+
} | {
|
|
368
|
+
type: 'link';
|
|
369
|
+
icon?: 'twitter' | 'github' | 'email';
|
|
370
|
+
href: string;
|
|
371
|
+
label: string;
|
|
372
|
+
new_tab: boolean;
|
|
373
|
+
/** `wide` / `narrow` from the directive, as the classes that act on them. A link is the
|
|
374
|
+
* one run that cannot be wrapped in `:t` -- nested, it stops being a link -- so it
|
|
375
|
+
* carries its own width the way a `:t` run carries one. See web's spec/styling/phone.md. */
|
|
376
|
+
width?: string;
|
|
377
|
+
};
|
|
378
|
+
export type PageBlock = {
|
|
379
|
+
type: 'p';
|
|
380
|
+
segments: InlineSegment[];
|
|
381
|
+
} | {
|
|
382
|
+
type: 'html';
|
|
383
|
+
html: string;
|
|
384
|
+
};
|
|
385
|
+
export type CompiledPage = {
|
|
386
|
+
meta: Record<string, string>;
|
|
387
|
+
blocks: PageBlock[];
|
|
388
|
+
body: string;
|
|
389
|
+
};
|
|
390
|
+
export type PageView = Pick<CompiledPage, 'meta' | 'blocks'>;
|
|
391
|
+
export type Page = {
|
|
392
|
+
path: string;
|
|
393
|
+
markdown: string;
|
|
394
|
+
views: Record<LocaleCode, PageView>;
|
|
395
|
+
};
|
|
396
|
+
//#endregion
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//#region cache/src/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* How long an answer from this site may be kept.
|
|
4
|
+
*
|
|
5
|
+
* Publication has one delay rather than a different one per resource, so the number saying
|
|
6
|
+
* what that delay is belongs in one place rather than in each worker that stamps it -- three
|
|
7
|
+
* of them wrote it out, and two that drifted apart would be two answers to one question. See
|
|
8
|
+
* spec/architecture/delivery.md and spec/architecture/artifacts.md, "The key says what may
|
|
9
|
+
* cache it".
|
|
10
|
+
*
|
|
11
|
+
* Only the values are here. Which answer earns which lifetime stays with the worker that knows,
|
|
12
|
+
* because the three do three different jobs, and one policy deciding for all of them would be
|
|
13
|
+
* one place holding three unrelated decisions.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Five minutes, in seconds. How far behind the current publication an answer may be.
|
|
17
|
+
*
|
|
18
|
+
* None of it while developing: a publication delay is a promise made to readers, and on a laptop
|
|
19
|
+
* it is only the distance between a rebuild and seeing it, which was minutes of waiting for a
|
|
20
|
+
* change nobody else was going to read.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PUBLICATION_DELAY: number;
|
|
23
|
+
/**
|
|
24
|
+
* Three hours, in seconds, for a caller that decides its answer is worth serving stale.
|
|
25
|
+
*
|
|
26
|
+
* A root that old names objects that are all still there and still immutable, so what it renders
|
|
27
|
+
* is a coherent older page rather than a broken one. Whether an answer may be served during an
|
|
28
|
+
* outage is a fact about what produced it, so that decision stays where it is made and only the
|
|
29
|
+
* window is shared.
|
|
30
|
+
*/
|
|
31
|
+
export declare const WHILE_UNREACHABLE = 10800;
|
|
32
|
+
/**
|
|
33
|
+
* What an answer about what is published right now earns.
|
|
34
|
+
*
|
|
35
|
+
* Its key names rather than identifies, so the bytes behind it change when somebody publishes
|
|
36
|
+
* and it is held for exactly the delay publication has. Refusals included: a 404 about the
|
|
37
|
+
* corpus and a 400 about an address are both true until the next publication, and giving either
|
|
38
|
+
* a number of its own would be a second publication delay.
|
|
39
|
+
*/
|
|
40
|
+
export declare const PUBLISHED: string;
|
|
41
|
+
/**
|
|
42
|
+
* What an answer keeps when its address names something rather than identifying it.
|
|
43
|
+
*
|
|
44
|
+
* Between the other two: what stands behind the name can change, but not on this site's
|
|
45
|
+
* publication clock -- a permanent shortcut whose target moves, somebody else's file fetched
|
|
46
|
+
* live. An hour is long enough to be worth an edge entry and short enough that a change lands
|
|
47
|
+
* the same day.
|
|
48
|
+
*/
|
|
49
|
+
export declare const NAMED = "public, max-age=3600";
|
|
50
|
+
/**
|
|
51
|
+
* What a key whose bytes cannot change earns.
|
|
52
|
+
*
|
|
53
|
+
* Content addressing is the usual reason -- a hashed name cannot denote different bytes, so a
|
|
54
|
+
* cached copy is correct forever and needs no invalidation. It is not the only one: a Latin font
|
|
55
|
+
* subset keeps the year on a written promise that re-subsetting renames it, which is why this is
|
|
56
|
+
* named for the property rather than for the hash that normally carries it.
|
|
57
|
+
*/
|
|
58
|
+
export declare const UNCHANGING = "public, max-age=31536000, immutable";
|
|
59
|
+
/**
|
|
60
|
+
* A name resolved to the object it stands for right now: the publication delay, and served stale
|
|
61
|
+
* through an outage, since the target is content-addressed and a stale one is still bytes.
|
|
62
|
+
*
|
|
63
|
+
* The alias layer stamps it on its redirect, and a page that follows that redirect for a browser
|
|
64
|
+
* stamps it on its own. See spec/architecture/delivery.md.
|
|
65
|
+
*/
|
|
66
|
+
export declare const RESOLVED: string;
|
|
67
|
+
//#endregion
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Five minutes, in seconds. How far behind the current publication an answer may be.
|
|
3
|
+
*
|
|
4
|
+
* None of it while developing: a publication delay is a promise made to readers, and on a laptop
|
|
5
|
+
* it is only the distance between a rebuild and seeing it, which was minutes of waiting for a
|
|
6
|
+
* change nobody else was going to read.
|
|
7
|
+
*/
|
|
8
|
+
const PUBLICATION_DELAY = import.meta.env?.MODE === "development" ? 0 : 300;
|
|
9
|
+
/** One year, in seconds: the longest a browser honours, and what `immutable` already implies. */
|
|
10
|
+
const UNCHANGING_LIFE = 31536e3;
|
|
11
|
+
/** One hour, in seconds. The middle number, for an address that names rather than identifies. */
|
|
12
|
+
const NAMED_LIFE = 3600;
|
|
13
|
+
/**
|
|
14
|
+
* Three hours, in seconds, for a caller that decides its answer is worth serving stale.
|
|
15
|
+
*
|
|
16
|
+
* A root that old names objects that are all still there and still immutable, so what it renders
|
|
17
|
+
* is a coherent older page rather than a broken one. Whether an answer may be served during an
|
|
18
|
+
* outage is a fact about what produced it, so that decision stays where it is made and only the
|
|
19
|
+
* window is shared.
|
|
20
|
+
*/
|
|
21
|
+
const WHILE_UNREACHABLE = 10800;
|
|
22
|
+
/**
|
|
23
|
+
* What an answer about what is published right now earns.
|
|
24
|
+
*
|
|
25
|
+
* Its key names rather than identifies, so the bytes behind it change when somebody publishes
|
|
26
|
+
* and it is held for exactly the delay publication has. Refusals included: a 404 about the
|
|
27
|
+
* corpus and a 400 about an address are both true until the next publication, and giving either
|
|
28
|
+
* a number of its own would be a second publication delay.
|
|
29
|
+
*/
|
|
30
|
+
const PUBLISHED = `public, max-age=${PUBLICATION_DELAY}`;
|
|
31
|
+
/**
|
|
32
|
+
* What an answer keeps when its address names something rather than identifying it.
|
|
33
|
+
*
|
|
34
|
+
* Between the other two: what stands behind the name can change, but not on this site's
|
|
35
|
+
* publication clock -- a permanent shortcut whose target moves, somebody else's file fetched
|
|
36
|
+
* live. An hour is long enough to be worth an edge entry and short enough that a change lands
|
|
37
|
+
* the same day.
|
|
38
|
+
*/
|
|
39
|
+
const NAMED = `public, max-age=${NAMED_LIFE}`;
|
|
40
|
+
/**
|
|
41
|
+
* What a key whose bytes cannot change earns.
|
|
42
|
+
*
|
|
43
|
+
* Content addressing is the usual reason -- a hashed name cannot denote different bytes, so a
|
|
44
|
+
* cached copy is correct forever and needs no invalidation. It is not the only one: a Latin font
|
|
45
|
+
* subset keeps the year on a written promise that re-subsetting renames it, which is why this is
|
|
46
|
+
* named for the property rather than for the hash that normally carries it.
|
|
47
|
+
*/
|
|
48
|
+
const UNCHANGING = `public, max-age=${UNCHANGING_LIFE}, immutable`;
|
|
49
|
+
/**
|
|
50
|
+
* A name resolved to the object it stands for right now: the publication delay, and served stale
|
|
51
|
+
* through an outage, since the target is content-addressed and a stale one is still bytes.
|
|
52
|
+
*
|
|
53
|
+
* The alias layer stamps it on its redirect, and a page that follows that redirect for a browser
|
|
54
|
+
* stamps it on its own. See spec/architecture/delivery.md.
|
|
55
|
+
*/
|
|
56
|
+
const RESOLVED = `${PUBLISHED}, stale-if-error=${WHILE_UNREACHABLE}`;
|
|
57
|
+
//#endregion
|
|
58
|
+
export { NAMED, PUBLICATION_DELAY, PUBLISHED, RESOLVED, UNCHANGING, WHILE_UNREACHABLE };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region imgsrc/src/index.d.ts
|
|
2
|
+
export type Options = {
|
|
3
|
+
cdnUrl?: string;
|
|
4
|
+
/**
|
|
5
|
+
* Selects the development CDN. Defaults to false, so server and browser always agree.
|
|
6
|
+
*
|
|
7
|
+
* Do not infer this from `globalThis.location`: that global is absent during SSR and
|
|
8
|
+
* present in the browser, so the same image would resolve to the production CDN on the
|
|
9
|
+
* server and the local one on the client -- a silent hydration mismatch that only appears
|
|
10
|
+
* in development. The caller knows the answer, so the caller passes it. In SvelteKit that
|
|
11
|
+
* is `dev` from `$app/environment`.
|
|
12
|
+
*/
|
|
13
|
+
isDev?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function imgsrc(input: string, opts?: Options): string;
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { URLS, pickUrls } from "../../src/index.js";
|
|
2
|
+
//#region imgsrc/src/index.ts
|
|
3
|
+
const GITHUB_AVATAR_SCHEME = "github:avatar:";
|
|
4
|
+
const GITHUB_SCHEME = "github:";
|
|
5
|
+
function imgsrc(input, opts = {}) {
|
|
6
|
+
const cdnUrl = opts.cdnUrl ?? pickUrls(opts.isDev ?? false).cdn;
|
|
7
|
+
if (input.startsWith("data:")) return input;
|
|
8
|
+
if (input.startsWith(GITHUB_AVATAR_SCHEME)) return resolveGithubAvatar(input, cdnUrl);
|
|
9
|
+
if (input.startsWith(GITHUB_SCHEME)) return resolveGithubScheme(input);
|
|
10
|
+
if (hasWebScheme(input)) return rewriteIfKnown(input, cdnUrl);
|
|
11
|
+
return `${cdnUrl}/object/${input}`;
|
|
12
|
+
}
|
|
13
|
+
function hostOf(url) {
|
|
14
|
+
return new URL(url).hostname;
|
|
15
|
+
}
|
|
16
|
+
function hasWebScheme(input) {
|
|
17
|
+
const schemeEnd = input.indexOf(":");
|
|
18
|
+
if (schemeEnd < 0) return false;
|
|
19
|
+
const scheme = input.slice(0, schemeEnd).toLowerCase();
|
|
20
|
+
return scheme === "http" || scheme === "https";
|
|
21
|
+
}
|
|
22
|
+
function resolveGithubAvatar(input, cdnUrl) {
|
|
23
|
+
const parsed = parseAvatarRef(input.slice(14));
|
|
24
|
+
if (!parsed) return input;
|
|
25
|
+
const query = parsed.size ? `?width=${parsed.size}` : "";
|
|
26
|
+
return `${cdnUrl}/proxy/github/avatar/${parsed.idOrName}${query}`;
|
|
27
|
+
}
|
|
28
|
+
function parseAvatarRef(rest) {
|
|
29
|
+
const lastAt = rest.lastIndexOf("@");
|
|
30
|
+
if (lastAt < 0) return rest ? {
|
|
31
|
+
idOrName: rest,
|
|
32
|
+
size: null
|
|
33
|
+
} : null;
|
|
34
|
+
if (lastAt === 0) {
|
|
35
|
+
const name = rest.slice(1);
|
|
36
|
+
return name ? {
|
|
37
|
+
idOrName: name,
|
|
38
|
+
size: null
|
|
39
|
+
} : null;
|
|
40
|
+
}
|
|
41
|
+
const before = rest.slice(0, lastAt);
|
|
42
|
+
const after = rest.slice(lastAt + 1);
|
|
43
|
+
if (!/^\d+$/.test(after)) return null;
|
|
44
|
+
const idOrName = before.startsWith("@") ? before.slice(1) : before;
|
|
45
|
+
return idOrName ? {
|
|
46
|
+
idOrName,
|
|
47
|
+
size: after
|
|
48
|
+
} : null;
|
|
49
|
+
}
|
|
50
|
+
function resolveGithubScheme(input) {
|
|
51
|
+
const rest = input.slice(7);
|
|
52
|
+
const atIdx = rest.lastIndexOf("@");
|
|
53
|
+
const pathPart = atIdx >= 0 ? rest.slice(0, atIdx) : rest;
|
|
54
|
+
const ref = atIdx >= 0 ? rest.slice(atIdx + 1) : null;
|
|
55
|
+
const [owner, repo, ...pathBits] = pathPart.split("/");
|
|
56
|
+
if (!owner || !repo || pathBits.length === 0) return input;
|
|
57
|
+
return toGithubCdn(owner, repo, ref, pathBits);
|
|
58
|
+
}
|
|
59
|
+
function rewriteIfKnown(input, cdnUrl) {
|
|
60
|
+
let url;
|
|
61
|
+
try {
|
|
62
|
+
url = new URL(input);
|
|
63
|
+
} catch {
|
|
64
|
+
return input;
|
|
65
|
+
}
|
|
66
|
+
if (url.hostname === hostOf(URLS.external.github.avatars)) {
|
|
67
|
+
const match = url.pathname.match(/^\/u\/(\d+)/);
|
|
68
|
+
if (match) {
|
|
69
|
+
const size = url.searchParams.get("s");
|
|
70
|
+
const query = size ? `?width=${size}` : "";
|
|
71
|
+
return `${cdnUrl}/proxy/github/avatar/${match[1]}${query}`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (url.hostname === hostOf(URLS.external.github.raw)) {
|
|
75
|
+
const [owner, repo, ref, ...pathBits] = url.pathname.split("/").filter(Boolean);
|
|
76
|
+
if (owner && repo && ref && pathBits.length > 0) return toGithubCdn(owner, repo, ref, pathBits);
|
|
77
|
+
}
|
|
78
|
+
if (url.hostname === hostOf(URLS.external.github.web)) {
|
|
79
|
+
const [owner, repo, kind, ref, ...pathBits] = url.pathname.split("/").filter(Boolean);
|
|
80
|
+
if (owner && repo && ref && pathBits.length > 0 && (kind === "raw" || kind === "blob")) return toGithubCdn(owner, repo, ref, pathBits);
|
|
81
|
+
}
|
|
82
|
+
return input;
|
|
83
|
+
}
|
|
84
|
+
function toGithubCdn(owner, repo, ref, pathBits) {
|
|
85
|
+
const refSuffix = ref ? `@${ref}` : "";
|
|
86
|
+
return `${URLS.external.github.cdn}/${owner}/${repo}${refSuffix}/${pathBits.join("/")}`;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
export { imgsrc };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region limits/src/bucket.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A limit as a bucket: `burst` calls at once, and room coming back at `count` calls in `seconds`.
|
|
4
|
+
* Counted with GCRA, which admits exactly what a token bucket would while keeping one number per
|
|
5
|
+
* key -- the moment the next call is due. Pure, so every deployment of `quota` runs the same
|
|
6
|
+
* arithmetic. See spec/architecture/quota.md, "A limit is a bucket".
|
|
7
|
+
*/
|
|
8
|
+
/** How fast room comes back, and how much of it there is. */
|
|
9
|
+
export interface Rate {
|
|
10
|
+
readonly count: number;
|
|
11
|
+
readonly seconds: number;
|
|
12
|
+
/** How many calls may come together; `count` when absent. */
|
|
13
|
+
readonly burst?: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Taken {
|
|
16
|
+
readonly allowed: boolean;
|
|
17
|
+
/** Whole seconds until another call would be allowed; 0 when this one was. */
|
|
18
|
+
readonly retryAfter: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* One call at `now`, in milliseconds, against a bucket whose next call was `due`, or never asked
|
|
22
|
+
* when undefined: whether it passes, and the `due` to keep after it. A refused call keeps the old
|
|
23
|
+
* `due`, so refusals spend nothing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function take(due: number | undefined, rate: Rate, now: number): Taken & {
|
|
26
|
+
readonly due: number | undefined;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region limits/src/bucket.ts
|
|
2
|
+
/**
|
|
3
|
+
* One call at `now`, in milliseconds, against a bucket whose next call was `due`, or never asked
|
|
4
|
+
* when undefined: whether it passes, and the `due` to keep after it. A refused call keeps the old
|
|
5
|
+
* `due`, so refusals spend nothing.
|
|
6
|
+
*/
|
|
7
|
+
function take(due, rate, now) {
|
|
8
|
+
const interval = rate.seconds * 1e3 / rate.count;
|
|
9
|
+
const room = (rate.burst ?? rate.count) * interval;
|
|
10
|
+
const next = Math.max(due ?? now, now) + interval;
|
|
11
|
+
if (next - now <= room) return {
|
|
12
|
+
allowed: true,
|
|
13
|
+
retryAfter: 0,
|
|
14
|
+
due: next
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
allowed: false,
|
|
18
|
+
retryAfter: Math.max(1, Math.ceil((next - now - room) / 1e3)),
|
|
19
|
+
due
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { take };
|