@chaos-overlords/bug-reports 0.1.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 +674 -0
- package/NOTICE +21 -0
- package/README.md +27 -0
- package/dist/blobs.d.ts +17 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +46 -0
- package/dist/blobs.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/node/index.d.ts +24 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +67 -0
- package/dist/node/index.js.map +1 -0
- package/dist/ports.d.ts +44 -0
- package/dist/ports.d.ts.map +1 -0
- package/dist/ports.js +2 -0
- package/dist/ports.js.map +1 -0
- package/dist/repository.d.ts +11 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +66 -0
- package/dist/repository.js.map +1 -0
- package/dist/schema.d.ts +278 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +44 -0
- package/dist/schema.js.map +1 -0
- package/dist/service.d.ts +50 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +140 -0
- package/dist/service.js.map +1 -0
- package/migrations/sqlite/0000_fine_squadron_supreme.sql +18 -0
- package/migrations/sqlite/meta/0000_snapshot.json +132 -0
- package/migrations/sqlite/meta/_journal.json +13 -0
- package/package.json +85 -0
package/NOTICE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Chaos Overlords: New Chrome
|
|
2
|
+
A clean-room MonoGame reimplementation of the 1996 turn-based strategy game.
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 kibertoad
|
|
5
|
+
|
|
6
|
+
This program is free software: you can redistribute it and/or modify it under
|
|
7
|
+
the terms of the GNU General Public License as published by the Free Software
|
|
8
|
+
Foundation, version 3 of the License.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
12
|
+
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
You should have received a copy of the GNU General Public License along with
|
|
15
|
+
this program in the LICENSE file. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
The GPL covers the original code in this repository. It does not cover or grant
|
|
18
|
+
rights to the original Chaos Overlords assets. Those assets are not distributed
|
|
19
|
+
by this project and must be imported locally from a legally owned copy.
|
|
20
|
+
|
|
21
|
+
Project contact: https://github.com/kibertoad/chaos-overlords-new-chrome/issues
|
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @chaos-overlords/bug-reports
|
|
2
|
+
|
|
3
|
+
Bug report intake for the [*Chaos Overlords: New Chrome*](https://github.com/kibertoad/chaos-overlords-new-chrome) multiplayer server.
|
|
4
|
+
|
|
5
|
+
Bug report intake in a database of its own: schema, migration lineage, repository, blob store
|
|
6
|
+
and service. It deliberately shares nothing with the multiplayer storage — reports arrive
|
|
7
|
+
unauthenticated, outlive the matches they describe, and carry other players' game journals.
|
|
8
|
+
|
|
9
|
+
The migration lineage ships under `migrations/sqlite`. `better-sqlite3` is an optional peer.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @chaos-overlords/bug-reports
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Requires Node.js 22 or newer.
|
|
18
|
+
|
|
19
|
+
## Documentation
|
|
20
|
+
|
|
21
|
+
The operator and contributor manual for the whole multiplayer workspace is in
|
|
22
|
+
[`multiplayer/README.md`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/multiplayer/README.md); the design notes are in
|
|
23
|
+
[`docs/MULTIPLAYER.md`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/docs/MULTIPLAYER.md).
|
|
24
|
+
|
|
25
|
+
## Licence
|
|
26
|
+
|
|
27
|
+
GPL-3.0-only. See [`LICENSE`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/LICENSE) and [`NOTICE`](https://github.com/kibertoad/chaos-overlords-new-chrome/blob/main/NOTICE).
|
package/dist/blobs.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { R2Bucket } from '@cloudflare/workers-types';
|
|
2
|
+
import type { BlobStore } from './ports';
|
|
3
|
+
/** What an archive is stored as, wherever it lands. */
|
|
4
|
+
export declare const ARCHIVE_CONTENT_TYPE = "application/octet-stream";
|
|
5
|
+
/**
|
|
6
|
+
* The key an archive is filed under.
|
|
7
|
+
*
|
|
8
|
+
* Sharded by date so a bucket listing stays browsable after a few thousand reports, and suffixed
|
|
9
|
+
* with the report id, which is already a random UUID — nothing in the key is derived from the
|
|
10
|
+
* report's contents, so a key cannot leak one.
|
|
11
|
+
*/
|
|
12
|
+
export declare function archiveKey(id: string, receivedAt: Date): string;
|
|
13
|
+
/** Cloudflare R2, which is where a public deployment puts the archives. */
|
|
14
|
+
export declare function createR2BlobStore(bucket: R2Bucket): BlobStore;
|
|
15
|
+
/** An in-process store, for tests and for a `sqlite::memory:` development server. */
|
|
16
|
+
export declare function createMemoryBlobStore(): BlobStore;
|
|
17
|
+
//# sourceMappingURL=blobs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blobs.d.ts","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAExC,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,6BAA6B,CAAA;AAE9D;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,IAAI,GAAG,MAAM,CAG/D;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAc7D;AAED,qFAAqF;AACrF,wBAAgB,qBAAqB,IAAI,SAAS,CAajD"}
|
package/dist/blobs.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** What an archive is stored as, wherever it lands. */
|
|
2
|
+
export const ARCHIVE_CONTENT_TYPE = 'application/octet-stream';
|
|
3
|
+
/**
|
|
4
|
+
* The key an archive is filed under.
|
|
5
|
+
*
|
|
6
|
+
* Sharded by date so a bucket listing stays browsable after a few thousand reports, and suffixed
|
|
7
|
+
* with the report id, which is already a random UUID — nothing in the key is derived from the
|
|
8
|
+
* report's contents, so a key cannot leak one.
|
|
9
|
+
*/
|
|
10
|
+
export function archiveKey(id, receivedAt) {
|
|
11
|
+
const day = receivedAt.toISOString().slice(0, 10);
|
|
12
|
+
return `bug-reports/${day}/${id}.rchjournal`;
|
|
13
|
+
}
|
|
14
|
+
/** Cloudflare R2, which is where a public deployment puts the archives. */
|
|
15
|
+
export function createR2BlobStore(bucket) {
|
|
16
|
+
return {
|
|
17
|
+
async put(key, bytes, contentType) {
|
|
18
|
+
await bucket.put(key, bytes, { httpMetadata: { contentType } });
|
|
19
|
+
},
|
|
20
|
+
async get(key) {
|
|
21
|
+
const object = await bucket.get(key);
|
|
22
|
+
if (!object)
|
|
23
|
+
return null;
|
|
24
|
+
return new Uint8Array(await object.arrayBuffer());
|
|
25
|
+
},
|
|
26
|
+
async delete(key) {
|
|
27
|
+
await bucket.delete(key);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** An in-process store, for tests and for a `sqlite::memory:` development server. */
|
|
32
|
+
export function createMemoryBlobStore() {
|
|
33
|
+
const objects = new Map();
|
|
34
|
+
return {
|
|
35
|
+
async put(key, bytes) {
|
|
36
|
+
objects.set(key, bytes.slice());
|
|
37
|
+
},
|
|
38
|
+
async get(key) {
|
|
39
|
+
return objects.get(key) ?? null;
|
|
40
|
+
},
|
|
41
|
+
async delete(key) {
|
|
42
|
+
objects.delete(key);
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=blobs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blobs.js","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"AAGA,uDAAuD;AACvD,MAAM,CAAC,MAAM,oBAAoB,GAAG,0BAA0B,CAAA;AAE9D;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,EAAU,EAAE,UAAgB;IACrD,MAAM,GAAG,GAAG,UAAU,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IACjD,OAAO,eAAe,GAAG,IAAI,EAAE,aAAa,CAAA;AAC9C,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,iBAAiB,CAAC,MAAgB;IAChD,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,WAAW;YAC/B,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,YAAY,EAAE,EAAE,WAAW,EAAE,EAAE,CAAC,CAAA;QACjE,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,GAAG;YACX,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACpC,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAA;YACxB,OAAO,IAAI,UAAU,CAAC,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC,CAAA;QACnD,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,GAAG;YACd,MAAM,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAC1B,CAAC;KACF,CAAA;AACH,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,qBAAqB;IACnC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAA;IAC7C,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK;YAClB,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAA;QACjC,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,GAAG;YACX,OAAO,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAA;QACjC,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,GAAG;YACd,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACrB,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { ARCHIVE_CONTENT_TYPE, archiveKey, createMemoryBlobStore, createR2BlobStore, } from './blobs';
|
|
2
|
+
export type { BlobStore, BugReportRepository, StoredBugReport, StoredBugReportState, } from './ports';
|
|
3
|
+
export { type BugReportDatabase, createBugReportRepository } from './repository';
|
|
4
|
+
export * as bugReportSchema from './schema';
|
|
5
|
+
export { type BugReportService, type BugReportServiceDeps, createBugReportService, decodeBase64, } from './service';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,SAAS,CAAA;AAChB,YAAY,EACV,SAAS,EACT,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,SAAS,CAAA;AAChB,OAAO,EAAE,KAAK,iBAAiB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAA;AAChF,OAAO,KAAK,eAAe,MAAM,UAAU,CAAA;AAC3C,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,sBAAsB,EACtB,YAAY,GACb,MAAM,WAAW,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { ARCHIVE_CONTENT_TYPE, archiveKey, createMemoryBlobStore, createR2BlobStore, } from './blobs';
|
|
2
|
+
export { createBugReportRepository } from './repository';
|
|
3
|
+
export * as bugReportSchema from './schema';
|
|
4
|
+
export { createBugReportService, decodeBase64, } from './service';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oBAAoB,EACpB,UAAU,EACV,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,SAAS,CAAA;AAOhB,OAAO,EAA0B,yBAAyB,EAAE,MAAM,cAAc,CAAA;AAChF,OAAO,KAAK,eAAe,MAAM,UAAU,CAAA;AAC3C,OAAO,EAGL,sBAAsB,EACtB,YAAY,GACb,MAAM,WAAW,CAAA"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BlobStore, BugReportRepository } from '../ports';
|
|
2
|
+
/** This area's own migration lineage, resolved from the package rather than guessed relatively. */
|
|
3
|
+
export declare const BUG_REPORT_MIGRATIONS_DIR: string;
|
|
4
|
+
export interface OpenedBugReportStorage {
|
|
5
|
+
repository: BugReportRepository;
|
|
6
|
+
close(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Opens the bug report database — a *different* file from the multiplayer one — and migrates it.
|
|
10
|
+
*
|
|
11
|
+
* Two connections to two files rather than two schemas in one: a self-hosted operator who decides
|
|
12
|
+
* they would rather not keep bug reports can delete the file, and one who wants to keep them
|
|
13
|
+
* forever is not fighting the match retention sweep to do it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function openBugReportStorage(filename: string): OpenedBugReportStorage;
|
|
16
|
+
/**
|
|
17
|
+
* A directory of archives, for a server hosting its own reports.
|
|
18
|
+
*
|
|
19
|
+
* Keys come from `archiveKey`, which builds them out of a date and a UUID, so nothing a reporter
|
|
20
|
+
* sends reaches this path. The traversal guard is here anyway: this writes to a filesystem, and the
|
|
21
|
+
* one place a key could ever start coming from somewhere else is a future change to that function.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createFileBlobStore(root: string): BlobStore;
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAA;AAI9D,mGAAmG;AACnG,eAAO,MAAM,yBAAyB,QAErC,CAAA;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,mBAAmB,CAAA;IAC/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,sBAAsB,CAW7E;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CA2B3D"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, join, normalize, resolve, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import BetterSqlite3 from 'better-sqlite3';
|
|
5
|
+
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
6
|
+
import { migrate } from 'drizzle-orm/better-sqlite3/migrator';
|
|
7
|
+
import { createBugReportRepository } from '../repository';
|
|
8
|
+
import * as schema from '../schema';
|
|
9
|
+
/** This area's own migration lineage, resolved from the package rather than guessed relatively. */
|
|
10
|
+
export const BUG_REPORT_MIGRATIONS_DIR = fileURLToPath(new URL('../../migrations/sqlite', import.meta.url));
|
|
11
|
+
/**
|
|
12
|
+
* Opens the bug report database — a *different* file from the multiplayer one — and migrates it.
|
|
13
|
+
*
|
|
14
|
+
* Two connections to two files rather than two schemas in one: a self-hosted operator who decides
|
|
15
|
+
* they would rather not keep bug reports can delete the file, and one who wants to keep them
|
|
16
|
+
* forever is not fighting the match retention sweep to do it.
|
|
17
|
+
*/
|
|
18
|
+
export function openBugReportStorage(filename) {
|
|
19
|
+
const connection = new BetterSqlite3(filename);
|
|
20
|
+
connection.pragma('journal_mode = WAL');
|
|
21
|
+
const db = drizzle(connection, { schema });
|
|
22
|
+
migrate(db, { migrationsFolder: BUG_REPORT_MIGRATIONS_DIR });
|
|
23
|
+
return {
|
|
24
|
+
repository: createBugReportRepository(db),
|
|
25
|
+
close: async () => {
|
|
26
|
+
connection.close();
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A directory of archives, for a server hosting its own reports.
|
|
32
|
+
*
|
|
33
|
+
* Keys come from `archiveKey`, which builds them out of a date and a UUID, so nothing a reporter
|
|
34
|
+
* sends reaches this path. The traversal guard is here anyway: this writes to a filesystem, and the
|
|
35
|
+
* one place a key could ever start coming from somewhere else is a future change to that function.
|
|
36
|
+
*/
|
|
37
|
+
export function createFileBlobStore(root) {
|
|
38
|
+
const base = resolve(root);
|
|
39
|
+
const pathFor = (key) => {
|
|
40
|
+
const full = normalize(join(base, key));
|
|
41
|
+
if (!full.startsWith(base + sep) || isAbsolute(key)) {
|
|
42
|
+
throw new Error(`Refusing a blob key that escapes the store: ${key}`);
|
|
43
|
+
}
|
|
44
|
+
return full;
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
async put(key, bytes) {
|
|
48
|
+
const path = pathFor(key);
|
|
49
|
+
await mkdir(dirname(path), { recursive: true });
|
|
50
|
+
await writeFile(path, bytes);
|
|
51
|
+
},
|
|
52
|
+
async get(key) {
|
|
53
|
+
try {
|
|
54
|
+
return new Uint8Array(await readFile(pathFor(key)));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (error.code === 'ENOENT')
|
|
58
|
+
return null;
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
async delete(key) {
|
|
63
|
+
await rm(pathFor(key), { force: true });
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAA;AAC9E,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,aAAa,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAA;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,qCAAqC,CAAA;AAE7D,OAAO,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAA;AACzD,OAAO,KAAK,MAAM,MAAM,WAAW,CAAA;AAEnC,mGAAmG;AACnG,MAAM,CAAC,MAAM,yBAAyB,GAAG,aAAa,CACpD,IAAI,GAAG,CAAC,yBAAyB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CACpD,CAAA;AAOD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAAgB;IACnD,MAAM,UAAU,GAAG,IAAI,aAAa,CAAC,QAAQ,CAAC,CAAA;IAC9C,UAAU,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAA;IACvC,MAAM,EAAE,GAAG,OAAO,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IAC1C,OAAO,CAAC,EAAE,EAAE,EAAE,gBAAgB,EAAE,yBAAyB,EAAE,CAAC,CAAA;IAC5D,OAAO;QACL,UAAU,EAAE,yBAAyB,CAAC,EAAE,CAAC;QACzC,KAAK,EAAE,KAAK,IAAI,EAAE;YAChB,UAAU,CAAC,KAAK,EAAE,CAAA;QACpB,CAAC;KACF,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,MAAM,OAAO,GAAG,CAAC,GAAW,EAAU,EAAE;QACtC,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAA;QACvC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CAAC,+CAA+C,GAAG,EAAE,CAAC,CAAA;QACvE,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC,CAAA;IACD,OAAO;QACL,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK;YAClB,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;YACzB,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;YAC/C,MAAM,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QAC9B,CAAC;QACD,KAAK,CAAC,GAAG,CAAC,GAAG;YACX,IAAI,CAAC;gBACH,OAAO,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YACrD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ;oBAAE,OAAO,IAAI,CAAA;gBACnE,MAAM,KAAK,CAAA;YACb,CAAC;QACH,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,GAAG;YACd,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QACzC,CAAC;KACF,CAAA;AACH,CAAC"}
|
package/dist/ports.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { BugReportCodec, SubmitBugReportRequest } from '@chaos-overlords/contracts';
|
|
2
|
+
/** A stored report, as the service writes it and a triage reader gets it back. */
|
|
3
|
+
export interface StoredBugReport {
|
|
4
|
+
id: string;
|
|
5
|
+
receivedAt: Date;
|
|
6
|
+
message: string;
|
|
7
|
+
clientVersion: string;
|
|
8
|
+
clientPlatform: string;
|
|
9
|
+
context: SubmitBugReportRequest['context'] | null;
|
|
10
|
+
state: StoredBugReportState | null;
|
|
11
|
+
}
|
|
12
|
+
/** Where the archive came to rest, and what it is. */
|
|
13
|
+
export interface StoredBugReportState {
|
|
14
|
+
codec: BugReportCodec;
|
|
15
|
+
replayFormatVersion: number;
|
|
16
|
+
uncompressedBytes: number;
|
|
17
|
+
compressedBytes: number;
|
|
18
|
+
sha256: string;
|
|
19
|
+
anonymized: boolean;
|
|
20
|
+
/** The object key when it went to the blob store, or null when `body` holds it instead. */
|
|
21
|
+
blobKey: string | null;
|
|
22
|
+
/** The base64 archive when it was small enough to keep in the row, or null. */
|
|
23
|
+
body: string | null;
|
|
24
|
+
}
|
|
25
|
+
/** The one write and the two reads this area needs; implemented over Drizzle, faked in tests. */
|
|
26
|
+
export interface BugReportRepository {
|
|
27
|
+
insert(report: StoredBugReport): Promise<void>;
|
|
28
|
+
get(id: string): Promise<StoredBugReport | null>;
|
|
29
|
+
/** Newest first, for triage. */
|
|
30
|
+
list(limit: number): Promise<StoredBugReport[]>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Somewhere to put bytes that have no business being in a database row.
|
|
34
|
+
*
|
|
35
|
+
* R2 on Cloudflare, a directory on a self-hosted server, an in-memory map in a test. Three methods
|
|
36
|
+
* because that is all a bug report needs: it is written once, read when somebody triages it, and
|
|
37
|
+
* deleted if the report is.
|
|
38
|
+
*/
|
|
39
|
+
export interface BlobStore {
|
|
40
|
+
put(key: string, bytes: Uint8Array, contentType: string): Promise<void>;
|
|
41
|
+
get(key: string): Promise<Uint8Array | null>;
|
|
42
|
+
delete(key: string): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=ports.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.d.ts","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAA;AAExF,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,IAAI,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,EAAE,MAAM,CAAA;IACrB,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,sBAAsB,CAAC,SAAS,CAAC,GAAG,IAAI,CAAA;IACjD,KAAK,EAAE,oBAAoB,GAAG,IAAI,CAAA;CACnC;AAED,sDAAsD;AACtD,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,cAAc,CAAA;IACrB,mBAAmB,EAAE,MAAM,CAAA;IAC3B,iBAAiB,EAAE,MAAM,CAAA;IACzB,eAAe,EAAE,MAAM,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,OAAO,CAAA;IACnB,2FAA2F;IAC3F,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,+EAA+E;IAC/E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,iGAAiG;AACjG,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC9C,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAA;IAChD,gCAAgC;IAChC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAAA;CAChD;AAED;;;;;;GAMG;AACH,MAAM,WAAW,SAAS;IACxB,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAA;IAC5C,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC"}
|
package/dist/ports.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ports.js","sourceRoot":"","sources":["../src/ports.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BaseSQLiteDatabase } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
import type { BugReportRepository } from './ports';
|
|
3
|
+
import type * as schema from './schema';
|
|
4
|
+
/**
|
|
5
|
+
* Either SQLite driver, exactly as the multiplayer storage takes either: better-sqlite3 (`'sync'`)
|
|
6
|
+
* or D1 (`'async'`), with one implementation over both because awaiting a synchronous result is a
|
|
7
|
+
* no-op. One migration lineage, two drivers — and a different database from the match one.
|
|
8
|
+
*/
|
|
9
|
+
export type BugReportDatabase = BaseSQLiteDatabase<'sync' | 'async', unknown, typeof schema>;
|
|
10
|
+
export declare function createBugReportRepository(db: BugReportDatabase): BugReportRepository;
|
|
11
|
+
//# sourceMappingURL=repository.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository.d.ts","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,KAAK,EAAE,mBAAmB,EAAmB,MAAM,SAAS,CAAA;AACnE,OAAO,KAAK,KAAK,MAAM,MAAM,UAAU,CAAA;AAGvC;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,CAAC,MAAM,GAAG,OAAO,EAAE,OAAO,EAAE,OAAO,MAAM,CAAC,CAAA;AAI5F,wBAAgB,yBAAyB,CAAC,EAAE,EAAE,iBAAiB,GAAG,mBAAmB,CAqBpF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { desc, eq } from 'drizzle-orm';
|
|
2
|
+
import { bugReports } from './schema';
|
|
3
|
+
export function createBugReportRepository(db) {
|
|
4
|
+
return {
|
|
5
|
+
async insert(report) {
|
|
6
|
+
await db.insert(bugReports).values(toRow(report));
|
|
7
|
+
},
|
|
8
|
+
async get(id) {
|
|
9
|
+
const rows = await db.select().from(bugReports).where(eq(bugReports.id, id)).limit(1);
|
|
10
|
+
const row = rows[0];
|
|
11
|
+
return row ? fromRow(row) : null;
|
|
12
|
+
},
|
|
13
|
+
async list(limit) {
|
|
14
|
+
const rows = await db
|
|
15
|
+
.select()
|
|
16
|
+
.from(bugReports)
|
|
17
|
+
.orderBy(desc(bugReports.receivedAt))
|
|
18
|
+
.limit(limit);
|
|
19
|
+
return rows.map(fromRow);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function toRow(report) {
|
|
24
|
+
const state = report.state;
|
|
25
|
+
return {
|
|
26
|
+
id: report.id,
|
|
27
|
+
receivedAt: report.receivedAt,
|
|
28
|
+
message: report.message,
|
|
29
|
+
clientVersion: report.clientVersion,
|
|
30
|
+
clientPlatform: report.clientPlatform,
|
|
31
|
+
context: report.context ?? null,
|
|
32
|
+
stateCodec: state?.codec ?? null,
|
|
33
|
+
stateFormatVersion: state?.replayFormatVersion ?? null,
|
|
34
|
+
stateUncompressedBytes: state?.uncompressedBytes ?? null,
|
|
35
|
+
stateCompressedBytes: state?.compressedBytes ?? null,
|
|
36
|
+
stateSha256: state?.sha256 ?? null,
|
|
37
|
+
stateAnonymized: state?.anonymized ?? null,
|
|
38
|
+
blobKey: state?.blobKey ?? null,
|
|
39
|
+
body: state?.body ?? null,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function fromRow(row) {
|
|
43
|
+
return {
|
|
44
|
+
id: row.id,
|
|
45
|
+
receivedAt: row.receivedAt,
|
|
46
|
+
message: row.message,
|
|
47
|
+
clientVersion: row.clientVersion,
|
|
48
|
+
clientPlatform: row.clientPlatform,
|
|
49
|
+
context: row.context ?? null,
|
|
50
|
+
// Every state column is written in one go or not at all, so the codec standing in for all of
|
|
51
|
+
// them is not a guess: a row with a codec has the rest, and a row without has none of it.
|
|
52
|
+
state: row.stateCodec === null
|
|
53
|
+
? null
|
|
54
|
+
: {
|
|
55
|
+
codec: row.stateCodec,
|
|
56
|
+
replayFormatVersion: row.stateFormatVersion ?? 0,
|
|
57
|
+
uncompressedBytes: row.stateUncompressedBytes ?? 0,
|
|
58
|
+
compressedBytes: row.stateCompressedBytes ?? 0,
|
|
59
|
+
sha256: row.stateSha256 ?? '',
|
|
60
|
+
anonymized: row.stateAnonymized ?? false,
|
|
61
|
+
blobKey: row.blobKey,
|
|
62
|
+
body: row.body,
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=repository.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repository.js","sourceRoot":"","sources":["../src/repository.ts"],"names":[],"mappings":"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"}
|