@ours.network/messenger-server 1.0.31-nightly.14 → 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
|
@@ -413,3 +413,25 @@ Set `OURS_NOTIFICATIONS_ORIGIN` and a Messenger-scoped `OURS_NOTIFICATIONS_PRODU
|
|
|
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
414
|
|
|
415
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
|
}
|
|
@@ -20999,7 +21034,13 @@ var ROUTES = {
|
|
|
20999
21034
|
// operation: roots ride along on the contacts view.
|
|
21000
21035
|
"GET /api/contacts/roots": async ({ client }) => ({ roots: (await client.listContacts()).roots }),
|
|
21001
21036
|
"POST /api/contacts/add": async ({ client, body }) => client.addContact({ invite: str(body, "invite"), name: optStr(body, "name") }),
|
|
21002
|
-
"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
|
+
},
|
|
21003
21044
|
"POST /api/contacts/rename": async ({ client, body }) => client.renameContact({ contact: str(body, "contact"), name: str(body, "name") }),
|
|
21004
21045
|
"GET /api/contacts/local-book": async ({ client }) => client.listLocalContactBook(),
|
|
21005
21046
|
"POST /api/contacts/local-book/policy": async ({ client, body }) => client.setLocalBookPolicy({ expose: optBool(body, "expose"), auto_accept: optBool(body, "auto_accept") }),
|
|
@@ -21038,6 +21079,31 @@ var ROUTES = {
|
|
|
21038
21079
|
return human.rootCid === void 0 ? { cid: identity.cid, preserveProfile: true } : { cid: identity.cid, rootCid: human.rootCid, preserveProfile: true };
|
|
21039
21080
|
},
|
|
21040
21081
|
// First-party setup operation: daemon signs the command using this bound root.
|
|
21082
|
+
"POST /api/workspace/unregister": async ({ client, body }) => {
|
|
21083
|
+
const identity = await client.currentIdentity();
|
|
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 };
|
|
21106
|
+
},
|
|
21041
21107
|
"POST /api/workspace/enroll": async ({ client, deps, body }) => {
|
|
21042
21108
|
const identity = await client.currentIdentity();
|
|
21043
21109
|
if (!identity.isRoot || identity.temporary) throw bad("Workspace proof requires the bound Human root");
|
|
@@ -21055,8 +21121,11 @@ var ROUTES = {
|
|
|
21055
21121
|
const invitation = str(body, "invitation");
|
|
21056
21122
|
if (invitation.length > 8192) throw bad("Workspace invitation too large");
|
|
21057
21123
|
if (humanProfile) writeHumanProfile(deps.config.stateDir, deps.identityCid, humanProfile);
|
|
21058
|
-
const
|
|
21059
|
-
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
|
+
}
|
|
21060
21129
|
const contactDeadline = Date.now() + 1e4;
|
|
21061
21130
|
while (!(await client.listContacts()).contacts.some((contact) => contact.container_id.toUpperCase() === serverCid)) {
|
|
21062
21131
|
if (Date.now() >= contactDeadline) throw bad("Enrollment contact is not ready; obtain a fresh setup payload before retrying");
|
|
@@ -23667,7 +23736,8 @@ async function start(cfg, buildInfo, dependencies = DEFAULT_START_DEPENDENCIES)
|
|
|
23667
23736
|
presenceSockets: presence.socketCount
|
|
23668
23737
|
}),
|
|
23669
23738
|
events,
|
|
23670
|
-
identityCid: bound.cid
|
|
23739
|
+
identityCid: bound.cid,
|
|
23740
|
+
retireContactNotifications: notificationOutbox ? (cid) => notificationOutbox.retireContact(cid) : void 0
|
|
23671
23741
|
};
|
|
23672
23742
|
const moduleDir = dirname5(fileURLToPath(import.meta.url));
|
|
23673
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
|
},
|