@ours.network/messenger-server 1.0.31-nightly.13 → 1.0.31-nightly.15
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
CHANGED
|
@@ -411,3 +411,27 @@ The older mock walkthrough and its browser tests describe an earlier design stag
|
|
|
411
411
|
Set `OURS_NOTIFICATIONS_ORIGIN` and a Messenger-scoped `OURS_NOTIFICATIONS_PRODUCER_TOKEN` to forward incoming messages/files to [ours-notifications](https://github.com/adapt-toolkit/ours-notifications). Both settings must be provided together. The canonical watcher durably queues notifications even without a local push binding, freezes projected content before its first HTTP attempt, and retries the same payload after outages/restarts. Keep the Messenger state directory across restarts. When configured, new events use the dedicated service; legacy pending push jobs may finish. Existing standalone push endpoints remain for compatibility.
|
|
412
412
|
|
|
413
413
|
Workspace enrollment supports optional `preserveProfile: true` on its protected POST endpoint. This skips human profile creation/replacement (including an absent profile); root checks and the signed binding command remain mandatory. The protected enrollment-identity response advertises `preserveProfile: true` so clients can fail closed against older servers. Omitted/false keeps fresh-host account profile setup.
|
|
414
|
+
|
|
415
|
+
Messenger may run as the Human root or as a permanent identity under it. For the latter, the enrollment-identity response also carries `rootCid`, the root the daemon describes for the bound identity; `POST /api/identity/profile` accepts both. Only a Messenger bound to the root itself signs the workspace binding on `POST /api/workspace/enroll`; for an identity under the root the caller obtains that signature from the root outside Messenger.
|
|
416
|
+
|
|
417
|
+
After successful contact removal, the notification producer durably retires that
|
|
418
|
+
conversation through the notification service's scoped `/api/v1/delete-target`
|
|
419
|
+
route. Pending sends for that contact are removed; cleanup follows any already
|
|
420
|
+
inflight send and retries after service outages or restart. Watch replay confirms
|
|
421
|
+
absence against a complete contacts/pending inventory before dropping an event.
|
|
422
|
+
Inventory errors preserve the queue. The gateway and notification service must
|
|
423
|
+
support this producer lifecycle route before source cleanup can be delivered.
|
|
424
|
+
|
|
425
|
+
Profile-preserving workspace enrollment reuses an already authenticated contact
|
|
426
|
+
with the exact pinned enrollment server CID. This permits a fresh signed account
|
|
427
|
+
challenge after the one-time invitation was used for registration retirement;
|
|
428
|
+
account-side invitation provenance or an exact signed replacement handoff still
|
|
429
|
+
controls proof acceptance.
|
|
430
|
+
|
|
431
|
+
`POST /api/workspace/unregister` carries the first-party retirement command when
|
|
432
|
+
Messenger already holds the Human root lease. It requires local API/CSRF
|
|
433
|
+
authentication, a permanent root matching `rootCid`, the exact ready enrollment
|
|
434
|
+
server contact, and well-formed workspace/host/nonce identifiers. An optional
|
|
435
|
+
three-field replacement challenge is forwarded unchanged. The account service
|
|
436
|
+
owns retirement authorization and cleanup; this route only sends the signed
|
|
437
|
+
command and acknowledges submission.
|
|
@@ -38,7 +38,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
38
38
|
var define_MESSENGER_BUILD_INFO_default;
|
|
39
39
|
var init_define_MESSENGER_BUILD_INFO = __esm({
|
|
40
40
|
"<define:__MESSENGER_BUILD_INFO__>"() {
|
|
41
|
-
define_MESSENGER_BUILD_INFO_default = { name: "@ours.network/messenger-server", version: "1.0.31-nightly.
|
|
41
|
+
define_MESSENGER_BUILD_INFO_default = { name: "@ours.network/messenger-server", version: "1.0.31-nightly.15", sha: "67be7f7cdc2c8fb8d901fb2cb97920da2af048de", dirty: true };
|
|
42
42
|
}
|
|
43
43
|
});
|
|
44
44
|
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
publicEngineError,
|
|
11
11
|
publicInternalError,
|
|
12
12
|
reportFailure
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-YU46BKJ2.js";
|
|
14
14
|
|
|
15
15
|
// node_modules/bn.js/lib/bn.js
|
|
16
16
|
var require_bn = __commonJS({
|
|
@@ -11131,11 +11131,12 @@ function producerConfig(env = process.env) {
|
|
|
11131
11131
|
}
|
|
11132
11132
|
var NotificationOutbox = class {
|
|
11133
11133
|
constructor(file, config, project, warn = () => {
|
|
11134
|
-
}) {
|
|
11134
|
+
}, shouldRetire) {
|
|
11135
11135
|
this.file = file;
|
|
11136
11136
|
this.config = config;
|
|
11137
11137
|
this.project = project;
|
|
11138
11138
|
this.warn = warn;
|
|
11139
|
+
this.shouldRetire = shouldRetire;
|
|
11139
11140
|
mkdirSync(dirname(file), { recursive: true, mode: 448 });
|
|
11140
11141
|
const saved = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : { entries: [] };
|
|
11141
11142
|
this.entries = Array.isArray(saved) ? saved : saved.entries;
|
|
@@ -11186,6 +11187,12 @@ var NotificationOutbox = class {
|
|
|
11186
11187
|
this.persist([...this.entries, { id, value }], cursor);
|
|
11187
11188
|
return true;
|
|
11188
11189
|
}
|
|
11190
|
+
retireTarget(url, matches) {
|
|
11191
|
+
const id = `delete-target:${url}`;
|
|
11192
|
+
const retained = this.entries.filter((entry) => entry.operation === "delete-target" || !matches(entry.value, entry.payload));
|
|
11193
|
+
if (!retained.some((entry) => entry.id === id)) retained.push({ id, value: { url }, operation: "delete-target" });
|
|
11194
|
+
this.persist(retained);
|
|
11195
|
+
}
|
|
11189
11196
|
drain() {
|
|
11190
11197
|
return this.running ??= this.drainOnce().finally(() => {
|
|
11191
11198
|
this.running = void 0;
|
|
@@ -11194,9 +11201,15 @@ var NotificationOutbox = class {
|
|
|
11194
11201
|
async drainOnce() {
|
|
11195
11202
|
for (const entry of [...this.entries].slice(0, 32)) {
|
|
11196
11203
|
try {
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11204
|
+
if (!this.entries.some((current) => current.id === entry.id)) continue;
|
|
11205
|
+
const value = entry.operation === "delete-target" ? this.shouldRetire && !await this.shouldRetire(entry.value.url) ? void 0 : entry.value : entry.payload ?? await this.project(entry.value, entry.id);
|
|
11206
|
+
if (!this.entries.some((current) => current.id === entry.id)) continue;
|
|
11207
|
+
if (value === void 0) {
|
|
11208
|
+
this.persist(this.entries.filter((current) => current.id !== entry.id));
|
|
11209
|
+
continue;
|
|
11210
|
+
}
|
|
11211
|
+
if (!entry.operation && !entry.payload) this.persist(this.entries.map((e2) => e2.id === entry.id ? { ...e2, payload: value } : e2));
|
|
11212
|
+
const response = await fetch(this.config.origin + (entry.operation === "delete-target" ? "/api/v1/delete-target" : "/api/v1/send"), {
|
|
11200
11213
|
method: "POST",
|
|
11201
11214
|
redirect: "error",
|
|
11202
11215
|
signal: AbortSignal.timeout(1e4),
|
|
@@ -11643,8 +11656,15 @@ var PushDeliveryQueue = class {
|
|
|
11643
11656
|
var MessengerNotificationProducer = class {
|
|
11644
11657
|
constructor(stateDir, config, identityCid, client, warn) {
|
|
11645
11658
|
this.identityCid = identityCid;
|
|
11659
|
+
const knownContact = async (cid) => {
|
|
11660
|
+
if (!client.listContacts) return void 0;
|
|
11661
|
+
const view = await client.listContacts();
|
|
11662
|
+
if (!Array.isArray(view.contacts) || !Array.isArray(view.pending) || [...view.contacts, ...view.pending].some((contact) => typeof contact?.container_id !== "string")) throw Error("Contact inventory unavailable");
|
|
11663
|
+
return [...view.contacts, ...view.pending].some((contact) => contact.container_id.toLowerCase() === cid.toLowerCase());
|
|
11664
|
+
};
|
|
11646
11665
|
this.outbox = new NotificationOutbox(join(stateDir, "notification-outbox.json"), config, async (value, eventId) => {
|
|
11647
11666
|
const record = value;
|
|
11667
|
+
if (await knownContact(record.sender_id) === false) return;
|
|
11648
11668
|
const event = await projectPushEvent(client, {
|
|
11649
11669
|
contactId: record.sender_id,
|
|
11650
11670
|
wireId: record.wire_id,
|
|
@@ -11657,7 +11677,7 @@ var MessengerNotificationProducer = class {
|
|
|
11657
11677
|
body: event.body.slice(0, 512) || "New message",
|
|
11658
11678
|
url: `/fleet/chats?source=messenger&chat=${encodeURIComponent(record.sender_id)}&detail=1#chat-message-${encodeURIComponent(record.wire_id)}`
|
|
11659
11679
|
};
|
|
11660
|
-
}, warn);
|
|
11680
|
+
}, warn, async (url) => await knownContact(new URL(url, "https://ours.invalid").searchParams.get("chat")) !== true);
|
|
11661
11681
|
}
|
|
11662
11682
|
outbox;
|
|
11663
11683
|
admit(record) {
|
|
@@ -11670,6 +11690,21 @@ var MessengerNotificationProducer = class {
|
|
|
11670
11690
|
});
|
|
11671
11691
|
return { status: admitted ? "queued" : "saturated" };
|
|
11672
11692
|
}
|
|
11693
|
+
retireContact(cid) {
|
|
11694
|
+
const url = `/fleet/chats?source=messenger&chat=${encodeURIComponent(cid)}`;
|
|
11695
|
+
this.outbox.retireTarget(url, (value, payload) => {
|
|
11696
|
+
const record = value;
|
|
11697
|
+
if (typeof record?.sender_id === "string" && record.sender_id.toLowerCase() === cid.toLowerCase()) return true;
|
|
11698
|
+
if (!payload) return false;
|
|
11699
|
+
try {
|
|
11700
|
+
const target = new URL(payload.url, "https://ours.invalid");
|
|
11701
|
+
return target.pathname === "/fleet/chats" && target.searchParams.get("source") === "messenger" && target.searchParams.get("chat")?.toLowerCase() === cid.toLowerCase();
|
|
11702
|
+
} catch {
|
|
11703
|
+
return false;
|
|
11704
|
+
}
|
|
11705
|
+
});
|
|
11706
|
+
void this.outbox.drain();
|
|
11707
|
+
}
|
|
11673
11708
|
close() {
|
|
11674
11709
|
return this.outbox.close();
|
|
11675
11710
|
}
|
|
@@ -20622,6 +20657,15 @@ function inlineBase64(body) {
|
|
|
20622
20657
|
}
|
|
20623
20658
|
return value;
|
|
20624
20659
|
}
|
|
20660
|
+
function humanIdentity(value) {
|
|
20661
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
20662
|
+
const identity = value;
|
|
20663
|
+
if (identity.temporary !== false || typeof identity.cid !== "string") return void 0;
|
|
20664
|
+
if (identity.isRoot === true) return {};
|
|
20665
|
+
const rootCid = identity.rootCid;
|
|
20666
|
+
if (identity.isRoot !== false || identity.described !== true || typeof identity.roleId !== "string" || !identity.roleId || typeof rootCid !== "string" || !/^[a-f0-9]{64}$/i.test(rootCid) || rootCid.toLowerCase() === identity.cid.toLowerCase()) return void 0;
|
|
20667
|
+
return { rootCid };
|
|
20668
|
+
}
|
|
20625
20669
|
function publicIdentity(value) {
|
|
20626
20670
|
if (value === null || typeof value !== "object") return {};
|
|
20627
20671
|
const identity = value;
|
|
@@ -20990,7 +21034,13 @@ var ROUTES = {
|
|
|
20990
21034
|
// operation: roots ride along on the contacts view.
|
|
20991
21035
|
"GET /api/contacts/roots": async ({ client }) => ({ roots: (await client.listContacts()).roots }),
|
|
20992
21036
|
"POST /api/contacts/add": async ({ client, body }) => client.addContact({ invite: str(body, "invite"), name: optStr(body, "name") }),
|
|
20993
|
-
"POST /api/contacts/remove": async ({ client, body }) =>
|
|
21037
|
+
"POST /api/contacts/remove": async ({ client, body, deps }) => {
|
|
21038
|
+
const contact = str(body, "contact");
|
|
21039
|
+
const canonical3 = deps.retireContactNotifications ? (await resolveContact(client, contact)).cid : contact;
|
|
21040
|
+
const result = await client.removeContact({ contact: canonical3 });
|
|
21041
|
+
deps.retireContactNotifications?.(canonical3);
|
|
21042
|
+
return result;
|
|
21043
|
+
},
|
|
20994
21044
|
"POST /api/contacts/rename": async ({ client, body }) => client.renameContact({ contact: str(body, "contact"), name: str(body, "name") }),
|
|
20995
21045
|
"GET /api/contacts/local-book": async ({ client }) => client.listLocalContactBook(),
|
|
20996
21046
|
"POST /api/contacts/local-book/policy": async ({ client, body }) => client.setLocalBookPolicy({ expose: optBool(body, "expose"), auto_accept: optBool(body, "auto_accept") }),
|
|
@@ -21017,15 +21067,42 @@ var ROUTES = {
|
|
|
21017
21067
|
// field on this result.
|
|
21018
21068
|
"GET /api/identity": async ({ client, deps }) => ({ ...publicIdentity(await client.currentIdentity()), humanProfile: readHumanProfile(deps.config.stateDir, deps.identityCid) }),
|
|
21019
21069
|
"POST /api/identity/profile": async ({ client, deps, body }) => {
|
|
21020
|
-
|
|
21021
|
-
if (!identity.isRoot || identity.temporary) throw bad("Human profile requires the root identity");
|
|
21070
|
+
if (!humanIdentity(await client.currentIdentity())) throw bad("Human profile requires the Human root or a permanent identity under it");
|
|
21022
21071
|
return writeHumanProfile(deps.config.stateDir, deps.identityCid, { name: profileName(body.name), surname: profileName(body.surname) });
|
|
21023
21072
|
},
|
|
21024
|
-
//
|
|
21073
|
+
// Who the person is on this host. A Messenger that runs as the Human root can sign the workspace proof
|
|
21074
|
+
// itself; one that runs as the person's own identity under that root names the root the daemon describes
|
|
21075
|
+
// for it, and the proof is then signed by that root outside Messenger.
|
|
21025
21076
|
"GET /api/workspace/enrollment-identity": async ({ client }) => {
|
|
21077
|
+
const identity = await client.currentIdentity(), human = humanIdentity(identity);
|
|
21078
|
+
if (!human) throw bad("Workspace proof requires the bound Human root or a permanent identity under it");
|
|
21079
|
+
return human.rootCid === void 0 ? { cid: identity.cid, preserveProfile: true } : { cid: identity.cid, rootCid: human.rootCid, preserveProfile: true };
|
|
21080
|
+
},
|
|
21081
|
+
// First-party setup operation: daemon signs the command using this bound root.
|
|
21082
|
+
"POST /api/workspace/unregister": async ({ client, body }) => {
|
|
21026
21083
|
const identity = await client.currentIdentity();
|
|
21027
|
-
if (!identity.isRoot || identity.temporary) throw bad("Workspace
|
|
21028
|
-
|
|
21084
|
+
if (!identity.isRoot || identity.temporary) throw bad("Workspace retirement requires the bound Human root");
|
|
21085
|
+
const rootCid = str(body, "rootCid").toUpperCase(), serverCid = str(body, "serverCid").toUpperCase();
|
|
21086
|
+
if (!/^[A-F0-9]{64}$/.test(rootCid) || !/^[A-F0-9]{64}$/.test(serverCid) || identity.cid.toUpperCase() !== rootCid) throw bad("Workspace retirement identity mismatch");
|
|
21087
|
+
const command = { type: "ours.app.unregister-workspace.v1" };
|
|
21088
|
+
for (const key of ["workspaceId", "hostWorkspaceId", "operationNonce"]) {
|
|
21089
|
+
const value = str(body, key);
|
|
21090
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(value)) throw bad("Invalid workspace retirement");
|
|
21091
|
+
command[key] = value;
|
|
21092
|
+
}
|
|
21093
|
+
if (body.operationExpiresAt !== void 0) {
|
|
21094
|
+
if (!Number.isSafeInteger(body.operationExpiresAt) || Number(body.operationExpiresAt) <= 0 || Number(body.operationExpiresAt) > Date.now() + 864e5) throw bad("Invalid retirement deadline");
|
|
21095
|
+
command.operationExpiresAt = Number(body.operationExpiresAt);
|
|
21096
|
+
}
|
|
21097
|
+
if (body.replacement !== void 0) {
|
|
21098
|
+
const replacement = body.replacement;
|
|
21099
|
+
if (!replacement || typeof replacement !== "object" || Object.keys(replacement).length !== 3 || !["workspaceId", "accountId", "nonce"].every((key) => typeof replacement[key] === "string" && /^[A-Za-z0-9_-]{43}$/.test(replacement[key]))) throw bad("Invalid replacement challenge");
|
|
21100
|
+
command.replacement = Object.fromEntries(["workspaceId", "accountId", "nonce"].map((key) => [key, replacement[key]]));
|
|
21101
|
+
}
|
|
21102
|
+
if (!(await client.listContacts()).contacts.some((contact) => contact.container_id.toUpperCase() === serverCid)) throw bad("Original enrollment contact is not ready");
|
|
21103
|
+
const outcome = await client.sendCommand({ contact: serverCid, command: "unregister-workspace", arguments: command });
|
|
21104
|
+
if (!("sent" in outcome) || !outcome.sent) throw bad("Workspace retirement was not sent");
|
|
21105
|
+
return { submitted: true, rootCid: identity.cid };
|
|
21029
21106
|
},
|
|
21030
21107
|
"POST /api/workspace/enroll": async ({ client, deps, body }) => {
|
|
21031
21108
|
const identity = await client.currentIdentity();
|
|
@@ -21044,8 +21121,11 @@ var ROUTES = {
|
|
|
21044
21121
|
const invitation = str(body, "invitation");
|
|
21045
21122
|
if (invitation.length > 8192) throw bad("Workspace invitation too large");
|
|
21046
21123
|
if (humanProfile) writeHumanProfile(deps.config.stateDir, deps.identityCid, humanProfile);
|
|
21047
|
-
const
|
|
21048
|
-
if (
|
|
21124
|
+
const connected = preserveProfile && (await client.listContacts()).contacts.some((contact) => contact.container_id.toUpperCase() === serverCid);
|
|
21125
|
+
if (!connected) {
|
|
21126
|
+
const peer = await client.addContact({ invite: invitation });
|
|
21127
|
+
if (peer.cid.toUpperCase() !== serverCid) throw bad("Enrollment server identity mismatch");
|
|
21128
|
+
}
|
|
21049
21129
|
const contactDeadline = Date.now() + 1e4;
|
|
21050
21130
|
while (!(await client.listContacts()).contacts.some((contact) => contact.container_id.toUpperCase() === serverCid)) {
|
|
21051
21131
|
if (Date.now() >= contactDeadline) throw bad("Enrollment contact is not ready; obtain a fresh setup payload before retrying");
|
|
@@ -23656,7 +23736,8 @@ async function start(cfg, buildInfo, dependencies = DEFAULT_START_DEPENDENCIES)
|
|
|
23656
23736
|
presenceSockets: presence.socketCount
|
|
23657
23737
|
}),
|
|
23658
23738
|
events,
|
|
23659
|
-
identityCid: bound.cid
|
|
23739
|
+
identityCid: bound.cid,
|
|
23740
|
+
retireContactNotifications: notificationOutbox ? (cid) => notificationOutbox.retireContact(cid) : void 0
|
|
23660
23741
|
};
|
|
23661
23742
|
const moduleDir = dirname5(fileURLToPath(import.meta.url));
|
|
23662
23743
|
const appCandidates = [
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
normalizeBasePath,
|
|
8
8
|
operatorError,
|
|
9
9
|
reportFailure
|
|
10
|
-
} from "./chunks/chunk-
|
|
10
|
+
} from "./chunks/chunk-YU46BKJ2.js";
|
|
11
11
|
|
|
12
12
|
// src/cli.ts
|
|
13
13
|
init_define_MESSENGER_BUILD_INFO();
|
|
@@ -131,7 +131,7 @@ async function main() {
|
|
|
131
131
|
}
|
|
132
132
|
if (process.argv.length !== 3) throw new ConfigurationError("serve does not accept command-line options");
|
|
133
133
|
const cfg = loadConfig();
|
|
134
|
-
const { start } = await import("./chunks/server-
|
|
134
|
+
const { start } = await import("./chunks/server-XMJRG2WC.js");
|
|
135
135
|
const handle = await start(cfg, BUILD_INFO);
|
|
136
136
|
let closing = false;
|
|
137
137
|
const shutdown = (signal) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/messenger-server",
|
|
3
|
-
"version": "1.0.31-nightly.
|
|
3
|
+
"version": "1.0.31-nightly.15",
|
|
4
4
|
"description": "Self-hosted Messenger REST/SSE backend for one identity on a shared Ours daemon.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
34
34
|
"test:startup": "node --import tsx tests/startup-readiness.test.mjs",
|
|
35
35
|
"test:runtime-catalog-candidate": "node --import tsx tests/runtime-command-catalog-reopen-e2e.test.mjs",
|
|
36
|
-
"test:offline": "for f in tests/workspace-profile-preservation.test.mjs tests/notification-producer.test.mjs tests/base-path.test.mjs tests/build-epoch.test.mjs tests/owner-session.test.mjs tests/v1-runtime.test.mjs tests/typed-commands-core.test.mjs tests/shared-daemon-contract.test.mjs tests/room-envelope-matrix.test.mjs tests/bundle-smoke.test.mjs tests/build-metadata.test.mjs tests/production-security.test.mjs tests/conversation-page.test.mjs tests/media-response.test.mjs tests/html-preview-transform.test.mjs tests/html-preview-response.test.mjs tests/push-store.test.mjs tests/push-store-hardening.test.mjs tests/push-delivery.test.mjs tests/push-queue-saturation.test.mjs tests/events.test.mjs tests/watch-cursor.test.mjs tests/watch-reconcile.test.mjs tests/introduction-send-guard.test.mjs tests/sse-e2e.test.mjs tests/presence.test.mjs tests/static-hosting.test.mjs tests/room-preview.test.mjs; do echo \"# $f\"; node --import tsx \"$f\" || exit 1; done",
|
|
36
|
+
"test:offline": "for f in tests/workspace-profile-preservation.test.mjs tests/notification-producer.test.mjs tests/notification-contact-retirement.test.mjs tests/workspace-root-retirement.test.mjs tests/base-path.test.mjs tests/build-epoch.test.mjs tests/owner-session.test.mjs tests/v1-runtime.test.mjs tests/typed-commands-core.test.mjs tests/shared-daemon-contract.test.mjs tests/room-envelope-matrix.test.mjs tests/bundle-smoke.test.mjs tests/build-metadata.test.mjs tests/production-security.test.mjs tests/conversation-page.test.mjs tests/media-response.test.mjs tests/html-preview-transform.test.mjs tests/html-preview-response.test.mjs tests/push-store.test.mjs tests/push-store-hardening.test.mjs tests/push-delivery.test.mjs tests/push-queue-saturation.test.mjs tests/events.test.mjs tests/watch-cursor.test.mjs tests/watch-reconcile.test.mjs tests/introduction-send-guard.test.mjs tests/sse-e2e.test.mjs tests/presence.test.mjs tests/static-hosting.test.mjs tests/room-preview.test.mjs; do echo \"# $f\"; node --import tsx \"$f\" || exit 1; done",
|
|
37
37
|
"test:loopback": "for f in tests/shared-daemon-e2e.test.mjs tests/typed-command-loopback-e2e.test.mjs tests/introduction-send.test.mjs tests/rest-e2e.test.mjs; do echo \"# $f\"; node --import tsx \"$f\" || exit 1; done",
|
|
38
38
|
"test": "npm run build && npm run test:startup && npm run test:offline && npm run test:loopback"
|
|
39
39
|
},
|