@micacms/runtime 0.2.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/README.md +16 -0
- package/dist/app.d.ts +62 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +107 -0
- package/dist/app.js.map +1 -0
- package/dist/config.d.ts +187 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +154 -0
- package/dist/config.js.map +1 -0
- package/dist/context.d.ts +121 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +117 -0
- package/dist/context.js.map +1 -0
- package/dist/db/driver.d.ts +35 -0
- package/dist/db/driver.d.ts.map +1 -0
- package/dist/db/driver.js +95 -0
- package/dist/db/driver.js.map +1 -0
- package/dist/http.d.ts +40 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +38 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/errors.d.ts +39 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +52 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/file-type.d.ts +26 -0
- package/dist/lib/file-type.d.ts.map +1 -0
- package/dist/lib/file-type.js +89 -0
- package/dist/lib/file-type.js.map +1 -0
- package/dist/lib/git.d.ts +29 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +62 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/id.d.ts +13 -0
- package/dist/lib/id.d.ts.map +1 -0
- package/dist/lib/id.js +56 -0
- package/dist/lib/id.js.map +1 -0
- package/dist/lib/logger.d.ts +20 -0
- package/dist/lib/logger.d.ts.map +1 -0
- package/dist/lib/logger.js +65 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/ports.d.ts +17 -0
- package/dist/lib/ports.d.ts.map +1 -0
- package/dist/lib/ports.js +17 -0
- package/dist/lib/ports.js.map +1 -0
- package/dist/lib/rate-limit.d.ts +29 -0
- package/dist/lib/rate-limit.d.ts.map +1 -0
- package/dist/lib/rate-limit.js +47 -0
- package/dist/lib/rate-limit.js.map +1 -0
- package/dist/lib/schema-stats.d.ts +64 -0
- package/dist/lib/schema-stats.d.ts.map +1 -0
- package/dist/lib/schema-stats.js +103 -0
- package/dist/lib/schema-stats.js.map +1 -0
- package/dist/lib/version.d.ts +14 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +25 -0
- package/dist/lib/version.js.map +1 -0
- package/dist/middleware/index.d.ts +41 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +142 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/routes/admin-content.d.ts +22 -0
- package/dist/routes/admin-content.d.ts.map +1 -0
- package/dist/routes/admin-content.js +194 -0
- package/dist/routes/admin-content.js.map +1 -0
- package/dist/routes/admin-media.d.ts +19 -0
- package/dist/routes/admin-media.d.ts.map +1 -0
- package/dist/routes/admin-media.js +81 -0
- package/dist/routes/admin-media.js.map +1 -0
- package/dist/routes/admin-spa.d.ts +13 -0
- package/dist/routes/admin-spa.d.ts.map +1 -0
- package/dist/routes/admin-spa.js +72 -0
- package/dist/routes/admin-spa.js.map +1 -0
- package/dist/routes/admin-staging.d.ts +15 -0
- package/dist/routes/admin-staging.d.ts.map +1 -0
- package/dist/routes/admin-staging.js +90 -0
- package/dist/routes/admin-staging.js.map +1 -0
- package/dist/routes/admin-system.d.ts +14 -0
- package/dist/routes/admin-system.d.ts.map +1 -0
- package/dist/routes/admin-system.js +153 -0
- package/dist/routes/admin-system.js.map +1 -0
- package/dist/routes/health.d.ts +12 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +44 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/schema.d.ts +56 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +173 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +47 -0
- package/dist/server.js.map +1 -0
- package/dist/services/media.d.ts +65 -0
- package/dist/services/media.d.ts.map +1 -0
- package/dist/services/media.js +180 -0
- package/dist/services/media.js.map +1 -0
- package/dist/services/staging-local.d.ts +58 -0
- package/dist/services/staging-local.d.ts.map +1 -0
- package/dist/services/staging-local.js +391 -0
- package/dist/services/staging-local.js.map +1 -0
- package/dist/services/staging.d.ts +167 -0
- package/dist/services/staging.d.ts.map +1 -0
- package/dist/services/staging.js +333 -0
- package/dist/services/staging.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* Every failure the API reports on purpose is an `ApiError` carrying a stable
|
|
5
|
+
* machine-readable `code`. Anything else that reaches the error middleware is
|
|
6
|
+
* treated as a bug: logged with its stack, reported as a bare 500.
|
|
7
|
+
*/
|
|
8
|
+
const STATUS = {
|
|
9
|
+
bad_request: 400,
|
|
10
|
+
validation_failed: 422,
|
|
11
|
+
unauthenticated: 401,
|
|
12
|
+
invalid_credentials: 401,
|
|
13
|
+
forbidden: 403,
|
|
14
|
+
not_found: 404,
|
|
15
|
+
conflict: 409,
|
|
16
|
+
payload_too_large: 413,
|
|
17
|
+
unsupported_media_type: 415,
|
|
18
|
+
rate_limited: 429,
|
|
19
|
+
internal_error: 500,
|
|
20
|
+
service_unavailable: 503,
|
|
21
|
+
};
|
|
22
|
+
export class ApiError extends Error {
|
|
23
|
+
name = "ApiError";
|
|
24
|
+
code;
|
|
25
|
+
status;
|
|
26
|
+
issues;
|
|
27
|
+
/** Extra response headers, e.g. `Retry-After` on a rate limit. */
|
|
28
|
+
headers;
|
|
29
|
+
constructor(code, message, options = {}) {
|
|
30
|
+
super(message, { cause: options.cause });
|
|
31
|
+
this.code = code;
|
|
32
|
+
this.status = STATUS[code];
|
|
33
|
+
this.issues = options.issues;
|
|
34
|
+
this.headers = options.headers;
|
|
35
|
+
}
|
|
36
|
+
toBody() {
|
|
37
|
+
return {
|
|
38
|
+
error: {
|
|
39
|
+
code: this.code,
|
|
40
|
+
message: this.message,
|
|
41
|
+
...(this.issues ? { issues: this.issues } : {}),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export const badRequest = (message) => new ApiError("bad_request", message);
|
|
47
|
+
export const unauthenticated = (message = "Authentication required.") => new ApiError("unauthenticated", message);
|
|
48
|
+
export const forbidden = (message = "You do not have access to this resource.") => new ApiError("forbidden", message);
|
|
49
|
+
export const notFound = (message = "Not found.") => new ApiError("not_found", message);
|
|
50
|
+
export const conflict = (message) => new ApiError("conflict", message);
|
|
51
|
+
export const validationFailed = (issues, message = "The content is not valid.") => new ApiError("validation_failed", message, { issues });
|
|
52
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmBH,MAAM,MAAM,GAA8B;IACxC,WAAW,EAAE,GAAG;IAChB,iBAAiB,EAAE,GAAG;IACtB,eAAe,EAAE,GAAG;IACpB,mBAAmB,EAAE,GAAG;IACxB,SAAS,EAAE,GAAG;IACd,SAAS,EAAE,GAAG;IACd,QAAQ,EAAE,GAAG;IACb,iBAAiB,EAAE,GAAG;IACtB,sBAAsB,EAAE,GAAG;IAC3B,YAAY,EAAE,GAAG;IACjB,cAAc,EAAE,GAAG;IACnB,mBAAmB,EAAE,GAAG;CACzB,CAAC;AAUF,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,IAAI,GAAG,UAAU,CAAC;IAClB,IAAI,CAAY;IAChB,MAAM,CAAS;IACf,MAAM,CAAgB;IAC/B,kEAAkE;IACzD,OAAO,CAA0B;IAE1C,YACE,IAAe,EACf,OAAe,EACf,UAAwF,EAAE;QAE1F,KAAK,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACjC,CAAC;IAED,MAAM;QACJ,OAAO;YACL,KAAK,EAAE;gBACL,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,OAAO,EAAE,IAAI,CAAC,OAAO;gBACrB,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aAChD;SACF,CAAC;IACJ,CAAC;CACF;AAED,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,IAAI,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC;AACpF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,GAAG,0BAA0B,EAAE,EAAE,CACtE,IAAI,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC;AAC3C,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,OAAO,GAAG,0CAA0C,EAAE,EAAE,CAChF,IAAI,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AACrC,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAO,GAAG,YAAY,EAAE,EAAE,CAAC,IAAI,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;AACvF,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,OAAe,EAAE,EAAE,CAAC,IAAI,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAoB,EAAE,OAAO,GAAG,2BAA2B,EAAE,EAAE,CAC9F,IAAI,QAAQ,CAAC,mBAAmB,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content sniffing for uploads.
|
|
3
|
+
*
|
|
4
|
+
* The browser-supplied MIME type is attacker-controlled and must not be
|
|
5
|
+
* trusted: an executable renamed to `.jpg` arrives as `image/jpeg`. The stored
|
|
6
|
+
* type is derived from the bytes instead, and the extension is regenerated
|
|
7
|
+
* from it rather than taken from the filename.
|
|
8
|
+
*/
|
|
9
|
+
export interface DetectedType {
|
|
10
|
+
mimeType: string;
|
|
11
|
+
extension: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function detectFileType(bytes: Uint8Array): DetectedType | null;
|
|
14
|
+
export declare const isImageType: (mimeType: string) => boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether a detected type is acceptable for a field's `accept` setting.
|
|
17
|
+
* `any` deliberately does not mean "anything": only the types listed here can
|
|
18
|
+
* be stored at all.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isAccepted(mimeType: string, accept: "image" | "document" | "any"): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* SVG can carry scripts, so it is served under a sandbox CSP. Callers use this
|
|
23
|
+
* to decide whether the extra header is needed.
|
|
24
|
+
*/
|
|
25
|
+
export declare const isScriptable: (mimeType: string) => boolean;
|
|
26
|
+
//# sourceMappingURL=file-type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-type.d.ts","sourceRoot":"","sources":["../../src/lib/file-type.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAQD,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,GAAG,IAAI,CA2CrE;AA4BD,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,KAAG,OAAoC,CAAC;AAEpF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,UAAU,GAAG,KAAK,GAAG,OAAO,CAI1F;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,GAAI,UAAU,MAAM,KAAG,OAAuC,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content sniffing for uploads.
|
|
3
|
+
*
|
|
4
|
+
* The browser-supplied MIME type is attacker-controlled and must not be
|
|
5
|
+
* trusted: an executable renamed to `.jpg` arrives as `image/jpeg`. The stored
|
|
6
|
+
* type is derived from the bytes instead, and the extension is regenerated
|
|
7
|
+
* from it rather than taken from the filename.
|
|
8
|
+
*/
|
|
9
|
+
const startsWith = (bytes, signature, offset = 0) => signature.every((byte, index) => bytes[offset + index] === byte);
|
|
10
|
+
const ascii = (bytes, offset, length) => Array.from(bytes.slice(offset, offset + length), (byte) => String.fromCharCode(byte)).join("");
|
|
11
|
+
export function detectFileType(bytes) {
|
|
12
|
+
if (bytes.length < 12)
|
|
13
|
+
return null;
|
|
14
|
+
if (startsWith(bytes, [0xff, 0xd8, 0xff])) {
|
|
15
|
+
return { mimeType: "image/jpeg", extension: "jpg" };
|
|
16
|
+
}
|
|
17
|
+
if (startsWith(bytes, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) {
|
|
18
|
+
return { mimeType: "image/png", extension: "png" };
|
|
19
|
+
}
|
|
20
|
+
const header = ascii(bytes, 0, 6);
|
|
21
|
+
if (header === "GIF87a" || header === "GIF89a") {
|
|
22
|
+
return { mimeType: "image/gif", extension: "gif" };
|
|
23
|
+
}
|
|
24
|
+
if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WEBP") {
|
|
25
|
+
return { mimeType: "image/webp", extension: "webp" };
|
|
26
|
+
}
|
|
27
|
+
// ISO base media formats declare their brand at offset 8, after the box size.
|
|
28
|
+
if (ascii(bytes, 4, 4) === "ftyp") {
|
|
29
|
+
const brand = ascii(bytes, 8, 4);
|
|
30
|
+
if (brand === "avif" || brand === "avis") {
|
|
31
|
+
return { mimeType: "image/avif", extension: "avif" };
|
|
32
|
+
}
|
|
33
|
+
if (brand === "heic" || brand === "heix" || brand === "mif1") {
|
|
34
|
+
return { mimeType: "image/heic", extension: "heic" };
|
|
35
|
+
}
|
|
36
|
+
if (brand === "isom" || brand === "mp42" || brand === "M4V ") {
|
|
37
|
+
return { mimeType: "video/mp4", extension: "mp4" };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (ascii(bytes, 0, 5) === "%PDF-") {
|
|
41
|
+
return { mimeType: "application/pdf", extension: "pdf" };
|
|
42
|
+
}
|
|
43
|
+
if (isSvg(bytes)) {
|
|
44
|
+
return { mimeType: "image/svg+xml", extension: "svg" };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** SVG has no magic number, so look for a root `<svg>` in the leading text. */
|
|
49
|
+
function isSvg(bytes) {
|
|
50
|
+
const head = new TextDecoder("utf-8", { fatal: false })
|
|
51
|
+
.decode(bytes.slice(0, 1024))
|
|
52
|
+
.trimStart()
|
|
53
|
+
.toLowerCase();
|
|
54
|
+
if (head.startsWith("<svg"))
|
|
55
|
+
return true;
|
|
56
|
+
if (head.startsWith("<?xml") || head.startsWith("<!doctype svg")) {
|
|
57
|
+
return head.includes("<svg");
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
const IMAGE_TYPES = new Set([
|
|
62
|
+
"image/jpeg",
|
|
63
|
+
"image/png",
|
|
64
|
+
"image/gif",
|
|
65
|
+
"image/webp",
|
|
66
|
+
"image/avif",
|
|
67
|
+
"image/heic",
|
|
68
|
+
"image/svg+xml",
|
|
69
|
+
]);
|
|
70
|
+
const DOCUMENT_TYPES = new Set(["application/pdf"]);
|
|
71
|
+
export const isImageType = (mimeType) => IMAGE_TYPES.has(mimeType);
|
|
72
|
+
/**
|
|
73
|
+
* Whether a detected type is acceptable for a field's `accept` setting.
|
|
74
|
+
* `any` deliberately does not mean "anything": only the types listed here can
|
|
75
|
+
* be stored at all.
|
|
76
|
+
*/
|
|
77
|
+
export function isAccepted(mimeType, accept) {
|
|
78
|
+
if (accept === "image")
|
|
79
|
+
return IMAGE_TYPES.has(mimeType);
|
|
80
|
+
if (accept === "document")
|
|
81
|
+
return DOCUMENT_TYPES.has(mimeType);
|
|
82
|
+
return IMAGE_TYPES.has(mimeType) || DOCUMENT_TYPES.has(mimeType) || mimeType === "video/mp4";
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* SVG can carry scripts, so it is served under a sandbox CSP. Callers use this
|
|
86
|
+
* to decide whether the extra header is needed.
|
|
87
|
+
*/
|
|
88
|
+
export const isScriptable = (mimeType) => mimeType === "image/svg+xml";
|
|
89
|
+
//# sourceMappingURL=file-type.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-type.js","sourceRoot":"","sources":["../../src/lib/file-type.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawning the developer's own `git`.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately the binary, not a JavaScript Git implementation. The reason is
|
|
5
|
+
* credentials: a developer's push already works because of their SSH agent,
|
|
6
|
+
* credential helper and remote configuration, and the spawned binary inherits
|
|
7
|
+
* all three. A library would have to reimplement them, and would do it worse
|
|
8
|
+
* — see decisions.md, "Local publishing spawns the `git` binary".
|
|
9
|
+
*
|
|
10
|
+
* `GIT_TERMINAL_PROMPT=0` because there is no terminal to answer: a push that
|
|
11
|
+
* would ask for credentials must fail with Git's own message rather than hang
|
|
12
|
+
* the request that triggered it.
|
|
13
|
+
*/
|
|
14
|
+
export declare class GitError extends Error {
|
|
15
|
+
readonly stderr: string;
|
|
16
|
+
readonly exitCode: number | null;
|
|
17
|
+
name: string;
|
|
18
|
+
constructor(message: string, stderr: string, exitCode: number | null);
|
|
19
|
+
}
|
|
20
|
+
export interface GitRunner {
|
|
21
|
+
/** Runs `git` in the runner's directory. Trimmed stdout, or a `GitError`. */
|
|
22
|
+
run(args: string[]): Promise<string>;
|
|
23
|
+
/** Like `run`, but null instead of a throw — for questions Git may say no to. */
|
|
24
|
+
tryRun(args: string[]): Promise<string | null>;
|
|
25
|
+
}
|
|
26
|
+
export declare function gitRunner(cwd: string): GitRunner;
|
|
27
|
+
/** The line a person should see first — Git puts its verdict there. */
|
|
28
|
+
export declare function firstLine(text: string): string;
|
|
29
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../../src/lib/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,qBAAa,QAAS,SAAQ,KAAK;IAI/B,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAJzB,IAAI,SAAc;gBAEzB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GAAG,IAAI;CAInC;AAED,MAAM,WAAW,SAAS;IACxB,6EAA6E;IAC7E,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACrC,iFAAiF;IACjF,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CAChD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAyChD;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO9C"}
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spawning the developer's own `git`.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately the binary, not a JavaScript Git implementation. The reason is
|
|
5
|
+
* credentials: a developer's push already works because of their SSH agent,
|
|
6
|
+
* credential helper and remote configuration, and the spawned binary inherits
|
|
7
|
+
* all three. A library would have to reimplement them, and would do it worse
|
|
8
|
+
* — see decisions.md, "Local publishing spawns the `git` binary".
|
|
9
|
+
*
|
|
10
|
+
* `GIT_TERMINAL_PROMPT=0` because there is no terminal to answer: a push that
|
|
11
|
+
* would ask for credentials must fail with Git's own message rather than hang
|
|
12
|
+
* the request that triggered it.
|
|
13
|
+
*/
|
|
14
|
+
import { execFile } from "node:child_process";
|
|
15
|
+
export class GitError extends Error {
|
|
16
|
+
stderr;
|
|
17
|
+
exitCode;
|
|
18
|
+
name = "GitError";
|
|
19
|
+
constructor(message, stderr, exitCode) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.stderr = stderr;
|
|
22
|
+
this.exitCode = exitCode;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function gitRunner(cwd) {
|
|
26
|
+
const run = (args) => new Promise((resolve, reject) => {
|
|
27
|
+
execFile("git", args, {
|
|
28
|
+
cwd,
|
|
29
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
30
|
+
timeout: 60_000,
|
|
31
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
32
|
+
}, (error, stdout, stderr) => {
|
|
33
|
+
if (error) {
|
|
34
|
+
const missing = error.code === "ENOENT";
|
|
35
|
+
reject(new GitError(missing
|
|
36
|
+
? "Git is not installed, or not on the PATH."
|
|
37
|
+
: firstLine(stderr) || error.message, stderr, missing ? null : (error.code ?? null)));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
resolve(stdout.replace(/\n$/, ""));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return {
|
|
44
|
+
run,
|
|
45
|
+
async tryRun(args) {
|
|
46
|
+
try {
|
|
47
|
+
return await run(args);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** The line a person should see first — Git puts its verdict there. */
|
|
56
|
+
export function firstLine(text) {
|
|
57
|
+
return (text
|
|
58
|
+
.split("\n")
|
|
59
|
+
.map((line) => line.trim())
|
|
60
|
+
.find((line) => line !== "") ?? "");
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=git.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.js","sourceRoot":"","sources":["../../src/lib/git.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,MAAM,OAAO,QAAS,SAAQ,KAAK;IAItB;IACA;IAJF,IAAI,GAAG,UAAU,CAAC;IAC3B,YACE,OAAe,EACN,MAAc,EACd,QAAuB;QAEhC,KAAK,CAAC,OAAO,CAAC,CAAC;QAHN,WAAM,GAAN,MAAM,CAAQ;QACd,aAAQ,GAAR,QAAQ,CAAe;IAGlC,CAAC;CACF;AASD,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,GAAG,GAAG,CAAC,IAAc,EAAmB,EAAE,CAC9C,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC9B,QAAQ,CACN,KAAK,EACL,IAAI,EACJ;YACE,GAAG;YACH,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,mBAAmB,EAAE,GAAG,EAAE;YACjD,OAAO,EAAE,MAAM;YACf,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI;SAC5B,EACD,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,OAAO,GAAI,KAA+B,CAAC,IAAI,KAAK,QAAQ,CAAC;gBACnE,MAAM,CACJ,IAAI,QAAQ,CACV,OAAO;oBACL,CAAC,CAAC,2CAA2C;oBAC7C,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,EACtC,MAAM,EACN,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,KAA2B,CAAC,IAAI,IAAI,IAAI,CAAC,CAC7D,CACF,CAAC;gBACF,OAAO;YACT,CAAC;YACD,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC;QACrC,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEL,OAAO;QACL,GAAG;QACH,KAAK,CAAC,MAAM,CAAC,IAAI;YACf,IAAI,CAAC;gBACH,OAAO,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,CACL,IAAI;SACD,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC,IAAI,EAAE,CACrC,CAAC;AACJ,CAAC"}
|
package/dist/lib/id.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ULID generation.
|
|
3
|
+
*
|
|
4
|
+
* Lexicographically sortable, so `ORDER BY id` matches creation order and
|
|
5
|
+
* pagination stays stable without a secondary sort. This is an identifier
|
|
6
|
+
* encoding, not a cryptographic construction — the randomness comes from
|
|
7
|
+
* `crypto.randomBytes`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ulid(now?: number): string;
|
|
10
|
+
export declare function isUlid(value: string): boolean;
|
|
11
|
+
/** URL-safe random token for sessions and one-time links. */
|
|
12
|
+
export declare function randomToken(bytes?: number): string;
|
|
13
|
+
//# sourceMappingURL=id.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.d.ts","sourceRoot":"","sources":["../../src/lib/id.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAsCH,wBAAgB,IAAI,CAAC,GAAG,SAAa,GAAG,MAAM,CAQ7C;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED,6DAA6D;AAC7D,wBAAgB,WAAW,CAAC,KAAK,SAAK,GAAG,MAAM,CAE9C"}
|
package/dist/lib/id.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ULID generation.
|
|
3
|
+
*
|
|
4
|
+
* Lexicographically sortable, so `ORDER BY id` matches creation order and
|
|
5
|
+
* pagination stays stable without a secondary sort. This is an identifier
|
|
6
|
+
* encoding, not a cryptographic construction — the randomness comes from
|
|
7
|
+
* `crypto.randomBytes`.
|
|
8
|
+
*/
|
|
9
|
+
import { randomBytes } from "node:crypto";
|
|
10
|
+
const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; // Crockford base32
|
|
11
|
+
const TIME_CHARS = 10;
|
|
12
|
+
const RANDOM_CHARS = 16;
|
|
13
|
+
let lastTime = 0;
|
|
14
|
+
let lastRandom = [];
|
|
15
|
+
function encodeTime(time) {
|
|
16
|
+
let out = "";
|
|
17
|
+
for (let i = TIME_CHARS - 1; i >= 0; i--) {
|
|
18
|
+
out = ALPHABET[time % 32] + out;
|
|
19
|
+
time = Math.floor(time / 32);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
function randomChars() {
|
|
24
|
+
const bytes = randomBytes(RANDOM_CHARS);
|
|
25
|
+
return Array.from(bytes, (byte) => byte % 32);
|
|
26
|
+
}
|
|
27
|
+
/** Increments the random component so ids created in the same millisecond still sort. */
|
|
28
|
+
function bumpRandom(chars) {
|
|
29
|
+
const next = [...chars];
|
|
30
|
+
for (let i = next.length - 1; i >= 0; i--) {
|
|
31
|
+
if (next[i] < 31) {
|
|
32
|
+
next[i] = next[i] + 1;
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
35
|
+
next[i] = 0;
|
|
36
|
+
}
|
|
37
|
+
return randomChars();
|
|
38
|
+
}
|
|
39
|
+
export function ulid(now = Date.now()) {
|
|
40
|
+
if (now === lastTime) {
|
|
41
|
+
lastRandom = bumpRandom(lastRandom);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
lastTime = now;
|
|
45
|
+
lastRandom = randomChars();
|
|
46
|
+
}
|
|
47
|
+
return encodeTime(now) + lastRandom.map((index) => ALPHABET[index]).join("");
|
|
48
|
+
}
|
|
49
|
+
export function isUlid(value) {
|
|
50
|
+
return value.length === 26 && [...value].every((char) => ALPHABET.includes(char));
|
|
51
|
+
}
|
|
52
|
+
/** URL-safe random token for sessions and one-time links. */
|
|
53
|
+
export function randomToken(bytes = 32) {
|
|
54
|
+
return randomBytes(bytes).toString("base64url");
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/lib/id.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,QAAQ,GAAG,kCAAkC,CAAC,CAAC,mBAAmB;AACxE,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,YAAY,GAAG,EAAE,CAAC;AAExB,IAAI,QAAQ,GAAG,CAAC,CAAC;AACjB,IAAI,UAAU,GAAa,EAAE,CAAC;AAE9B,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,UAAU,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,GAAG,GAAG,QAAQ,CAAC,IAAI,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC;QAChC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,KAAK,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC;IACxC,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,EAAE,CAAC,CAAC;AAChD,CAAC;AAED,yFAAyF;AACzF,SAAS,UAAU,CAAC,KAAe;IACjC,MAAM,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IACxB,KAAK,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,IAAI,IAAI,CAAC,CAAC,CAAE,GAAG,EAAE,EAAE,CAAC;YAClB,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAE,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QACD,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACd,CAAC;IACD,OAAO,WAAW,EAAE,CAAC;AACvB,CAAC;AAED,MAAM,UAAU,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACnC,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;QACrB,UAAU,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;IACtC,CAAC;SAAM,CAAC;QACN,QAAQ,GAAG,GAAG,CAAC;QACf,UAAU,GAAG,WAAW,EAAE,CAAC;IAC7B,CAAC;IACD,OAAO,UAAU,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC/E,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO,KAAK,CAAC,MAAM,KAAK,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;AACpF,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,WAAW,CAAC,KAAK,GAAG,EAAE;IACpC,OAAO,WAAW,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging.
|
|
3
|
+
*
|
|
4
|
+
* JSON lines in production so a log shipper can parse them; a compact
|
|
5
|
+
* human-readable form in development.
|
|
6
|
+
*/
|
|
7
|
+
export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
|
|
8
|
+
export interface Logger {
|
|
9
|
+
debug(message: string, fields?: Record<string, unknown>): void;
|
|
10
|
+
info(message: string, fields?: Record<string, unknown>): void;
|
|
11
|
+
warn(message: string, fields?: Record<string, unknown>): void;
|
|
12
|
+
error(message: string, fields?: Record<string, unknown>): void;
|
|
13
|
+
child(bindings: Record<string, unknown>): Logger;
|
|
14
|
+
}
|
|
15
|
+
export declare function createLogger(options: {
|
|
16
|
+
level: LogLevel;
|
|
17
|
+
pretty: boolean;
|
|
18
|
+
bindings?: Record<string, unknown>;
|
|
19
|
+
}): Logger;
|
|
20
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAUtE,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC9D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/D,KAAK,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;CAClD;AASD,wBAAgB,YAAY,CAAC,OAAO,EAAE;IACpC,KAAK,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC,GAAG,MAAM,CAkCT"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured logging.
|
|
3
|
+
*
|
|
4
|
+
* JSON lines in production so a log shipper can parse them; a compact
|
|
5
|
+
* human-readable form in development.
|
|
6
|
+
*/
|
|
7
|
+
const ORDER = {
|
|
8
|
+
debug: 10,
|
|
9
|
+
info: 20,
|
|
10
|
+
warn: 30,
|
|
11
|
+
error: 40,
|
|
12
|
+
silent: 100,
|
|
13
|
+
};
|
|
14
|
+
const COLORS = {
|
|
15
|
+
debug: "[90m",
|
|
16
|
+
info: "[36m",
|
|
17
|
+
warn: "[33m",
|
|
18
|
+
error: "[31m",
|
|
19
|
+
};
|
|
20
|
+
export function createLogger(options) {
|
|
21
|
+
const threshold = ORDER[options.level];
|
|
22
|
+
const bindings = options.bindings ?? {};
|
|
23
|
+
const write = (level, message, fields) => {
|
|
24
|
+
if (ORDER[level] < threshold)
|
|
25
|
+
return;
|
|
26
|
+
const payload = { ...bindings, ...fields };
|
|
27
|
+
const stream = level === "error" || level === "warn" ? process.stderr : process.stdout;
|
|
28
|
+
if (options.pretty) {
|
|
29
|
+
const extra = Object.keys(payload).length > 0 ? ` ${formatFields(payload)}` : "";
|
|
30
|
+
stream.write(`${COLORS[level]}${level.padEnd(5)}[0m ${message}${extra}\n`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
stream.write(`${JSON.stringify({
|
|
34
|
+
time: new Date().toISOString(),
|
|
35
|
+
level,
|
|
36
|
+
message,
|
|
37
|
+
...payload,
|
|
38
|
+
})}\n`);
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
debug: (message, fields) => write("debug", message, fields),
|
|
42
|
+
info: (message, fields) => write("info", message, fields),
|
|
43
|
+
warn: (message, fields) => write("warn", message, fields),
|
|
44
|
+
error: (message, fields) => write("error", message, fields),
|
|
45
|
+
child: (extra) => createLogger({ ...options, bindings: { ...bindings, ...extra } }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function formatFields(fields) {
|
|
49
|
+
return Object.entries(fields)
|
|
50
|
+
.map(([key, value]) => `[90m${key}=[0m${format(value)}`)
|
|
51
|
+
.join(" ");
|
|
52
|
+
}
|
|
53
|
+
function format(value) {
|
|
54
|
+
if (typeof value === "string")
|
|
55
|
+
return value;
|
|
56
|
+
if (value instanceof Error)
|
|
57
|
+
return value.stack ?? value.message;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.stringify(value) ?? String(value);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return String(value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/lib/logger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The port Mica listens on when nobody says otherwise.
|
|
3
|
+
*
|
|
4
|
+
* 6422 is `MICA` on a telephone keypad, which is the only reason anybody will
|
|
5
|
+
* remember it. What matters more is what it is not: 3000, 4000, 4173, 4200,
|
|
6
|
+
* 4321, 5000, 5173, 8000, 8080 and 1313 all belong to something a developer is
|
|
7
|
+
* likely to be running beside Mica — Astro's dev server most of all — and IANA
|
|
8
|
+
* has 6420 and 6421 assigned but not 6422, so nothing on the machine is
|
|
9
|
+
* expected to want it.
|
|
10
|
+
*
|
|
11
|
+
* A port collision is a bad first minute: the command fails, and the reason is
|
|
12
|
+
* somebody else's software. Picking an unfashionable number is the whole fix.
|
|
13
|
+
*/
|
|
14
|
+
export declare const DEFAULT_PORT = 6422;
|
|
15
|
+
/** The address the CLI prints and the browser opens. */
|
|
16
|
+
export declare const DEFAULT_BASE_URL = "http://localhost:6422";
|
|
17
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../../src/lib/ports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,wDAAwD;AACxD,eAAO,MAAM,gBAAgB,0BAAqC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The port Mica listens on when nobody says otherwise.
|
|
3
|
+
*
|
|
4
|
+
* 6422 is `MICA` on a telephone keypad, which is the only reason anybody will
|
|
5
|
+
* remember it. What matters more is what it is not: 3000, 4000, 4173, 4200,
|
|
6
|
+
* 4321, 5000, 5173, 8000, 8080 and 1313 all belong to something a developer is
|
|
7
|
+
* likely to be running beside Mica — Astro's dev server most of all — and IANA
|
|
8
|
+
* has 6420 and 6421 assigned but not 6422, so nothing on the machine is
|
|
9
|
+
* expected to want it.
|
|
10
|
+
*
|
|
11
|
+
* A port collision is a bad first minute: the command fails, and the reason is
|
|
12
|
+
* somebody else's software. Picking an unfashionable number is the whole fix.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_PORT = 6422;
|
|
15
|
+
/** The address the CLI prints and the browser opens. */
|
|
16
|
+
export const DEFAULT_BASE_URL = `http://localhost:${DEFAULT_PORT}`;
|
|
17
|
+
//# sourceMappingURL=ports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../../src/lib/ports.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AAEjC,wDAAwD;AACxD,MAAM,CAAC,MAAM,gBAAgB,GAAG,oBAAoB,YAAY,EAAE,CAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed-window rate limiting, in memory.
|
|
3
|
+
*
|
|
4
|
+
* The CMS is a single process by design, so an in-memory counter is
|
|
5
|
+
* sufficient and avoids a Redis dependency. The trade-off is deliberate:
|
|
6
|
+
* counters reset on restart. Login is additionally protected by scrypt's cost,
|
|
7
|
+
* so a restart-and-retry loop gains an attacker very little.
|
|
8
|
+
*/
|
|
9
|
+
export interface RateLimitResult {
|
|
10
|
+
allowed: boolean;
|
|
11
|
+
remaining: number;
|
|
12
|
+
/** Seconds until the window resets. */
|
|
13
|
+
retryAfter: number;
|
|
14
|
+
}
|
|
15
|
+
export interface RateLimiter {
|
|
16
|
+
check(key: string): RateLimitResult;
|
|
17
|
+
/** Forgets a key — called after a successful login. */
|
|
18
|
+
reset(key: string): void;
|
|
19
|
+
size(): number;
|
|
20
|
+
stop(): void;
|
|
21
|
+
}
|
|
22
|
+
export interface RateLimiterOptions {
|
|
23
|
+
limit: number;
|
|
24
|
+
windowMs: number;
|
|
25
|
+
/** How often expired buckets are swept. */
|
|
26
|
+
sweepMs?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function createRateLimiter(options: RateLimiterOptions): RateLimiter;
|
|
29
|
+
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/lib/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,uCAAuC;IACvC,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,uDAAuD;IACvD,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,IAAI,IAAI,MAAM,CAAC;IACf,IAAI,IAAI,IAAI,CAAC;CACd;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CA8C1E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixed-window rate limiting, in memory.
|
|
3
|
+
*
|
|
4
|
+
* The CMS is a single process by design, so an in-memory counter is
|
|
5
|
+
* sufficient and avoids a Redis dependency. The trade-off is deliberate:
|
|
6
|
+
* counters reset on restart. Login is additionally protected by scrypt's cost,
|
|
7
|
+
* so a restart-and-retry loop gains an attacker very little.
|
|
8
|
+
*/
|
|
9
|
+
export function createRateLimiter(options) {
|
|
10
|
+
const buckets = new Map();
|
|
11
|
+
const sweep = setInterval(() => {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [key, bucket] of buckets) {
|
|
14
|
+
if (bucket.resetAt <= now)
|
|
15
|
+
buckets.delete(key);
|
|
16
|
+
}
|
|
17
|
+
}, options.sweepMs ?? Math.max(options.windowMs, 60_000));
|
|
18
|
+
// A sweep timer must not keep the process alive on shutdown.
|
|
19
|
+
sweep.unref?.();
|
|
20
|
+
return {
|
|
21
|
+
check(key) {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
const bucket = buckets.get(key);
|
|
24
|
+
if (!bucket || bucket.resetAt <= now) {
|
|
25
|
+
buckets.set(key, { count: 1, resetAt: now + options.windowMs });
|
|
26
|
+
return { allowed: true, remaining: options.limit - 1, retryAfter: 0 };
|
|
27
|
+
}
|
|
28
|
+
bucket.count++;
|
|
29
|
+
const retryAfter = Math.ceil((bucket.resetAt - now) / 1000);
|
|
30
|
+
if (bucket.count > options.limit) {
|
|
31
|
+
return { allowed: false, remaining: 0, retryAfter };
|
|
32
|
+
}
|
|
33
|
+
return { allowed: true, remaining: options.limit - bucket.count, retryAfter };
|
|
34
|
+
},
|
|
35
|
+
reset(key) {
|
|
36
|
+
buckets.delete(key);
|
|
37
|
+
},
|
|
38
|
+
size() {
|
|
39
|
+
return buckets.size;
|
|
40
|
+
},
|
|
41
|
+
stop() {
|
|
42
|
+
clearInterval(sweep);
|
|
43
|
+
buckets.clear();
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/lib/rate-limit.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAwBH,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,MAAM,OAAO,GAAG,IAAI,GAAG,EAA8C,CAAC;IAEtE,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACpC,IAAI,MAAM,CAAC,OAAO,IAAI,GAAG;gBAAE,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjD,CAAC;IACH,CAAC,EAAE,OAAO,CAAC,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;IAE1D,6DAA6D;IAC7D,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IAEhB,OAAO;QACL,KAAK,CAAC,GAAG;YACP,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAEhC,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,IAAI,GAAG,EAAE,CAAC;gBACrC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;gBAChE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC;YACxE,CAAC;YAED,MAAM,CAAC,KAAK,EAAE,CAAC;YACf,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;YAE5D,IAAI,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;gBACjC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC;YACtD,CAAC;YAED,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,UAAU,EAAE,CAAC;QAChF,CAAC;QAED,KAAK,CAAC,GAAG;YACP,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,CAAC;QAED,IAAI;YACF,OAAO,OAAO,CAAC,IAAI,CAAC;QACtB,CAAC;QAED,IAAI;YACF,aAAa,CAAC,KAAK,CAAC,CAAC;YACrB,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC"}
|