@peers-app/peers-sdk 0.23.14 → 0.24.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.
Files changed (80) hide show
  1. package/dist/contracts/index.d.ts +1 -0
  2. package/dist/contracts/index.js +4 -1
  3. package/dist/contracts/system/index.d.ts +2 -0
  4. package/dist/contracts/system/index.js +5 -1
  5. package/dist/contracts/system/invites-contract.d.ts +121 -0
  6. package/dist/contracts/system/invites-contract.js +150 -0
  7. package/dist/data/create-group.d.ts +24 -0
  8. package/dist/data/create-group.js +61 -0
  9. package/dist/data/group-secret.d.ts +10 -0
  10. package/dist/data/group-secret.js +26 -0
  11. package/dist/data/group-share.d.ts +0 -20
  12. package/dist/data/group-share.js +0 -35
  13. package/dist/data/index.d.ts +2 -0
  14. package/dist/data/index.js +2 -0
  15. package/dist/data/invites.d.ts +350 -0
  16. package/dist/data/invites.js +99 -0
  17. package/dist/data/users.js +4 -0
  18. package/dist/device-pairing/constants.d.ts +27 -9
  19. package/dist/device-pairing/constants.js +28 -10
  20. package/dist/device-pairing/cpace.d.ts +46 -0
  21. package/dist/device-pairing/cpace.js +173 -0
  22. package/dist/device-pairing/cpace.test.js +116 -0
  23. package/dist/device-pairing/crypto.d.ts +40 -15
  24. package/dist/device-pairing/crypto.js +74 -33
  25. package/dist/device-pairing/device-pairing.test.js +156 -119
  26. package/dist/device-pairing/index.d.ts +3 -1
  27. package/dist/device-pairing/index.js +3 -1
  28. package/dist/device-pairing/pairing-code.d.ts +27 -0
  29. package/dist/device-pairing/pairing-code.js +84 -0
  30. package/dist/device-pairing/pgp-word-list.d.ts +14 -0
  31. package/dist/device-pairing/pgp-word-list.js +531 -0
  32. package/dist/device-pairing/signaling.d.ts +17 -6
  33. package/dist/device-pairing/signaling.js +31 -8
  34. package/dist/device-pairing/transcript.d.ts +3 -3
  35. package/dist/device-pairing/transcript.js +6 -13
  36. package/dist/device-pairing/types.d.ts +870 -224
  37. package/dist/device-pairing/types.js +186 -108
  38. package/dist/index.d.ts +1 -2
  39. package/dist/index.js +1 -2
  40. package/dist/invites/data-box-schema.d.ts +15 -0
  41. package/dist/invites/data-box-schema.js +15 -0
  42. package/dist/invites/group-approval.d.ts +43 -0
  43. package/dist/invites/group-approval.js +155 -0
  44. package/dist/invites/group-approval.test.js +221 -0
  45. package/dist/invites/index.d.ts +5 -0
  46. package/dist/{group-invite → invites}/index.js +5 -3
  47. package/dist/invites/invite-epoch.d.ts +18 -0
  48. package/dist/invites/invite-epoch.js +35 -0
  49. package/dist/invites/invite-messages.d.ts +1354 -0
  50. package/dist/invites/invite-messages.js +145 -0
  51. package/dist/invites/invite-token.d.ts +334 -0
  52. package/dist/invites/invite-token.js +178 -0
  53. package/dist/invites/invite-token.test.d.ts +1 -0
  54. package/dist/invites/invite-token.test.js +123 -0
  55. package/dist/rpc-types.d.ts +5 -4
  56. package/dist/rpc-types.js +2 -1
  57. package/dist/system-ids.d.ts +22 -0
  58. package/dist/system-ids.js +23 -1
  59. package/package.json +1 -1
  60. package/dist/device-pairing/invitation.d.ts +0 -13
  61. package/dist/device-pairing/invitation.js +0 -85
  62. package/dist/group-invite/group-invite.d.ts +0 -53
  63. package/dist/group-invite/group-invite.js +0 -231
  64. package/dist/group-invite/group-invite.pvars.d.ts +0 -94
  65. package/dist/group-invite/group-invite.pvars.js +0 -139
  66. package/dist/group-invite/group-invite.types.d.ts +0 -124
  67. package/dist/group-invite/group-invite.types.js +0 -8
  68. package/dist/group-invite/index.d.ts +0 -3
  69. package/dist/user-connect/connection-code.d.ts +0 -67
  70. package/dist/user-connect/connection-code.js +0 -176
  71. package/dist/user-connect/connection-code.test.js +0 -213
  72. package/dist/user-connect/index.d.ts +0 -3
  73. package/dist/user-connect/index.js +0 -19
  74. package/dist/user-connect/user-connect.pvars.d.ts +0 -20
  75. package/dist/user-connect/user-connect.pvars.js +0 -63
  76. package/dist/user-connect/user-connect.pvars.test.js +0 -46
  77. package/dist/user-connect/user-connect.types.d.ts +0 -94
  78. package/dist/user-connect/user-connect.types.js +0 -68
  79. /package/dist/{user-connect/connection-code.test.d.ts → device-pairing/cpace.test.d.ts} +0 -0
  80. /package/dist/{user-connect/user-connect.pvars.test.d.ts → invites/group-approval.test.d.ts} +0 -0
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const group_member_roles_1 = require("../data/group-member-roles");
4
+ const keys_1 = require("../keys");
5
+ const utils_1 = require("../utils");
6
+ const invite_messages_1 = require("./invite-messages");
7
+ const invite_token_1 = require("./invite-token");
8
+ function identity(keys = (0, keys_1.newKeys)()) {
9
+ return {
10
+ userId: (0, utils_1.newid)(),
11
+ publicKey: keys.publicKey,
12
+ publicBoxKey: keys.publicBoxKey,
13
+ displayName: "Alice",
14
+ secretKey: keys.secretKey,
15
+ };
16
+ }
17
+ function contactToken(inviter = identity(), expiresAt = new Date(Date.now() + 60_000)) {
18
+ const { secretKey, ...publicIdentity } = inviter;
19
+ return (0, invite_token_1.createInviteToken)({
20
+ type: "contact",
21
+ inviter: publicIdentity,
22
+ expiresAt: expiresAt.toISOString(),
23
+ nonce: (0, invite_token_1.newInviteNonce)(),
24
+ epoch: 0,
25
+ uses: 1,
26
+ }, secretKey);
27
+ }
28
+ describe("PeersInvite tokens", () => {
29
+ it("round-trips through every shareable encoding and verifies the signature", () => {
30
+ const token = contactToken();
31
+ const encoded = (0, invite_token_1.encodeInviteToken)(token);
32
+ expect((0, invite_token_1.parseInviteToken)(encoded)).toEqual(token);
33
+ expect((0, invite_token_1.parseInviteToken)((0, invite_token_1.inviteUrl)(token))).toEqual(token);
34
+ expect((0, invite_token_1.parseInviteToken)((0, invite_token_1.inviteDeepLink)(token))).toEqual(token);
35
+ expect((0, invite_token_1.parseInviteToken)(`https://peers.app/?app#invite=${encoded}`)).toEqual(token);
36
+ expect((0, invite_token_1.parseInviteToken)(` ${encoded}\n`)).toEqual(token);
37
+ expect((0, invite_token_1.extractInviteTokenText)((0, invite_token_1.inviteUrl)(token))).toBe(encoded);
38
+ });
39
+ it("rejects tampered contents without echoing the input", () => {
40
+ const token = contactToken();
41
+ const tampered = (0, invite_token_1.encodeInviteToken)({ ...token, epoch: token.epoch + 1 });
42
+ expect(() => (0, invite_token_1.parseInviteToken)(tampered)).toThrow("Invite signature is not valid");
43
+ expect(() => (0, invite_token_1.parseInviteToken)("not a token")).toThrow("not a valid Peers invite");
44
+ });
45
+ it("refuses to sign with a key that is not the inviter's", () => {
46
+ const inviter = identity();
47
+ const { secretKey: _ignored, ...publicIdentity } = inviter;
48
+ expect(() => (0, invite_token_1.createInviteToken)({
49
+ type: "contact",
50
+ inviter: publicIdentity,
51
+ expiresAt: new Date(Date.now() + 1000).toISOString(),
52
+ nonce: (0, invite_token_1.newInviteNonce)(),
53
+ epoch: 0,
54
+ uses: 1,
55
+ }, (0, keys_1.newKeys)().secretKey)).toThrow("inviter's own key");
56
+ });
57
+ it("enforces expiry and type expectations", () => {
58
+ const expired = contactToken(identity(), new Date(Date.now() - 1000));
59
+ const encoded = (0, invite_token_1.encodeInviteToken)(expired);
60
+ expect(() => (0, invite_token_1.parseInviteToken)(encoded)).toThrow("expired");
61
+ expect((0, invite_token_1.parseInviteToken)(encoded, { allowExpired: true })).toEqual(expired);
62
+ expect(() => (0, invite_token_1.parseInviteToken)(encoded, { allowExpired: true, expectType: "group" })).toThrow("Expected a group invite");
63
+ });
64
+ it("requires group facts exactly when the type is group", () => {
65
+ const inviter = identity();
66
+ const { secretKey, ...publicIdentity } = inviter;
67
+ const base = {
68
+ inviter: publicIdentity,
69
+ expiresAt: new Date(Date.now() + 1000).toISOString(),
70
+ nonce: (0, invite_token_1.newInviteNonce)(),
71
+ epoch: 0,
72
+ uses: "unlimited",
73
+ };
74
+ expect(() => (0, invite_token_1.createInviteToken)({ ...base, type: "group" }, secretKey)).toThrow();
75
+ const groupKeys = (0, keys_1.newKeys)();
76
+ const token = (0, invite_token_1.createInviteToken)({
77
+ ...base,
78
+ type: "group",
79
+ group: {
80
+ groupId: (0, utils_1.newid)(),
81
+ name: "Family",
82
+ publicKey: groupKeys.publicKey,
83
+ publicBoxKey: groupKeys.publicBoxKey,
84
+ role: group_member_roles_1.GroupMemberRole.Reader,
85
+ mode: "open",
86
+ },
87
+ }, secretKey);
88
+ expect((0, invite_token_1.parseInviteToken)((0, invite_token_1.encodeInviteToken)(token), { expectType: "group" }).group?.mode).toBe("open");
89
+ });
90
+ });
91
+ describe("invite envelopes", () => {
92
+ it("seals to the recipient and exposes the verified sender keys", () => {
93
+ const sender = identity();
94
+ const recipient = identity();
95
+ const message = invite_messages_1.contactReplySchema.parse({
96
+ kind: "contact-reply",
97
+ inviteNonce: (0, invite_token_1.newInviteNonce)(),
98
+ inviteEpoch: 0,
99
+ responder: {
100
+ userId: sender.userId,
101
+ publicKey: sender.publicKey,
102
+ publicBoxKey: sender.publicBoxKey,
103
+ },
104
+ responderDeviceId: (0, utils_1.newid)(),
105
+ });
106
+ const envelope = (0, invite_messages_1.sealInviteEnvelope)(message, {
107
+ publicKey: sender.publicKey,
108
+ publicBoxKey: sender.publicBoxKey,
109
+ secretKey: sender.secretKey,
110
+ }, { userId: recipient.userId, publicBoxKey: recipient.publicBoxKey });
111
+ expect(envelope.to).toBe(recipient.userId);
112
+ expect(JSON.stringify(envelope)).not.toContain(sender.userId);
113
+ const opened = (0, invite_messages_1.openInviteEnvelope)(envelope, {
114
+ userId: recipient.userId,
115
+ secretKey: recipient.secretKey,
116
+ });
117
+ expect(opened.message).toEqual(message);
118
+ expect(opened.senderPublicKey).toBe(sender.publicKey);
119
+ expect(opened.senderPublicBoxKey).toBe(sender.publicBoxKey);
120
+ expect(() => (0, invite_messages_1.openInviteEnvelope)(envelope, { userId: (0, utils_1.newid)(), secretKey: recipient.secretKey })).toThrow("different user");
121
+ expect(() => (0, invite_messages_1.openInviteEnvelope)(envelope, { userId: recipient.userId, secretKey: (0, keys_1.newKeys)().secretKey })).toThrow();
122
+ });
123
+ });
@@ -1,6 +1,6 @@
1
1
  import type { IDeviceDatabaseQueryResult, IDeviceOperationStatus, IDeviceToolRunInput } from "./contracts/system/device-operations-contract";
2
2
  import type { ITool } from "./data/tools";
3
- import type { DevicePairingCeremonyAction, DevicePairingStartDestinationInput, DevicePairingStartSourceResult, DevicePairingState } from "./device-pairing";
3
+ import type { DevicePairingCeremonyAction, DevicePairingStartDestinationInput, DevicePairingStartSourceInput, DevicePairingState } from "./device-pairing";
4
4
  import type { IConsoleLog } from "./logging/console-logs.table";
5
5
  import type { IAddOrUpdatePackageResult } from "./package-installer/types";
6
6
  import type { IAppNav } from "./types/app-nav";
@@ -66,13 +66,14 @@ export declare const rpcServerCalls: {
66
66
  seedBundledPeersCore: (dataContextId?: string) => Promise<boolean>;
67
67
  setUserIdAndSecretKey: (userId: string, secretKey: string) => Promise<void>;
68
68
  getUserId: () => Promise<string | undefined>;
69
- devicePairingStartSource: () => Promise<DevicePairingStartSourceResult>;
70
- devicePairingStartDestination: (input: DevicePairingStartDestinationInput) => Promise<DevicePairingState>;
69
+ /** Signed-in side: joins the room named by a typed or scanned pairing code. */
70
+ devicePairingStartSource: (input: DevicePairingStartSourceInput) => Promise<DevicePairingState>;
71
+ /** Signed-out side: creates a room and returns state whose `code` the UI shows as text and QR. */
72
+ devicePairingStartDestination: (input?: DevicePairingStartDestinationInput) => Promise<DevicePairingState>;
71
73
  devicePairingApprove: (input: DevicePairingCeremonyAction) => Promise<DevicePairingState>;
72
74
  devicePairingReject: (input: DevicePairingCeremonyAction) => Promise<DevicePairingState>;
73
75
  devicePairingCancel: (input: DevicePairingCeremonyAction) => Promise<DevicePairingState>;
74
76
  devicePairingGetState: () => Promise<DevicePairingState>;
75
- devicePairingGetInvitationUrl: () => Promise<string | undefined>;
76
77
  encryptData: (value: string, groupId?: string) => Promise<string>;
77
78
  /**
78
79
  * Decrypt (if secret) and copy a persistent variable's value to the system clipboard
package/dist/rpc-types.js CHANGED
@@ -21,13 +21,14 @@ exports.rpcServerCalls = {
21
21
  seedBundledPeersCore: rpcStub("seedBundledPeersCore"),
22
22
  setUserIdAndSecretKey: rpcStub("setUserIdAndSecretKey"),
23
23
  getUserId: rpcStub("getUserId"),
24
+ /** Signed-in side: joins the room named by a typed or scanned pairing code. */
24
25
  devicePairingStartSource: rpcStub("devicePairingStartSource"),
26
+ /** Signed-out side: creates a room and returns state whose `code` the UI shows as text and QR. */
25
27
  devicePairingStartDestination: rpcStub("devicePairingStartDestination"),
26
28
  devicePairingApprove: rpcStub("devicePairingApprove"),
27
29
  devicePairingReject: rpcStub("devicePairingReject"),
28
30
  devicePairingCancel: rpcStub("devicePairingCancel"),
29
31
  devicePairingGetState: rpcStub("devicePairingGetState"),
30
- devicePairingGetInvitationUrl: rpcStub("devicePairingGetInvitationUrl"),
31
32
  encryptData: rpcStub("encryptData"),
32
33
  /**
33
34
  * Decrypt (if secret) and copy a persistent variable's value to the system clipboard
@@ -17,6 +17,8 @@ export declare const runWorkflowToolId = "000peers0tool0runworkflow";
17
17
  * Distinct from Device Operations contract `queryDatabase`.
18
18
  */
19
19
  export declare const queryDatabaseToolId = "000peers0tool00querydb001";
20
+ /** Stable ID of the `create-group` system tool (creates a group founded by the current user). */
21
+ export declare const createGroupToolId = "000peers0tool0creategroup";
20
22
  /** Stable package ID of the bundled peers-core package. */
21
23
  export declare const peersCorePackageId = "00mh0wlipjixk2gqmurbwee0o";
22
24
  /** Canonical source repository for peers-core. */
@@ -54,3 +56,23 @@ export declare const toolAccessControlToolIds: {
54
56
  readonly reject: "000peers0tacreject0000001";
55
57
  readonly listPending: "000peers0taclist000000001";
56
58
  };
59
+ /**
60
+ * Contract id for the local-only contact and group invite surface (System Invites).
61
+ * Not available over device-to-device connections.
62
+ */
63
+ export declare const invitesContractId = "00mu8uuzvo2zux1wfb9f1r48l";
64
+ /** Stable IDs for tools in System Invites v1. */
65
+ export declare const invitesToolIds: {
66
+ readonly createContactInvite: "00mu8uuzvqbn5o23ya563wmp5";
67
+ readonly createGroupInvite: "00mu8uuzvqz17wedq1iqmh9i4";
68
+ readonly inviteContactToGroup: "00mu8uuzvqhbk5avjpew7w9ng";
69
+ readonly previewInvite: "00mu8uuzvqwbo180wgz4ctslm";
70
+ readonly acceptInvite: "00mu8uuzvq8i1boc4ml1ywk30";
71
+ readonly declineInvite: "00mu8uuzvq97veint1977p2ey";
72
+ readonly approveJoinRequest: "00mu8uuzvqisozhlfbe1oeo0e";
73
+ readonly denyJoinRequest: "00mu8uuzvqdars4o466eansi2";
74
+ readonly revokeInvite: "00mu8uuzvqndg6qdh74dob9j1";
75
+ readonly regenerateProfileInvite: "00mu8uuzvqbu5ri9octpk64oo";
76
+ readonly syncMailbox: "00mu8uuzvq4xlaiqrn5notj1j";
77
+ readonly listInvites: "00mu8uuzvqfego6lyjjkgr4s7";
78
+ };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
3
+ exports.invitesToolIds = exports.invitesContractId = exports.toolAccessControlToolIds = exports.toolAccessControlContractId = exports.networkControlContractId = exports.networkDiagnosticsContractId = exports.deviceOperationsContractId = exports.logsContractId = exports.peersSystemPackageId = exports.peersCorePublishPublicKey = exports.peersCorePackageRepoUrl = exports.peersCorePackageId = exports.createGroupToolId = exports.queryDatabaseToolId = exports.runWorkflowToolId = exports.defaultSendMessageToolId = exports.defaultAssistantRunnerToolId = exports.adminAssistantId = exports.openCodeAssistantId = exports.defaultAssistantId = exports.peersRootUserId = void 0;
4
4
  /** Stable user ID that owns built-in Peers system records. */
5
5
  exports.peersRootUserId = "000peers0user000000000001";
6
6
  /** Default assistant used when no user-selected assistant is available. */
@@ -21,6 +21,8 @@ exports.runWorkflowToolId = "000peers0tool0runworkflow";
21
21
  * Distinct from Device Operations contract `queryDatabase`.
22
22
  */
23
23
  exports.queryDatabaseToolId = "000peers0tool00querydb001";
24
+ /** Stable ID of the `create-group` system tool (creates a group founded by the current user). */
25
+ exports.createGroupToolId = "000peers0tool0creategroup";
24
26
  /** Stable package ID of the bundled peers-core package. */
25
27
  exports.peersCorePackageId = "00mh0wlipjixk2gqmurbwee0o";
26
28
  /** Canonical source repository for peers-core. */
@@ -58,3 +60,23 @@ exports.toolAccessControlToolIds = {
58
60
  reject: "000peers0tacreject0000001",
59
61
  listPending: "000peers0taclist000000001",
60
62
  };
63
+ /**
64
+ * Contract id for the local-only contact and group invite surface (System Invites).
65
+ * Not available over device-to-device connections.
66
+ */
67
+ exports.invitesContractId = "00mu8uuzvo2zux1wfb9f1r48l";
68
+ /** Stable IDs for tools in System Invites v1. */
69
+ exports.invitesToolIds = {
70
+ createContactInvite: "00mu8uuzvqbn5o23ya563wmp5",
71
+ createGroupInvite: "00mu8uuzvqz17wedq1iqmh9i4",
72
+ inviteContactToGroup: "00mu8uuzvqhbk5avjpew7w9ng",
73
+ previewInvite: "00mu8uuzvqwbo180wgz4ctslm",
74
+ acceptInvite: "00mu8uuzvq8i1boc4ml1ywk30",
75
+ declineInvite: "00mu8uuzvq97veint1977p2ey",
76
+ approveJoinRequest: "00mu8uuzvqisozhlfbe1oeo0e",
77
+ denyJoinRequest: "00mu8uuzvqdars4o466eansi2",
78
+ revokeInvite: "00mu8uuzvqndg6qdh74dob9j1",
79
+ regenerateProfileInvite: "00mu8uuzvqbu5ri9octpk64oo",
80
+ syncMailbox: "00mu8uuzvq4xlaiqrn5notj1j",
81
+ listInvites: "00mu8uuzvqfego6lyjjkgr4s7",
82
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.23.14",
3
+ "version": "0.24.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"
@@ -1,13 +0,0 @@
1
- import { type DevicePairingInvitation } from "./types";
2
- /** Creates a five-minute, high-entropy invitation for a signaling service origin. */
3
- export declare function createDevicePairingInvitation({ serviceOrigin, now, ttlMs, }: {
4
- serviceOrigin: string;
5
- now?: number;
6
- ttlMs?: number;
7
- }): DevicePairingInvitation;
8
- /** Encodes an invitation as a versioned token suitable for a URL fragment or manual entry. */
9
- export declare function encodeDevicePairingInvitation(invitation: DevicePairingInvitation): string;
10
- /** Parses and validates a versioned invitation token or fragment URL. */
11
- export declare function parseDevicePairingInvitation(input: string, now?: number): DevicePairingInvitation;
12
- /** Places the invitation in a URL fragment so it is excluded from HTTP requests and access logs. */
13
- export declare function createDevicePairingInvitationUrl(invitationBaseUrl: string, invitation: DevicePairingInvitation): string;
@@ -1,85 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createDevicePairingInvitation = createDevicePairingInvitation;
4
- exports.encodeDevicePairingInvitation = encodeDevicePairingInvitation;
5
- exports.parseDevicePairingInvitation = parseDevicePairingInvitation;
6
- exports.createDevicePairingInvitationUrl = createDevicePairingInvitationUrl;
7
- const keys_1 = require("../keys");
8
- const constants_1 = require("./constants");
9
- const types_1 = require("./types");
10
- const INVITATION_TOKEN_PREFIX = "peers-pairing-v1.";
11
- const textDecoder = new TextDecoder();
12
- const textEncoder = new TextEncoder();
13
- function normalizeServiceOrigin(value) {
14
- const url = new URL(value);
15
- if (url.protocol !== "https:" && url.protocol !== "http:") {
16
- throw new Error("Pairing service must use HTTP or HTTPS");
17
- }
18
- return url.origin;
19
- }
20
- function extractInvitationToken(input) {
21
- const trimmed = input.trim();
22
- if (trimmed.startsWith(INVITATION_TOKEN_PREFIX)) {
23
- return trimmed;
24
- }
25
- try {
26
- const url = new URL(trimmed);
27
- const fragment = url.hash.slice(1);
28
- if (fragment.startsWith("pair=")) {
29
- return decodeURIComponent(fragment.slice("pair=".length));
30
- }
31
- if (fragment.startsWith("pair?")) {
32
- const token = new URLSearchParams(fragment.slice("pair?".length)).get("token");
33
- if (token) {
34
- return token;
35
- }
36
- }
37
- }
38
- catch {
39
- // The generic error below deliberately avoids echoing invitation input.
40
- }
41
- throw new Error("Invalid pairing invitation");
42
- }
43
- /** Creates a five-minute, high-entropy invitation for a signaling service origin. */
44
- function createDevicePairingInvitation({ serviceOrigin, now = Date.now(), ttlMs = constants_1.DEVICE_PAIRING_TTL_MS, }) {
45
- if (!Number.isSafeInteger(now) || ttlMs <= 0 || ttlMs > constants_1.DEVICE_PAIRING_TTL_MS) {
46
- throw new Error("Invalid pairing invitation lifetime");
47
- }
48
- return types_1.devicePairingInvitationSchema.parse({
49
- version: constants_1.DEVICE_PAIRING_PROTOCOL_VERSION,
50
- roomId: (0, keys_1.newToken)(16),
51
- roomSecret: (0, keys_1.newToken)(32),
52
- expiresAt: now + ttlMs,
53
- serviceOrigin: normalizeServiceOrigin(serviceOrigin),
54
- });
55
- }
56
- /** Encodes an invitation as a versioned token suitable for a URL fragment or manual entry. */
57
- function encodeDevicePairingInvitation(invitation) {
58
- const parsed = types_1.devicePairingInvitationSchema.parse(invitation);
59
- return `${INVITATION_TOKEN_PREFIX}${(0, keys_1.encodeBase64)(textEncoder.encode(JSON.stringify(parsed)))}`;
60
- }
61
- /** Parses and validates a versioned invitation token or fragment URL. */
62
- function parseDevicePairingInvitation(input, now = Date.now()) {
63
- try {
64
- const token = extractInvitationToken(input);
65
- if (!token.startsWith(INVITATION_TOKEN_PREFIX)) {
66
- throw new Error("unsupported");
67
- }
68
- const encoded = token.slice(INVITATION_TOKEN_PREFIX.length);
69
- const parsed = types_1.devicePairingInvitationSchema.parse(JSON.parse(textDecoder.decode((0, keys_1.decodeBase64)(encoded))));
70
- if (parsed.expiresAt <= now ||
71
- parsed.expiresAt > now + constants_1.DEVICE_PAIRING_TTL_MS + constants_1.DEVICE_PAIRING_CLOCK_SKEW_MS) {
72
- throw new Error("expired");
73
- }
74
- return parsed;
75
- }
76
- catch {
77
- throw new Error("Invalid or expired pairing invitation");
78
- }
79
- }
80
- /** Places the invitation in a URL fragment so it is excluded from HTTP requests and access logs. */
81
- function createDevicePairingInvitationUrl(invitationBaseUrl, invitation) {
82
- const url = new URL(invitationBaseUrl);
83
- url.hash = `pair=${encodeURIComponent(encodeDevicePairingInvitation(invitation))}`;
84
- return url.toString();
85
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * Utilities for group invitation flow.
3
- *
4
- * Uses a simple password that users can edit or customize.
5
- * The password is hashed to create an alias for network discovery.
6
- */
7
- /**
8
- * Generate a random memorable passphrase.
9
- * Format: "word-word-word-word" (4 words separated by dashes)
10
- *
11
- * @param wordCount Number of words to use (default 4)
12
- * @returns A memorable passphrase
13
- */
14
- export declare function generateInvitePassword(wordCount?: number): string;
15
- /**
16
- * Derive a network alias from a password.
17
- * Uses SHA-256 hash, takes first 8 characters as hex.
18
- *
19
- * @param password The password to derive from
20
- * @returns An 8-character hex alias
21
- */
22
- export declare function deriveAliasFromPassword(password: string): string;
23
- /**
24
- * Encrypt data using the invite password.
25
- * Uses NaCl secretbox with a key derived from the password.
26
- *
27
- * @param data Data to encrypt
28
- * @param password The invitation password
29
- * @returns Base64-encoded encrypted data with nonce
30
- */
31
- export declare function encryptWithPassword(data: any, password: string): string;
32
- /**
33
- * Decrypt data using the invite password.
34
- *
35
- * @param encrypted Base64-encoded encrypted data
36
- * @param password The invitation password
37
- * @returns Decrypted data
38
- */
39
- export declare function decryptWithPassword(encrypted: string, password: string): any;
40
- /**
41
- * Validate a password format.
42
- * Passwords should be at least 4 characters.
43
- *
44
- * @param password Password to validate
45
- * @returns true if valid
46
- */
47
- export declare function isValidInvitePassword(password: string): boolean;
48
- /**
49
- * Generate a unique request ID for tracking join requests.
50
- *
51
- * @returns A unique request ID
52
- */
53
- export declare function generateRequestId(): string;
@@ -1,231 +0,0 @@
1
- "use strict";
2
- /**
3
- * Utilities for group invitation flow.
4
- *
5
- * Uses a simple password that users can edit or customize.
6
- * The password is hashed to create an alias for network discovery.
7
- */
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.generateInvitePassword = generateInvitePassword;
10
- exports.deriveAliasFromPassword = deriveAliasFromPassword;
11
- exports.encryptWithPassword = encryptWithPassword;
12
- exports.decryptWithPassword = decryptWithPassword;
13
- exports.isValidInvitePassword = isValidInvitePassword;
14
- exports.generateRequestId = generateRequestId;
15
- const sha2_1 = require("@noble/hashes/sha2");
16
- const nacl = require("tweetnacl");
17
- const tx_encoding_1 = require("../device/tx-encoding");
18
- const keys_1 = require("../keys");
19
- /**
20
- * Word list for generating memorable passphrases.
21
- * Using common, easy-to-spell words.
22
- */
23
- const WORD_LIST = [
24
- "apple",
25
- "banana",
26
- "cherry",
27
- "date",
28
- "elder",
29
- "fig",
30
- "grape",
31
- "honey",
32
- "iris",
33
- "jade",
34
- "kiwi",
35
- "lemon",
36
- "mango",
37
- "nectar",
38
- "olive",
39
- "peach",
40
- "quince",
41
- "rose",
42
- "sugar",
43
- "tulip",
44
- "umber",
45
- "violet",
46
- "willow",
47
- "yarrow",
48
- "zebra",
49
- "amber",
50
- "blaze",
51
- "coral",
52
- "dusk",
53
- "ember",
54
- "frost",
55
- "glow",
56
- "haze",
57
- "ivory",
58
- "jewel",
59
- "karma",
60
- "lunar",
61
- "maple",
62
- "nova",
63
- "opal",
64
- "prism",
65
- "quest",
66
- "river",
67
- "storm",
68
- "terra",
69
- "ultra",
70
- "vivid",
71
- "winter",
72
- "azure",
73
- "brave",
74
- "calm",
75
- "dawn",
76
- "echo",
77
- "fern",
78
- "gentle",
79
- "harbor",
80
- "island",
81
- "jungle",
82
- "kindle",
83
- "light",
84
- "meadow",
85
- "night",
86
- "ocean",
87
- "piano",
88
- "quiet",
89
- "rain",
90
- "silk",
91
- "tide",
92
- "unity",
93
- "velvet",
94
- "wave",
95
- "zenith",
96
- "arch",
97
- "beam",
98
- "cloud",
99
- "drift",
100
- "earth",
101
- "flame",
102
- "green",
103
- "hill",
104
- "ink",
105
- "jazz",
106
- "keen",
107
- "lake",
108
- "moon",
109
- "nest",
110
- "oak",
111
- "peak",
112
- "ruby",
113
- "sand",
114
- "tree",
115
- "vine",
116
- "wind",
117
- "year",
118
- "zone",
119
- "bold",
120
- "crisp",
121
- "deep",
122
- "fair",
123
- "glad",
124
- "warm",
125
- "pure",
126
- "safe",
127
- "true",
128
- ];
129
- /**
130
- * Generate a random memorable passphrase.
131
- * Format: "word-word-word-word" (4 words separated by dashes)
132
- *
133
- * @param wordCount Number of words to use (default 4)
134
- * @returns A memorable passphrase
135
- */
136
- function generateInvitePassword(wordCount = 4) {
137
- const words = [];
138
- const bytes = nacl.randomBytes(wordCount);
139
- for (let i = 0; i < wordCount; i++) {
140
- const index = bytes[i] % WORD_LIST.length;
141
- words.push(WORD_LIST[index]);
142
- }
143
- return words.join("-");
144
- }
145
- /**
146
- * Derive a network alias from a password.
147
- * Uses SHA-256 hash, takes first 8 characters as hex.
148
- *
149
- * @param password The password to derive from
150
- * @returns An 8-character hex alias
151
- */
152
- function deriveAliasFromPassword(password) {
153
- const normalized = password.toLowerCase().trim();
154
- const hash = (0, sha2_1.sha256)(new TextEncoder().encode(normalized));
155
- // Convert first 4 bytes to 8-char hex string
156
- const hexChars = Array.from(hash.slice(0, 4))
157
- .map((b) => b.toString(16).padStart(2, "0"))
158
- .join("")
159
- .toUpperCase();
160
- return hexChars;
161
- }
162
- /**
163
- * Derive a 32-byte encryption key from the password.
164
- * Uses SHA-256 of the password.
165
- *
166
- * @param password The password to derive key from
167
- * @returns A 32-byte encryption key
168
- */
169
- function deriveKeyFromPassword(password) {
170
- const normalized = password.toLowerCase().trim();
171
- return (0, sha2_1.sha256)(new TextEncoder().encode(normalized));
172
- }
173
- /**
174
- * Encrypt data using the invite password.
175
- * Uses NaCl secretbox with a key derived from the password.
176
- *
177
- * @param data Data to encrypt
178
- * @param password The invitation password
179
- * @returns Base64-encoded encrypted data with nonce
180
- */
181
- function encryptWithPassword(data, password) {
182
- const key = deriveKeyFromPassword(password);
183
- const nonce = nacl.randomBytes(24);
184
- const dataBytes = (0, tx_encoding_1.txEncode)(data);
185
- const encrypted = nacl.secretbox(dataBytes, nonce, key);
186
- // Combine nonce + encrypted data
187
- const combined = new Uint8Array(nonce.length + encrypted.length);
188
- combined.set(nonce);
189
- combined.set(encrypted, nonce.length);
190
- return (0, keys_1.encodeBase64)(combined);
191
- }
192
- /**
193
- * Decrypt data using the invite password.
194
- *
195
- * @param encrypted Base64-encoded encrypted data
196
- * @param password The invitation password
197
- * @returns Decrypted data
198
- */
199
- function decryptWithPassword(encrypted, password) {
200
- const key = deriveKeyFromPassword(password);
201
- const combined = (0, keys_1.decodeBase64)(encrypted);
202
- const nonce = combined.slice(0, 24);
203
- const ciphertext = combined.slice(24);
204
- const decrypted = nacl.secretbox.open(ciphertext, nonce, key);
205
- if (!decrypted) {
206
- throw new Error("Decryption failed - invalid password or corrupted data");
207
- }
208
- return (0, tx_encoding_1.txDecode)(decrypted);
209
- }
210
- /**
211
- * Validate a password format.
212
- * Passwords should be at least 4 characters.
213
- *
214
- * @param password Password to validate
215
- * @returns true if valid
216
- */
217
- function isValidInvitePassword(password) {
218
- const trimmed = password.trim();
219
- return trimmed.length >= 4;
220
- }
221
- /**
222
- * Generate a unique request ID for tracking join requests.
223
- *
224
- * @returns A unique request ID
225
- */
226
- function generateRequestId() {
227
- const bytes = nacl.randomBytes(8);
228
- return Array.from(bytes)
229
- .map((b) => b.toString(16).padStart(2, "0"))
230
- .join("");
231
- }