@datamoc/mw_games 0.15.0 → 0.17.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/CHANGELOG.md +154 -1
- package/README.md +21 -1
- package/dist/ai/lua.d.ts.map +1 -1
- package/dist/ai/lua.js +1 -0
- package/dist/ai/lua.js.map +1 -1
- package/dist/core/ActionJournal.d.ts +5 -0
- package/dist/core/ActionJournal.d.ts.map +1 -1
- package/dist/core/ActionJournal.js +12 -1
- package/dist/core/ActionJournal.js.map +1 -1
- package/dist/core/Clone.d.ts +31 -0
- package/dist/core/Clone.d.ts.map +1 -0
- package/dist/core/Clone.js +84 -0
- package/dist/core/Clone.js.map +1 -0
- package/dist/core/Collection.d.ts +1 -0
- package/dist/core/Collection.d.ts.map +1 -1
- package/dist/core/Collection.js +6 -2
- package/dist/core/Collection.js.map +1 -1
- package/dist/core/HttpTransport.d.ts +17 -0
- package/dist/core/HttpTransport.d.ts.map +1 -1
- package/dist/core/HttpTransport.js +61 -0
- package/dist/core/HttpTransport.js.map +1 -1
- package/dist/core/Multiplayer.d.ts +9 -0
- package/dist/core/Multiplayer.d.ts.map +1 -1
- package/dist/core/Multiplayer.js +48 -4
- package/dist/core/Multiplayer.js.map +1 -1
- package/dist/core/News.d.ts.map +1 -1
- package/dist/core/News.js +1 -2
- package/dist/core/News.js.map +1 -1
- package/dist/core/Sanitize.d.ts +19 -0
- package/dist/core/Sanitize.d.ts.map +1 -1
- package/dist/core/Sanitize.js +37 -0
- package/dist/core/Sanitize.js.map +1 -1
- package/dist/core/Save.d.ts +18 -9
- package/dist/core/Save.d.ts.map +1 -1
- package/dist/core/Save.js +37 -21
- package/dist/core/Save.js.map +1 -1
- package/dist/core/SaveSync.js +4 -4
- package/dist/core/SaveSync.js.map +1 -1
- package/dist/core/Settings.d.ts.map +1 -1
- package/dist/core/Settings.js +2 -1
- package/dist/core/Settings.js.map +1 -1
- package/dist/core/State.d.ts.map +1 -1
- package/dist/core/State.js +8 -7
- package/dist/core/State.js.map +1 -1
- package/dist/core/StoredValue.d.ts +1 -1
- package/dist/core/StoredValue.d.ts.map +1 -1
- package/dist/core/StoredValue.js +3 -2
- package/dist/core/StoredValue.js.map +1 -1
- package/dist/core/Telemetry.d.ts +19 -1
- package/dist/core/Telemetry.d.ts.map +1 -1
- package/dist/core/Telemetry.js +26 -1
- package/dist/core/Telemetry.js.map +1 -1
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/mw_games.global.js +107 -107
- package/dist/mwl/MapFile.js +11 -1
- package/dist/mwl/MapFile.js.map +1 -1
- package/dist/mwl/expression.js +38 -5
- package/dist/mwl/expression.js.map +1 -1
- package/dist/mwl/fengari.d.ts +8 -0
- package/dist/mwl/fengari.d.ts.map +1 -1
- package/dist/mwl/fengari.js +72 -2
- package/dist/mwl/fengari.js.map +1 -1
- package/dist/mwl/persistence.d.ts.map +1 -1
- package/dist/mwl/persistence.js +2 -1
- package/dist/mwl/persistence.js.map +1 -1
- package/dist/mwl/runtime.d.ts.map +1 -1
- package/dist/mwl/runtime.js +6 -1
- package/dist/mwl/runtime.js.map +1 -1
- package/dist/rpg/Marshal.d.ts.map +1 -1
- package/dist/rpg/Marshal.js +13 -0
- package/dist/rpg/Marshal.js.map +1 -1
- package/dist/simulation/Campaign.d.ts.map +1 -1
- package/dist/simulation/Campaign.js +9 -5
- package/dist/simulation/Campaign.js.map +1 -1
- package/dist/simulation/Harness.d.ts.map +1 -1
- package/dist/simulation/Harness.js +2 -1
- package/dist/simulation/Harness.js.map +1 -1
- package/dist/simulation/Runtime.d.ts +10 -0
- package/dist/simulation/Runtime.d.ts.map +1 -1
- package/dist/simulation/Runtime.js +25 -4
- package/dist/simulation/Runtime.js.map +1 -1
- package/dist/testing/index.d.ts +105 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +134 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/two-d/render/RpgmAutotile.d.ts +217 -0
- package/dist/two-d/render/RpgmAutotile.d.ts.map +1 -1
- package/dist/two-d/render/RpgmAutotile.js +756 -0
- package/dist/two-d/render/RpgmAutotile.js.map +1 -1
- package/dist/two-d/render/TileMap.d.ts +90 -0
- package/dist/two-d/render/TileMap.d.ts.map +1 -1
- package/dist/two-d/render/TileMap.js +319 -31
- package/dist/two-d/render/TileMap.js.map +1 -1
- package/dist/two-d/render/index.d.ts +3 -3
- package/dist/two-d/render/index.d.ts.map +1 -1
- package/dist/two-d/render/index.js +1 -1
- package/dist/two-d/render/index.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +90 -25
- package/sbom.cdx.json +5589 -0
- package/tools/benchmark-browser.d.mts +33 -0
- package/tools/benchmark-browser.mjs +163 -0
- package/tools/benchmark-history.d.mts +8 -0
- package/tools/benchmark-history.mjs +31 -0
- package/tools/browser-smoke.d.mts +30 -0
- package/tools/browser-smoke.mjs +167 -0
- package/tools/bundle-size.d.mts +11 -0
- package/tools/bundle-size.mjs +138 -0
- package/tools/csp.d.mts +7 -0
- package/tools/csp.mjs +56 -0
- package/tools/emit-page.d.mts +44 -0
- package/tools/emit-page.mjs +188 -0
- package/tools/extract-rgssad.d.mts +5 -0
- package/tools/extract-rgssad.mjs +64 -0
- package/tools/find-chrome.d.mts +7 -0
- package/tools/find-chrome.mjs +104 -0
- package/tools/i18n-edit.d.mts +29 -0
- package/tools/i18n-edit.mjs +748 -0
- package/tools/make-example-assets.d.mts +2 -0
- package/tools/make-example-assets.mjs +727 -0
- package/tools/multiplayer-server.d.mts +27 -0
- package/tools/multiplayer-server.mjs +195 -0
- package/tools/mwl.mjs +1 -0
- package/tools/sbom.d.mts +83 -0
- package/tools/sbom.mjs +330 -0
- package/tools/vendored/rgssad-wasm/LICENSE-MIT +21 -0
- package/tools/vendored/rgssad-wasm/README.md +15 -0
- package/tools/vendored/rgssad-wasm/package.json +17 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm.d.ts +41 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm.js +6 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.js +399 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.wasm +0 -0
- package/tools/vendored/rgssad-wasm/rgssad_wasm_bg.wasm.d.ts +17 -0
- package/tools/vite.d.mts +17 -0
- package/tools/vite.mjs +83 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WebSocketServer } from 'ws';
|
|
2
|
+
|
|
3
|
+
export interface LockstepServerOptions {
|
|
4
|
+
port?: number;
|
|
5
|
+
/** the address to listen on; every interface when omitted (the `mwg-lockstep-server` command defaults to 127.0.0.1) */
|
|
6
|
+
host?: string;
|
|
7
|
+
tickTimeoutMs?: number;
|
|
8
|
+
/** the room seed sent in every `welcome` */
|
|
9
|
+
seed?: number;
|
|
10
|
+
/** the JSON-safe state every peer starts from, sent in every `welcome` */
|
|
11
|
+
initialState?: unknown;
|
|
12
|
+
/** returning `false` or a reason rejects an input */
|
|
13
|
+
validateInput?: (payload: unknown) => boolean | string;
|
|
14
|
+
/** largest client message accepted, in bytes; defaults to 64 KB */
|
|
15
|
+
maxMessageBytes?: number;
|
|
16
|
+
/** further connections to a full room are closed with 1013; defaults to 64 */
|
|
17
|
+
maxClientsPerRoom?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface LockstepServer {
|
|
21
|
+
wss: WebSocketServer;
|
|
22
|
+
ready: Promise<void>;
|
|
23
|
+
address: () => ReturnType<WebSocketServer['address']>;
|
|
24
|
+
close: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createLockstepServer(options?: LockstepServerOptions): LockstepServer;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
//the inbound pipeline every client-side reader uses: the TypeScript source inside this
|
|
6
|
+
//repository, the built `dist` inside an installed package, which ships no `src`
|
|
7
|
+
const { parseInbound } = await import(
|
|
8
|
+
existsSync(new URL('../src/core/Sanitize.ts', import.meta.url))
|
|
9
|
+
? '../src/core/Sanitize.ts'
|
|
10
|
+
: '../dist/core/index.js'
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/** `ws`, an optional peer of the published package: only a game running this server needs it */
|
|
14
|
+
const { WebSocketServer } = await import('ws').catch(() => {
|
|
15
|
+
throw new Error('the lockstep server needs ws: npm install ws');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/** a room name a client can choose: short, printable, so one client cannot mint unbounded keys */
|
|
19
|
+
const ROOM_NAME = /^[\w-]{1,64}$/;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A reference lockstep server for `core.LockstepClient` (item 129 of ROADMAP.md).
|
|
23
|
+
*
|
|
24
|
+
* `mwg` ships no backend anywhere else - `FeedbackClient`/`NewsClient`/`SaveSyncClient` all
|
|
25
|
+
* assume a game brings its own - but real-time multiplayer has no one-shot request/response
|
|
26
|
+
* shape to hide a server behind, so this is a genuine exception, not a precedent for the rest
|
|
27
|
+
* of the project: a game using this either runs it as-is or replaces it with its own server
|
|
28
|
+
* speaking the same three-message protocol (`welcome`/`input`/`tick`).
|
|
29
|
+
*
|
|
30
|
+
* Model: each `room` (a `?room=` query string, default `"default"`) advances one shared tick
|
|
31
|
+
* counter. A client's `{type: 'input', payload}` is buffered for the room's current tick; once
|
|
32
|
+
* every currently-connected client in the room has submitted one, the server broadcasts
|
|
33
|
+
* `{type: 'tick', tick, inputs}` (every client id mapped to its input, or `null` for anyone
|
|
34
|
+
* who missed the deadline) and starts the next tick. A per-room timer also forces the
|
|
35
|
+
* broadcast after `tickTimeoutMs` regardless of who has submitted, so one slow or dropped
|
|
36
|
+
* client cannot stall everyone else forever - the reconciliation rule item 129 asked for,
|
|
37
|
+
* kept as simple as a rule can be while still being real: nothing here guesses a missing
|
|
38
|
+
* input, it is reported as `null` and left to the game's own rules to interpret (skip that
|
|
39
|
+
* actor's turn, repeat its last input, whatever the game decides).
|
|
40
|
+
*/
|
|
41
|
+
export function createLockstepServer(options = {}) {
|
|
42
|
+
const {
|
|
43
|
+
port = 0,
|
|
44
|
+
host,
|
|
45
|
+
tickTimeoutMs = 200,
|
|
46
|
+
seed,
|
|
47
|
+
initialState,
|
|
48
|
+
validateInput,
|
|
49
|
+
maxMessageBytes = 64 * 1024,
|
|
50
|
+
maxClientsPerRoom = 64,
|
|
51
|
+
} = options;
|
|
52
|
+
//ws accepts 100 MiB messages unless told otherwise; an input is a few bytes
|
|
53
|
+
const wss = new WebSocketServer({ port, host, maxPayload: maxMessageBytes });
|
|
54
|
+
|
|
55
|
+
const rooms = new Map();
|
|
56
|
+
let nextId = 1;
|
|
57
|
+
|
|
58
|
+
function room(name) {
|
|
59
|
+
let entry = rooms.get(name);
|
|
60
|
+
if (!entry) {
|
|
61
|
+
//submittedCount tracks how many of entry.clients currently have hasSubmitted set,
|
|
62
|
+
//kept in step by every place that changes hasSubmitted, rather than counting the
|
|
63
|
+
//whole client map on every single input message (checked on every message, not
|
|
64
|
+
//just once per tick, so it is worth not being O(clients) each time)
|
|
65
|
+
entry = { clients: new Map(), tick: 0, timer: null, submittedCount: 0 };
|
|
66
|
+
rooms.set(name, entry);
|
|
67
|
+
}
|
|
68
|
+
return entry;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function scheduleTimeout(entry) {
|
|
72
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
73
|
+
entry.timer = entry.clients.size > 0 ? setTimeout(() => advance(entry), tickTimeoutMs) : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function advance(entry) {
|
|
77
|
+
const inputs = {};
|
|
78
|
+
const checksums = {};
|
|
79
|
+
for (const [id, client] of entry.clients) {
|
|
80
|
+
inputs[id] = client.hasSubmitted ? client.pendingInput : null;
|
|
81
|
+
if (client.hasSubmitted && Number.isSafeInteger(client.pendingChecksum))
|
|
82
|
+
checksums[id] = client.pendingChecksum;
|
|
83
|
+
client.pendingInput = null;
|
|
84
|
+
client.pendingChecksum = null;
|
|
85
|
+
client.hasSubmitted = false;
|
|
86
|
+
}
|
|
87
|
+
entry.submittedCount = 0;
|
|
88
|
+
const message = JSON.stringify({
|
|
89
|
+
type: 'tick',
|
|
90
|
+
tick: entry.tick,
|
|
91
|
+
inputs,
|
|
92
|
+
...(Object.keys(checksums).length === 0 ? {} : { checksums }),
|
|
93
|
+
});
|
|
94
|
+
for (const client of entry.clients.values()) client.socket.send(message);
|
|
95
|
+
entry.tick += 1;
|
|
96
|
+
scheduleTimeout(entry);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function maybeAdvanceEarly(entry) {
|
|
100
|
+
if (entry.clients.size > 0 && entry.submittedCount === entry.clients.size) advance(entry);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
wss.on('connection', (socket, request) => {
|
|
104
|
+
const url = new URL(request.url ?? '/', 'http://localhost');
|
|
105
|
+
const roomName = url.searchParams.get('room') ?? 'default';
|
|
106
|
+
if (!ROOM_NAME.test(roomName)) return socket.close(1008, 'invalid room name');
|
|
107
|
+
if ((rooms.get(roomName)?.clients.size ?? 0) >= maxClientsPerRoom) return socket.close(1013, 'room is full');
|
|
108
|
+
const id = `p${nextId++}`;
|
|
109
|
+
const entry = room(roomName);
|
|
110
|
+
entry.clients.set(id, { socket, pendingInput: null, hasSubmitted: false });
|
|
111
|
+
|
|
112
|
+
socket.send(
|
|
113
|
+
JSON.stringify({
|
|
114
|
+
type: 'welcome',
|
|
115
|
+
id,
|
|
116
|
+
...(seed === undefined ? {} : { seed }),
|
|
117
|
+
...(initialState === undefined ? {} : { initialState }),
|
|
118
|
+
}),
|
|
119
|
+
);
|
|
120
|
+
if (!entry.timer) scheduleTimeout(entry);
|
|
121
|
+
|
|
122
|
+
//ws reports an oversized or malformed frame as an 'error' event and closes the socket itself;
|
|
123
|
+
//with no listener, that event would throw and stop the whole server, every room with it
|
|
124
|
+
socket.on('error', () => {});
|
|
125
|
+
|
|
126
|
+
socket.on('message', (raw) => {
|
|
127
|
+
let message;
|
|
128
|
+
try {
|
|
129
|
+
message = parseInbound(String(raw), { maxBytes: maxMessageBytes, label: 'lockstep input' });
|
|
130
|
+
} catch {
|
|
131
|
+
return; //malformed input from one client must not take the room down
|
|
132
|
+
}
|
|
133
|
+
if (message?.type !== 'input') return;
|
|
134
|
+
const client = entry.clients.get(id);
|
|
135
|
+
if (!client) return;
|
|
136
|
+
const validation = validateInput?.(message.payload);
|
|
137
|
+
if (validation === false || typeof validation === 'string') {
|
|
138
|
+
socket.send(
|
|
139
|
+
JSON.stringify({
|
|
140
|
+
type: 'rejected',
|
|
141
|
+
reason: validation === false ? 'input rejected by server validation' : validation,
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
client.pendingInput = message.payload ?? null;
|
|
147
|
+
client.pendingChecksum = message.checksum;
|
|
148
|
+
if (!client.hasSubmitted) {
|
|
149
|
+
client.hasSubmitted = true;
|
|
150
|
+
entry.submittedCount += 1;
|
|
151
|
+
}
|
|
152
|
+
maybeAdvanceEarly(entry);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
socket.on('close', () => {
|
|
156
|
+
const client = entry.clients.get(id);
|
|
157
|
+
if (client?.hasSubmitted) entry.submittedCount -= 1;
|
|
158
|
+
entry.clients.delete(id);
|
|
159
|
+
if (entry.clients.size === 0) {
|
|
160
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
161
|
+
rooms.delete(roomName);
|
|
162
|
+
} else {
|
|
163
|
+
maybeAdvanceEarly(entry);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
wss,
|
|
170
|
+
/** resolves once the server is actually listening; `address().port` is only valid after this */
|
|
171
|
+
ready: new Promise((resolve) => wss.once('listening', resolve)),
|
|
172
|
+
address: () => wss.address(),
|
|
173
|
+
close: () =>
|
|
174
|
+
new Promise((resolve) => {
|
|
175
|
+
for (const entry of rooms.values()) if (entry.timer) clearTimeout(entry.timer);
|
|
176
|
+
wss.close(() => resolve());
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* `mwg-lockstep-server [port] [--host=<address>]`: listens on 127.0.0.1 unless `--host` names
|
|
183
|
+
* another address, so running it never exposes a port by accident. It speaks plain `ws:`, and
|
|
184
|
+
* `LockstepClient` refuses `ws:` to anything but this machine: a deployment puts it behind a TLS
|
|
185
|
+
* reverse proxy and connects with `wss:`.
|
|
186
|
+
*/
|
|
187
|
+
//realpath, since an npm `bin` shim reaches this file through a symlink
|
|
188
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
189
|
+
const args = process.argv.slice(2);
|
|
190
|
+
const port = Number(args.find((arg) => !arg.startsWith('--')) ?? 8787);
|
|
191
|
+
const host = args.find((arg) => arg.startsWith('--host='))?.slice(7) ?? '127.0.0.1';
|
|
192
|
+
const server = createLockstepServer({ port, host });
|
|
193
|
+
await server.ready;
|
|
194
|
+
console.log(`mwg reference lockstep server listening on ws://${host}:${server.address().port}`);
|
|
195
|
+
}
|
package/tools/mwl.mjs
CHANGED
package/tools/sbom.d.mts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface CycloneDxHash {
|
|
2
|
+
alg: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface CycloneDxLicense {
|
|
7
|
+
license: { id?: string; name?: string };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CycloneDxComponent {
|
|
11
|
+
type: 'library' | 'application' | 'file';
|
|
12
|
+
'bom-ref': string;
|
|
13
|
+
name: string;
|
|
14
|
+
/** absent on a `file` component, which is identified by its hash */
|
|
15
|
+
version?: string;
|
|
16
|
+
purl?: string;
|
|
17
|
+
scope?: 'required' | 'optional' | 'excluded';
|
|
18
|
+
hashes?: CycloneDxHash[];
|
|
19
|
+
licenses?: CycloneDxLicense[];
|
|
20
|
+
description?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CycloneDxBom {
|
|
24
|
+
bomFormat: 'CycloneDX';
|
|
25
|
+
specVersion: string;
|
|
26
|
+
serialNumber: string;
|
|
27
|
+
version: number;
|
|
28
|
+
metadata: {
|
|
29
|
+
tools: { components: CycloneDxComponent[] };
|
|
30
|
+
component: CycloneDxComponent;
|
|
31
|
+
};
|
|
32
|
+
components: CycloneDxComponent[];
|
|
33
|
+
dependencies: Array<{ ref: string; dependsOn: string[] }>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface LockfilePackage {
|
|
37
|
+
version?: string;
|
|
38
|
+
integrity?: string;
|
|
39
|
+
license?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
dev?: boolean;
|
|
42
|
+
optional?: boolean;
|
|
43
|
+
link?: boolean;
|
|
44
|
+
dependencies?: Record<string, string>;
|
|
45
|
+
devDependencies?: Record<string, string>;
|
|
46
|
+
optionalDependencies?: Record<string, string>;
|
|
47
|
+
peerDependencies?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface Lockfile {
|
|
51
|
+
packages?: Record<string, LockfilePackage>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PackageJson {
|
|
55
|
+
name: string;
|
|
56
|
+
version: string;
|
|
57
|
+
description?: string;
|
|
58
|
+
license?: string;
|
|
59
|
+
private?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function purlFor(name: string, version: string): string;
|
|
63
|
+
export function hashesFromIntegrity(integrity: unknown): CycloneDxHash[];
|
|
64
|
+
/** `type`: `'library'` (the default) or `'application'` for a game */
|
|
65
|
+
export function buildSbom(
|
|
66
|
+
packageJson: PackageJson,
|
|
67
|
+
lockfile: Lockfile,
|
|
68
|
+
options?: { type?: 'library' | 'application' },
|
|
69
|
+
): CycloneDxBom;
|
|
70
|
+
export function serialize(bom: CycloneDxBom): string;
|
|
71
|
+
|
|
72
|
+
/** the npm package a bundled module id belongs to, or null for the game's own source */
|
|
73
|
+
export function packageOfModule(id: string): { name: string; dir: string } | null;
|
|
74
|
+
|
|
75
|
+
/** the SBOM of what a build ships: the packages its modules came from, and its files with their SHA-256 */
|
|
76
|
+
export function buildArtifactSbom(input: {
|
|
77
|
+
packageJson: PackageJson;
|
|
78
|
+
modules: string[];
|
|
79
|
+
files: Array<{ path: string; sha256: string }>;
|
|
80
|
+
}): CycloneDxBom;
|
|
81
|
+
|
|
82
|
+
/** SHA-256 of every shipped file under `dist`, precompressed siblings and SBOMs left out */
|
|
83
|
+
export function shippedFiles(dist: string): Promise<Array<{ path: string; sha256: string }>>;
|
package/tools/sbom.mjs
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
4
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { join, relative, resolve, sep } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const OUTPUT = 'sbom.cdx.json';
|
|
9
|
+
/** this tool's own package, named in every document's `metadata.tools` whichever project it describes */
|
|
10
|
+
const TOOL = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
11
|
+
const SPEC_VERSION = '1.6';
|
|
12
|
+
|
|
13
|
+
const HASH_ALGORITHMS = { sha512: 'SHA-512', sha384: 'SHA-384', sha256: 'SHA-256', sha1: 'SHA-1' };
|
|
14
|
+
|
|
15
|
+
/** the RFC 4122 DNS namespace, undashed because it is only ever fed to the hash as bytes */
|
|
16
|
+
const DNS_NAMESPACE = '6ba7b8109dad11d180b400c04fd430c8';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Package URL for an npm package, the SBOM's `bom-ref` as well. A scoped name's `@` is
|
|
20
|
+
* percent-encoded but its `/` is not, which is what the purl spec's own npm examples do
|
|
21
|
+
* (`pkg:npm/%40angular/animation@12.3.1`).
|
|
22
|
+
*/
|
|
23
|
+
export function purlFor(name, version) {
|
|
24
|
+
const encoded = name.startsWith('@') ? `%40${name.slice(1)}` : name;
|
|
25
|
+
return `pkg:npm/${encoded}@${version}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* npm's `integrity` field (`sha512-<base64>`, sometimes several space-separated) as CycloneDX
|
|
30
|
+
* hashes. CycloneDX carries a hash as lowercase hex, so the base64 is decoded and re-encoded.
|
|
31
|
+
*/
|
|
32
|
+
export function hashesFromIntegrity(integrity) {
|
|
33
|
+
if (typeof integrity !== 'string') return [];
|
|
34
|
+
const hashes = [];
|
|
35
|
+
for (const part of integrity.split(/\s+/)) {
|
|
36
|
+
const dash = part.indexOf('-');
|
|
37
|
+
if (dash < 0) continue;
|
|
38
|
+
const alg = HASH_ALGORITHMS[part.slice(0, dash).toLowerCase()];
|
|
39
|
+
if (alg === undefined) continue;
|
|
40
|
+
hashes.push({ alg, content: Buffer.from(part.slice(dash + 1), 'base64').toString('hex') });
|
|
41
|
+
}
|
|
42
|
+
return hashes;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** an SPDX id stays an `id`; anything with spaces or an expression stays a `name` instead */
|
|
46
|
+
function licenseFor(license) {
|
|
47
|
+
if (typeof license !== 'string' || license.trim() === '') return undefined;
|
|
48
|
+
const value = license.trim();
|
|
49
|
+
return /^[A-Za-z0-9.+-]+$/.test(value) ? { license: { id: value } } : { license: { name: value } };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** the package a lockfile location holds: `node_modules/@scope/name` is `@scope/name` */
|
|
53
|
+
function nameFromLocation(location) {
|
|
54
|
+
const marker = 'node_modules/';
|
|
55
|
+
return location.slice(location.lastIndexOf(marker) + marker.length);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** node resolution over a lockfile: the nearest `node_modules/<name>` at or above `location` */
|
|
59
|
+
function resolveDependency(packages, location, name) {
|
|
60
|
+
let dir = location;
|
|
61
|
+
for (;;) {
|
|
62
|
+
const candidate = dir === '' ? `node_modules/${name}` : `${dir}/node_modules/${name}`;
|
|
63
|
+
if (packages[candidate] !== undefined) return candidate;
|
|
64
|
+
if (dir === '') return undefined;
|
|
65
|
+
const marker = dir.lastIndexOf('/node_modules/');
|
|
66
|
+
dir = marker < 0 ? '' : dir.slice(0, marker);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function scopeOf(entry) {
|
|
71
|
+
if (entry.dev === true) return 'excluded';
|
|
72
|
+
if (entry.optional === true) return 'optional';
|
|
73
|
+
return 'required';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** required beats optional beats excluded when one purl is reachable more than one way */
|
|
77
|
+
const SCOPE_RANK = { required: 0, optional: 1, excluded: 2 };
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A version-5 (RFC 4122 name-based) UUID in the DNS namespace. The same name always hashes to the
|
|
81
|
+
* same UUID, which is what lets `serialNumber` below be derived instead of invented; twelve lines
|
|
82
|
+
* of SHA-1 beats a dependency for the one UUID this tool will ever need.
|
|
83
|
+
*/
|
|
84
|
+
function uuidv5(name) {
|
|
85
|
+
const bytes = createHash('sha1')
|
|
86
|
+
.update(Buffer.from(DNS_NAMESPACE, 'hex'))
|
|
87
|
+
.update(name, 'utf8')
|
|
88
|
+
.digest()
|
|
89
|
+
.subarray(0, 16);
|
|
90
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x50; // version 5
|
|
91
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // RFC 4122 variant
|
|
92
|
+
const hex = bytes.toString('hex');
|
|
93
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Builds a CycloneDX document from `package.json` and `package-lock.json` alone, so the SBOM is
|
|
98
|
+
* reproducible and needs no network access. Every locked package is a component (dev-only ones
|
|
99
|
+
* marked `excluded`, since they are not part of a published install) and the lockfile's own
|
|
100
|
+
* resolution is turned into the dependency graph. Output is deterministic: components and edges
|
|
101
|
+
* are sorted and `serialNumber` is a UUID derived from the document's own content rather than
|
|
102
|
+
* from the clock, so the committed file can be checked for drift the way `API_REPORT.md` and
|
|
103
|
+
* `PROJECT_STATS.json` already are: two BOMs that share a serial are the same BOM, and any change
|
|
104
|
+
* to a component, a hash or an edge produces a new one.
|
|
105
|
+
*
|
|
106
|
+
* `metadata.timestamp` is deliberately absent. A generation time stamped into a committed file is
|
|
107
|
+
* either a drift-check failure on every run or one stale moment every later read is dated to,
|
|
108
|
+
* while `CHANGELOG.md` already dates each version.
|
|
109
|
+
*
|
|
110
|
+
* `type` is the described component's CycloneDX type: `'library'` for this framework,
|
|
111
|
+
* `'application'` for a game (`mwg-sbom` picks it from whether the project is `private`).
|
|
112
|
+
*/
|
|
113
|
+
export function buildSbom(packageJson, lockfile, { type = 'library' } = {}) {
|
|
114
|
+
const packages = lockfile.packages ?? {};
|
|
115
|
+
const rootPurl = purlFor(packageJson.name, packageJson.version);
|
|
116
|
+
|
|
117
|
+
const byPurl = new Map();
|
|
118
|
+
for (const [location, entry] of Object.entries(packages)) {
|
|
119
|
+
if (location === '' || entry.link === true || typeof entry.version !== 'string') continue;
|
|
120
|
+
const name = typeof entry.name === 'string' ? entry.name : nameFromLocation(location);
|
|
121
|
+
const purl = purlFor(name, entry.version);
|
|
122
|
+
const scope = scopeOf(entry);
|
|
123
|
+
const existing = byPurl.get(purl);
|
|
124
|
+
if (existing !== undefined) {
|
|
125
|
+
if (SCOPE_RANK[scope] < SCOPE_RANK[existing.scope]) existing.scope = scope;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
const component = { type: 'library', 'bom-ref': purl, name, version: entry.version, purl, scope };
|
|
129
|
+
const hashes = hashesFromIntegrity(entry.integrity);
|
|
130
|
+
if (hashes.length > 0) component.hashes = hashes;
|
|
131
|
+
const licenses = licenseFor(entry.license);
|
|
132
|
+
if (licenses !== undefined) component.licenses = [licenses];
|
|
133
|
+
byPurl.set(purl, component);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const components = [...byPurl.values()].sort((a, b) => (a.purl < b.purl ? -1 : a.purl > b.purl ? 1 : 0));
|
|
137
|
+
|
|
138
|
+
const edges = new Map();
|
|
139
|
+
const addEdge = (from, to) => {
|
|
140
|
+
if (!edges.has(from)) edges.set(from, new Set());
|
|
141
|
+
edges.get(from).add(to);
|
|
142
|
+
};
|
|
143
|
+
for (const [location, entry] of Object.entries(packages)) {
|
|
144
|
+
if (location !== '' && (entry.link === true || typeof entry.version !== 'string')) continue;
|
|
145
|
+
const from =
|
|
146
|
+
location === ''
|
|
147
|
+
? rootPurl
|
|
148
|
+
: purlFor(typeof entry.name === 'string' ? entry.name : nameFromLocation(location), entry.version);
|
|
149
|
+
const declared = {
|
|
150
|
+
...(entry.dependencies ?? {}),
|
|
151
|
+
...(entry.devDependencies ?? {}),
|
|
152
|
+
...(entry.optionalDependencies ?? {}),
|
|
153
|
+
...(entry.peerDependencies ?? {}),
|
|
154
|
+
};
|
|
155
|
+
for (const name of Object.keys(declared)) {
|
|
156
|
+
const target = resolveDependency(packages, location, name);
|
|
157
|
+
const targetEntry = target === undefined ? undefined : packages[target];
|
|
158
|
+
if (targetEntry === undefined || typeof targetEntry.version !== 'string') continue;
|
|
159
|
+
const targetName = typeof targetEntry.name === 'string' ? targetEntry.name : nameFromLocation(target);
|
|
160
|
+
addEdge(from, purlFor(targetName, targetEntry.version));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const refs = [rootPurl, ...components.map((component) => component['bom-ref'])];
|
|
165
|
+
const dependencies = [...new Set(refs)]
|
|
166
|
+
.sort()
|
|
167
|
+
.map((ref) => ({ ref, dependsOn: [...(edges.get(ref) ?? [])].sort() }));
|
|
168
|
+
|
|
169
|
+
return document(packageJson, type, components, dependencies);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** the envelope both builders share, with the content-derived serial */
|
|
173
|
+
function document(packageJson, type, components, dependencies) {
|
|
174
|
+
const rootPurl = purlFor(packageJson.name, packageJson.version);
|
|
175
|
+
const rootLicenses = licenseFor(packageJson.license);
|
|
176
|
+
const metadata = {
|
|
177
|
+
tools: {
|
|
178
|
+
components: [{ type: 'application', group: 'datamoc', name: 'tools/sbom.mjs', version: TOOL.version }],
|
|
179
|
+
},
|
|
180
|
+
component: {
|
|
181
|
+
type,
|
|
182
|
+
'bom-ref': rootPurl,
|
|
183
|
+
name: packageJson.name,
|
|
184
|
+
version: packageJson.version,
|
|
185
|
+
purl: rootPurl,
|
|
186
|
+
...(typeof packageJson.description === 'string' ? { description: packageJson.description } : {}),
|
|
187
|
+
...(rootLicenses === undefined ? {} : { licenses: [rootLicenses] }),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
bomFormat: 'CycloneDX',
|
|
192
|
+
specVersion: SPEC_VERSION,
|
|
193
|
+
serialNumber: `urn:uuid:${uuidv5(JSON.stringify({ metadata, components, dependencies }))}`,
|
|
194
|
+
version: 1,
|
|
195
|
+
metadata,
|
|
196
|
+
components,
|
|
197
|
+
dependencies,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The package a bundled module came from: the last `node_modules/<name>` (or `<@scope/name>`) in
|
|
203
|
+
* its path, with the directory holding that package's `package.json`. Null for the game's own
|
|
204
|
+
* source and for bundler-virtual modules.
|
|
205
|
+
*/
|
|
206
|
+
export function packageOfModule(id) {
|
|
207
|
+
const path = id.replace(/^\0/, '').split('?')[0].split(sep).join('/');
|
|
208
|
+
const marker = path.lastIndexOf('/node_modules/');
|
|
209
|
+
if (marker < 0) return null;
|
|
210
|
+
const parts = path.slice(marker + '/node_modules/'.length).split('/');
|
|
211
|
+
const name = parts[0].startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
212
|
+
if (!name || name.startsWith('.')) return null;
|
|
213
|
+
return { name, dir: `${path.slice(0, marker)}/node_modules/${name}` };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The SBOM of what a build actually ships, rather than of `node_modules` (item 380): a game is
|
|
218
|
+
* one tree-shaken bundle, so the lockfile lists tooling that never reaches a player and cannot say
|
|
219
|
+
* which packages did. `modules` are the bundle's module ids (vite's module graph, collected by
|
|
220
|
+
* `mwgPage()`), each package they come from becomes a `required` library component with its
|
|
221
|
+
* version and licence read from its own `package.json`, and `files` are the shipped files as
|
|
222
|
+
* `file` components carrying their SHA-256.
|
|
223
|
+
*
|
|
224
|
+
* @param {{ packageJson: object, modules: string[], files: { path: string, sha256: string }[] }} input
|
|
225
|
+
*/
|
|
226
|
+
export function buildArtifactSbom({ packageJson, modules, files }) {
|
|
227
|
+
const packages = new Map();
|
|
228
|
+
for (const id of modules) {
|
|
229
|
+
const found = packageOfModule(id);
|
|
230
|
+
if (!found || packages.has(found.dir)) continue;
|
|
231
|
+
let manifest;
|
|
232
|
+
try {
|
|
233
|
+
manifest = JSON.parse(readFileSync(join(found.dir, 'package.json'), 'utf8'));
|
|
234
|
+
} catch {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
packages.set(found.dir, {
|
|
238
|
+
name: manifest.name ?? found.name,
|
|
239
|
+
version: manifest.version,
|
|
240
|
+
license: manifest.license,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const libraries = new Map();
|
|
244
|
+
for (const { name, version, license } of packages.values()) {
|
|
245
|
+
if (typeof version !== 'string') continue;
|
|
246
|
+
const purl = purlFor(name, version);
|
|
247
|
+
const licenses = licenseFor(license);
|
|
248
|
+
libraries.set(purl, {
|
|
249
|
+
type: 'library',
|
|
250
|
+
'bom-ref': purl,
|
|
251
|
+
name,
|
|
252
|
+
version,
|
|
253
|
+
purl,
|
|
254
|
+
scope: 'required',
|
|
255
|
+
...(licenses === undefined ? {} : { licenses: [licenses] }),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
const fileComponents = [...files]
|
|
259
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
260
|
+
.map(({ path, sha256 }) => ({
|
|
261
|
+
type: 'file',
|
|
262
|
+
'bom-ref': `file:${path}`,
|
|
263
|
+
name: path,
|
|
264
|
+
hashes: [{ alg: 'SHA-256', content: sha256 }],
|
|
265
|
+
}));
|
|
266
|
+
const components = [...[...libraries.values()].sort((a, b) => (a.purl < b.purl ? -1 : 1)), ...fileComponents];
|
|
267
|
+
const rootPurl = purlFor(packageJson.name, packageJson.version);
|
|
268
|
+
const dependencies = [{ ref: rootPurl, dependsOn: [...libraries.keys()].sort() }];
|
|
269
|
+
return document(packageJson, 'application', components, dependencies);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** SHA-256 of every file under `dist` a player receives: precompressed siblings and the SBOM itself left out */
|
|
273
|
+
export async function shippedFiles(dist) {
|
|
274
|
+
const { walk } = await import('./compile-resources.mjs');
|
|
275
|
+
const files = [];
|
|
276
|
+
for await (const file of walk(dist)) {
|
|
277
|
+
const path = relative(dist, file).split(sep).join('/');
|
|
278
|
+
if (/\.(gz|br|xz)$/.test(path) || path.endsWith('.cdx.json')) continue;
|
|
279
|
+
files.push({
|
|
280
|
+
path,
|
|
281
|
+
sha256: createHash('sha256')
|
|
282
|
+
.update(await readFile(file))
|
|
283
|
+
.digest('hex'),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return files;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** the exact committed bytes: two-space JSON and a trailing newline */
|
|
290
|
+
export function serialize(bom) {
|
|
291
|
+
return `${JSON.stringify(bom, null, 2)}\n`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function readJson(path) {
|
|
295
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* `mwg-sbom [project folder] [--out=<file>] [--check]`: the lockfile SBOM of any npm project,
|
|
300
|
+
* this framework included (`npm run sbom` here). A `private` project is described as an
|
|
301
|
+
* `application`, anything else as a `library`. `--check` compares instead of writing, for CI.
|
|
302
|
+
*/
|
|
303
|
+
async function main(argv) {
|
|
304
|
+
const root = resolve(argv.find((arg) => !arg.startsWith('--')) ?? '.');
|
|
305
|
+
const out = resolve(root, argv.find((arg) => arg.startsWith('--out='))?.slice(6) ?? OUTPUT);
|
|
306
|
+
const packageJson = await readJson(join(root, 'package.json'));
|
|
307
|
+
const bom = buildSbom(packageJson, await readJson(join(root, 'package-lock.json')), {
|
|
308
|
+
type: packageJson.private === true ? 'application' : 'library',
|
|
309
|
+
});
|
|
310
|
+
const json = serialize(bom);
|
|
311
|
+
const name = relative(root, out) || out;
|
|
312
|
+
const summary = `CycloneDX ${SPEC_VERSION}, ${bom.components.length} components`;
|
|
313
|
+
|
|
314
|
+
if (argv.includes('--check')) {
|
|
315
|
+
const committed = await readFile(out, 'utf8').catch(() => undefined);
|
|
316
|
+
if (committed !== json) {
|
|
317
|
+
console.error(`${name} is out of date with package-lock.json. Run "npm run sbom" and commit the result.`);
|
|
318
|
+
process.exitCode = 1;
|
|
319
|
+
} else {
|
|
320
|
+
console.log(`${name} matches package-lock.json (${summary}).`);
|
|
321
|
+
}
|
|
322
|
+
} else {
|
|
323
|
+
await writeFile(out, json, 'utf8');
|
|
324
|
+
console.log(`wrote ${name} (${summary})`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
//realpath, since an npm `bin` shim reaches this file through a symlink
|
|
329
|
+
if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url))
|
|
330
|
+
await main(process.argv.slice(2));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Nathaniel Daniel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# rgssad-wasm (vendored)
|
|
2
|
+
|
|
3
|
+
RGSSAD/RGSS2A archive decoder (RPG Maker XP / VX archives), compiled to
|
|
4
|
+
WebAssembly. Used by `tools/extract-rgssad.mjs`.
|
|
5
|
+
|
|
6
|
+
- Source: <https://github.com/nathaniel-daniel/rgssad-rs> (`rgssad-wasm`)
|
|
7
|
+
- Licence: MIT (see `LICENSE-MIT`), © 2022 Nathaniel Daniel.
|
|
8
|
+
Upstream offers the crate under either MIT or Apache-2.0 at your option;
|
|
9
|
+
this project uses it under the MIT option.
|
|
10
|
+
- Files vendored here: `rgssad_wasm.js`, `rgssad_wasm_bg.js`,
|
|
11
|
+
`rgssad_wasm_bg.wasm`, `rgssad_wasm.d.ts`, `rgssad_wasm_bg.wasm.d.ts`,
|
|
12
|
+
`package.json`.
|
|
13
|
+
|
|
14
|
+
Nothing else in this repository is covered by this licence; `mwg` itself
|
|
15
|
+
remains MPL-2.0 (see the root `LICENSE`).
|