@wipcomputer/wip-ldm-os 0.4.85-alpha.6 → 0.4.85-alpha.8
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wipcomputer/wip-ldm-os",
|
|
3
|
-
"version": "0.4.85-alpha.
|
|
3
|
+
"version": "0.4.85-alpha.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
|
|
6
6
|
"engines": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"test:bin-manifest": "node scripts/test-bin-manifest.mjs",
|
|
30
30
|
"test:crc-agentid-tenant-boundary": "node scripts/test-crc-agentid-tenant-boundary.mjs",
|
|
31
31
|
"test:crc-pair-login-flow": "node scripts/test-crc-pair-login-flow.mjs",
|
|
32
|
+
"test:crc-pair-status-poll-token": "node scripts/test-crc-pair-status-poll-token.mjs",
|
|
32
33
|
"test:crc-e2ee-session-route": "node scripts/test-crc-e2ee-session-route.mjs",
|
|
33
34
|
"fmt": "npx prettier --write 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'",
|
|
34
35
|
"fmt:check": "npx prettier --check 'src/**/*.{ts,mjs}' 'lib/**/*.mjs' 'bin/**/*.js'"
|
|
@@ -17,15 +17,15 @@ function assertNotContains(needle, label) {
|
|
|
17
17
|
|
|
18
18
|
assertContains('const ACCOUNT_TENANT_PREFIX = "acct:";', "account tenant prefix");
|
|
19
19
|
assertContains('const LEGACY_API_KEY_TENANT_PREFIX = "key:";', "legacy key tenant prefix");
|
|
20
|
-
assertContains('const RESERVED_AGENT_HANDLES = new Set([', "reserved handle set");
|
|
21
|
-
assertContains('"parker-smoke-test",', "reserved parker smoke handle");
|
|
22
20
|
assertContains('function accountTenantIdForUserId(userId)', "account tenant helper");
|
|
23
21
|
assertContains('function identityForApiKey(key)', "api key identity helper");
|
|
24
22
|
assertContains('return identityForApiKey(key);', "http auth uses identity helper");
|
|
25
23
|
assertContains("const agentId = accountTenantIdForUserId(stored.userId);", "registration uses immutable account tenant");
|
|
24
|
+
assertContains("function sanitizeDisplayLabel(raw)", "display label sanitizer");
|
|
25
|
+
assertContains('replace(/[\\u0000-\\u001f\\u007f]/g, "").replace(/\\s+/g, " ").trim().slice(0, 64)', "display label sanitizer preserves label semantics");
|
|
26
|
+
assertContains("const displayLabel = sanitizeDisplayLabel(body?.displayName || body?.username);", "registration treats entered name as display label");
|
|
27
|
+
assertContains("displayLabel,", "registration challenge stores display label");
|
|
26
28
|
assertContains("await saveApiKey(apiKey, agentId, { handle: credentialLabel });", "registration stores handle separately");
|
|
27
|
-
assertContains('json(res, 409, { error: "reserved_handle"', "reserved handle rejected");
|
|
28
|
-
assertContains('json(res, 409, { error: "handle_taken"', "duplicate handle rejected");
|
|
29
29
|
assertContains("p.handle = identity.handle;", "pair stores display handle separately");
|
|
30
30
|
assertContains("handle: identity.handle,", "relay metadata returns display handle");
|
|
31
31
|
assertContains("codexDaemons.has(identity.agentId)", "daemon presence uses tenant id");
|
|
@@ -37,6 +37,10 @@ assertContains("const key = codexRelayKey(identity.agentId, threadId);", "web ws
|
|
|
37
37
|
assertContains("const daemonWs = codexDaemons.get(identity.agentId);", "web sends to tenant daemon");
|
|
38
38
|
assertNotContains("const agentId = stored.username || (\"passkey-\"", "registration must not use chosen handle as tenant");
|
|
39
39
|
assertNotContains("const existingKey = Object.entries(API_KEYS).find(([k, v]) => v === agentId);", "oauth must not reuse chosen handle as tenant");
|
|
40
|
+
assertNotContains("function isUsernameTaken", "display labels must not be globally unique usernames");
|
|
41
|
+
assertNotContains("function sanitizeUsername", "display labels must not be modeled as usernames");
|
|
42
|
+
assertNotContains('json(res, 409, { error: "reserved_handle"', "display labels must not be blocked as reserved security handles");
|
|
43
|
+
assertNotContains('json(res, 409, { error: "handle_taken"', "duplicate display labels must be allowed");
|
|
40
44
|
|
|
41
45
|
function legacyTenantIdForApiKey(key) {
|
|
42
46
|
return "key:" + createHash("sha256").update(key).digest("base64url").slice(0, 32);
|
|
@@ -47,11 +51,16 @@ function accountTenantIdForUserId(userId) {
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
const chosenHandle = "parker-smoke-test";
|
|
54
|
+
const sharedDisplayLabel = "Parker";
|
|
50
55
|
const accountA = accountTenantIdForUserId("user-a");
|
|
51
56
|
const accountB = accountTenantIdForUserId("user-b");
|
|
57
|
+
const threadId = "thread-019dfa";
|
|
52
58
|
if (accountA === accountB) {
|
|
53
59
|
throw new Error("different user ids collapsed to one account tenant");
|
|
54
60
|
}
|
|
61
|
+
if (`${sharedDisplayLabel}:${threadId}` === `${accountA}:${threadId}` || `${sharedDisplayLabel}:${threadId}` === `${accountB}:${threadId}`) {
|
|
62
|
+
throw new Error("display label was used as a relay route key");
|
|
63
|
+
}
|
|
55
64
|
|
|
56
65
|
const legacyA = legacyTenantIdForApiKey("ck-a");
|
|
57
66
|
const legacyB = legacyTenantIdForApiKey("ck-b");
|
|
@@ -59,7 +68,6 @@ if (legacyA === legacyB) {
|
|
|
59
68
|
throw new Error("legacy API-key tenants collided");
|
|
60
69
|
}
|
|
61
70
|
|
|
62
|
-
const threadId = "thread-019dfa";
|
|
63
71
|
const webKeyA = `${accountA}:${threadId}`;
|
|
64
72
|
const webKeyB = `${accountB}:${threadId}`;
|
|
65
73
|
if (webKeyA === webKeyB) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
const server = readFileSync("src/hosted-mcp/server.mjs", "utf8");
|
|
4
|
+
|
|
5
|
+
function assertContains(needle, label) {
|
|
6
|
+
if (!server.includes(needle)) {
|
|
7
|
+
throw new Error(`${label} missing expected text: ${needle}`);
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assertNotContains(needle, label) {
|
|
12
|
+
if (server.includes(needle)) {
|
|
13
|
+
throw new Error(`${label} still contains forbidden text: ${needle}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
assertContains("function generateCodexPairPollToken()", "pair poll token generator");
|
|
18
|
+
assertContains('return "ppt_" + randomBytes(32).toString("base64url");', "pair poll token entropy");
|
|
19
|
+
assertContains("function getBearerToken(req)", "bearer token helper");
|
|
20
|
+
assertContains("const pollToken = generateCodexPairPollToken();", "pair-init mints poll token");
|
|
21
|
+
assertContains("poll_token: pollToken,", "pair state stores poll token");
|
|
22
|
+
assertContains("poll_token_used: false,", "pair state tracks token consumption");
|
|
23
|
+
assertContains("pair_poll_token: pollToken,", "pair-init returns poll token to daemon");
|
|
24
|
+
assertContains('json(res, 401, { error: "pair_poll_token_expired" });', "expired token rejected");
|
|
25
|
+
assertContains('json(res, 401, { error: "invalid_pair_poll_token" });', "missing or wrong token rejected");
|
|
26
|
+
assertContains("if (!pollToken || pollToken !== p.poll_token || p.poll_token_used)", "pair-status validates token");
|
|
27
|
+
assertContains("p.poll_token_used = true;", "completed credential response consumes token");
|
|
28
|
+
|
|
29
|
+
function pairStatusModel(pair, bearer, now) {
|
|
30
|
+
if (now > pair.expires) return { code: 401, body: { error: "pair_poll_token_expired" } };
|
|
31
|
+
if (!bearer || bearer !== pair.poll_token || pair.poll_token_used) {
|
|
32
|
+
return { code: 401, body: { error: "invalid_pair_poll_token" } };
|
|
33
|
+
}
|
|
34
|
+
if (pair.status === "completed") {
|
|
35
|
+
pair.poll_token_used = true;
|
|
36
|
+
return { code: 200, body: { status: "completed", api_key: pair.apiKey, handle: pair.handle } };
|
|
37
|
+
}
|
|
38
|
+
return { code: 200, body: { status: pair.status } };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const pair = {
|
|
42
|
+
status: "pending",
|
|
43
|
+
expires: 10_000,
|
|
44
|
+
poll_token: "ppt_good",
|
|
45
|
+
poll_token_used: false,
|
|
46
|
+
apiKey: "ck_secret",
|
|
47
|
+
handle: "Parker",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (pairStatusModel({ ...pair }, null, 1).code !== 401) {
|
|
51
|
+
throw new Error("missing poll token should fail");
|
|
52
|
+
}
|
|
53
|
+
if (pairStatusModel({ ...pair }, "ppt_wrong", 1).code !== 401) {
|
|
54
|
+
throw new Error("wrong poll token should fail");
|
|
55
|
+
}
|
|
56
|
+
if (pairStatusModel({ ...pair }, "ppt_good", 20_000).code !== 401) {
|
|
57
|
+
throw new Error("expired poll token should fail");
|
|
58
|
+
}
|
|
59
|
+
if (pairStatusModel({ ...pair }, "ppt_good", 1).body.status !== "pending") {
|
|
60
|
+
throw new Error("correct poll token should return pending before completion");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const completedPair = { ...pair, status: "completed" };
|
|
64
|
+
const completed = pairStatusModel(completedPair, "ppt_good", 1);
|
|
65
|
+
if (completed.code !== 200 || completed.body.api_key !== "ck_secret") {
|
|
66
|
+
throw new Error("correct poll token should return completed credential once");
|
|
67
|
+
}
|
|
68
|
+
const replay = pairStatusModel(completedPair, "ppt_good", 1);
|
|
69
|
+
if (replay.code !== 401) {
|
|
70
|
+
throw new Error("reused completed poll token should fail");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log("crc pair-status poll token checks passed");
|
|
@@ -110,9 +110,6 @@ const API_KEY_HANDLES = {};
|
|
|
110
110
|
const ACCOUNT_TENANT_PREFIX = "acct:";
|
|
111
111
|
const LEGACY_API_KEY_TENANT_PREFIX = "key:";
|
|
112
112
|
const OAUTH_API_KEY_TENANT_PREFIX = "oauth:";
|
|
113
|
-
const RESERVED_AGENT_HANDLES = new Set([
|
|
114
|
-
"parker-smoke-test",
|
|
115
|
-
]);
|
|
116
113
|
|
|
117
114
|
function isInternalTenantId(id) {
|
|
118
115
|
return typeof id === "string"
|
|
@@ -519,21 +516,12 @@ function esc(s) {
|
|
|
519
516
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
520
517
|
}
|
|
521
518
|
|
|
522
|
-
function
|
|
519
|
+
function sanitizeDisplayLabel(raw) {
|
|
523
520
|
if (!raw || typeof raw !== "string") return null;
|
|
524
|
-
const cleaned = raw.
|
|
521
|
+
const cleaned = raw.replace(/[\u0000-\u001f\u007f]/g, "").replace(/\s+/g, " ").trim().slice(0, 64);
|
|
525
522
|
return cleaned.length > 0 ? cleaned : null;
|
|
526
523
|
}
|
|
527
524
|
|
|
528
|
-
async function isUsernameTaken(username) {
|
|
529
|
-
if (!username) return false;
|
|
530
|
-
if (usePrisma) {
|
|
531
|
-
const existing = await prisma.user.findFirst({ where: { name: username } });
|
|
532
|
-
return !!existing;
|
|
533
|
-
}
|
|
534
|
-
return passkeys.some((entry) => entry.handle === username || entry.agentId === username);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
525
|
// ---------- Session cleanup ----------
|
|
538
526
|
|
|
539
527
|
function touchSession(sid) {
|
|
@@ -655,22 +643,17 @@ async function handleRegisterOptions(req, res) {
|
|
|
655
643
|
let body;
|
|
656
644
|
try { body = await readBody(req); } catch { body = {}; }
|
|
657
645
|
|
|
658
|
-
// Accept
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
if (username && await isUsernameTaken(username)) {
|
|
665
|
-
json(res, 409, { error: "handle_taken", error_description: "This handle is already in use." });
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
646
|
+
// Accept the existing `username` field for wire compatibility, but
|
|
647
|
+
// treat it only as a display label for the passkey prompt. It is not
|
|
648
|
+
// a public username, account handle, or relay tenant boundary.
|
|
649
|
+
// Duplicate display labels are allowed.
|
|
650
|
+
const displayLabel = sanitizeDisplayLabel(body?.displayName || body?.username);
|
|
668
651
|
|
|
669
652
|
const userId = randomBytes(16);
|
|
670
653
|
const userIdB64 = userId.toString("base64url");
|
|
671
654
|
|
|
672
|
-
const userName =
|
|
673
|
-
const displayName =
|
|
655
|
+
const userName = displayLabel || ("user-" + userIdB64.slice(0, 8));
|
|
656
|
+
const displayName = displayLabel || "Memory Crystal User";
|
|
674
657
|
|
|
675
658
|
let options;
|
|
676
659
|
try {
|
|
@@ -699,7 +682,7 @@ async function handleRegisterOptions(req, res) {
|
|
|
699
682
|
challenge: options.challenge,
|
|
700
683
|
type: "registration",
|
|
701
684
|
userId: userIdB64,
|
|
702
|
-
|
|
685
|
+
displayLabel,
|
|
703
686
|
expires: Date.now() + 120000,
|
|
704
687
|
};
|
|
705
688
|
|
|
@@ -752,8 +735,8 @@ async function handleRegisterVerify(req, res) {
|
|
|
752
735
|
|
|
753
736
|
const { credential: cred, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
|
|
754
737
|
|
|
755
|
-
// Internal tenancy is the immutable WebAuthn user id. The
|
|
756
|
-
//
|
|
738
|
+
// Internal tenancy is the immutable WebAuthn user id. The user-entered
|
|
739
|
+
// display label is metadata only and never owns a relay namespace.
|
|
757
740
|
const agentId = accountTenantIdForUserId(stored.userId);
|
|
758
741
|
// credentialLabel matches the userName passed to
|
|
759
742
|
// generateRegistrationOptions in handleRegisterOptions, which is what
|
|
@@ -761,7 +744,7 @@ async function handleRegisterVerify(req, res) {
|
|
|
761
744
|
// welcome view should display this, not agentId. Auth semantics are
|
|
762
745
|
// unchanged; only the user-facing label is aligned with the saved
|
|
763
746
|
// credential.
|
|
764
|
-
const credentialLabel = stored.
|
|
747
|
+
const credentialLabel = stored.displayLabel || ("user-" + stored.userId.slice(0, 8));
|
|
765
748
|
const apiKey = generateApiKey();
|
|
766
749
|
|
|
767
750
|
const entry = {
|
|
@@ -2616,7 +2599,7 @@ const httpServer = createServer(async (req, res) => {
|
|
|
2616
2599
|
|
|
2617
2600
|
const CODEX_PAIR_EXPIRY_MS = 5 * 60 * 1000;
|
|
2618
2601
|
const CODEX_PAIR_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
|
2619
|
-
const codexPairings = {}; // pairing_id -> { code, status, expires, daemon_info, apiKey?, agentId?, handle?, daemon_public_key?, crypto_versions? }
|
|
2602
|
+
const codexPairings = {}; // pairing_id -> { code, status, expires, poll_token, poll_token_used?, daemon_info, apiKey?, agentId?, handle?, daemon_public_key?, crypto_versions? }
|
|
2620
2603
|
const codexPairingByCode = {}; // code -> pairing_id (only while pending)
|
|
2621
2604
|
const codexDaemons = new Map(); // agentId -> ws
|
|
2622
2605
|
const codexWebClients = new Map(); // `${agentId}:${threadId}` -> Set<ws>
|
|
@@ -2706,16 +2689,30 @@ function generateCodexPairingCode() {
|
|
|
2706
2689
|
throw new Error("Could not generate unique codex-relay pairing code");
|
|
2707
2690
|
}
|
|
2708
2691
|
|
|
2692
|
+
function generateCodexPairPollToken() {
|
|
2693
|
+
return "ppt_" + randomBytes(32).toString("base64url");
|
|
2694
|
+
}
|
|
2695
|
+
|
|
2696
|
+
function getBearerToken(req) {
|
|
2697
|
+
const auth = req.headers["authorization"];
|
|
2698
|
+
if (typeof auth !== "string" || !auth.startsWith("Bearer ")) return null;
|
|
2699
|
+
const token = auth.slice(7).trim();
|
|
2700
|
+
return token || null;
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2709
2703
|
async function handleCodexPairInit(req, res) {
|
|
2710
2704
|
let body = {};
|
|
2711
2705
|
try { body = (await readBody(req)) || {}; } catch {}
|
|
2712
2706
|
const code = generateCodexPairingCode();
|
|
2713
2707
|
const pairingId = randomUUID();
|
|
2708
|
+
const pollToken = generateCodexPairPollToken();
|
|
2714
2709
|
const expires = Date.now() + CODEX_PAIR_EXPIRY_MS;
|
|
2715
2710
|
codexPairings[pairingId] = {
|
|
2716
2711
|
code,
|
|
2717
2712
|
status: "pending",
|
|
2718
2713
|
expires,
|
|
2714
|
+
poll_token: pollToken,
|
|
2715
|
+
poll_token_used: false,
|
|
2719
2716
|
daemon_info: {
|
|
2720
2717
|
hostname: typeof body.hostname === "string" ? body.hostname.slice(0, 64) : null,
|
|
2721
2718
|
platform: typeof body.platform === "string" ? body.platform.slice(0, 32) : null,
|
|
@@ -2738,6 +2735,7 @@ async function handleCodexPairInit(req, res) {
|
|
|
2738
2735
|
json(res, 200, {
|
|
2739
2736
|
code,
|
|
2740
2737
|
pairing_id: pairingId,
|
|
2738
|
+
pair_poll_token: pollToken,
|
|
2741
2739
|
web_url: ISSUER_URL + "/login?next=" + encodeURIComponent("/pair/" + code),
|
|
2742
2740
|
expires_at: new Date(expires).toISOString(),
|
|
2743
2741
|
});
|
|
@@ -2746,11 +2744,19 @@ async function handleCodexPairInit(req, res) {
|
|
|
2746
2744
|
function handleCodexPairStatus(req, res, pairingId) {
|
|
2747
2745
|
const p = codexPairings[pairingId];
|
|
2748
2746
|
if (!p) { json(res, 404, { error: "pairing not found" }); return; }
|
|
2749
|
-
if (
|
|
2747
|
+
if (Date.now() > p.expires) {
|
|
2750
2748
|
p.status = "expired";
|
|
2751
2749
|
if (codexPairingByCode[p.code] === pairingId) delete codexPairingByCode[p.code];
|
|
2750
|
+
json(res, 401, { error: "pair_poll_token_expired" });
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
const pollToken = getBearerToken(req);
|
|
2754
|
+
if (!pollToken || pollToken !== p.poll_token || p.poll_token_used) {
|
|
2755
|
+
json(res, 401, { error: "invalid_pair_poll_token" });
|
|
2756
|
+
return;
|
|
2752
2757
|
}
|
|
2753
2758
|
if (p.status === "completed") {
|
|
2759
|
+
p.poll_token_used = true;
|
|
2754
2760
|
json(res, 200, { status: "completed", api_key: p.apiKey, handle: p.handle || p.agentId });
|
|
2755
2761
|
} else {
|
|
2756
2762
|
json(res, 200, { status: p.status });
|
|
@@ -2784,8 +2790,8 @@ async function handleCodexPairComplete(req, res) {
|
|
|
2784
2790
|
p.agentId = identity.agentId;
|
|
2785
2791
|
p.handle = identity.handle;
|
|
2786
2792
|
// Phase 2.5: register the daemon's E2EE public key against the
|
|
2787
|
-
// authenticated
|
|
2788
|
-
//
|
|
2793
|
+
// authenticated immutable tenant id. The display handle is returned
|
|
2794
|
+
// as metadata only.
|
|
2789
2795
|
if (p.daemon_public_key) {
|
|
2790
2796
|
codexDaemonPubkeys.set(identity.agentId, {
|
|
2791
2797
|
pubkey: p.daemon_public_key,
|