@peers-app/peers-sdk 0.23.13 → 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.
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +4 -1
- package/dist/contracts/system/index.d.ts +2 -0
- package/dist/contracts/system/index.js +5 -1
- package/dist/contracts/system/invites-contract.d.ts +121 -0
- package/dist/contracts/system/invites-contract.js +150 -0
- package/dist/data/create-group.d.ts +24 -0
- package/dist/data/create-group.js +61 -0
- package/dist/data/group-secret.d.ts +10 -0
- package/dist/data/group-secret.js +26 -0
- package/dist/data/group-share.d.ts +0 -20
- package/dist/data/group-share.js +0 -35
- package/dist/data/index.d.ts +2 -0
- package/dist/data/index.js +2 -0
- package/dist/data/invites.d.ts +350 -0
- package/dist/data/invites.js +99 -0
- package/dist/data/users.js +4 -0
- package/dist/device-pairing/constants.d.ts +27 -9
- package/dist/device-pairing/constants.js +28 -10
- package/dist/device-pairing/cpace.d.ts +46 -0
- package/dist/device-pairing/cpace.js +173 -0
- package/dist/device-pairing/cpace.test.js +116 -0
- package/dist/device-pairing/crypto.d.ts +40 -15
- package/dist/device-pairing/crypto.js +74 -33
- package/dist/device-pairing/device-pairing.test.js +156 -119
- package/dist/device-pairing/index.d.ts +3 -1
- package/dist/device-pairing/index.js +3 -1
- package/dist/device-pairing/pairing-code.d.ts +27 -0
- package/dist/device-pairing/pairing-code.js +84 -0
- package/dist/device-pairing/pgp-word-list.d.ts +14 -0
- package/dist/device-pairing/pgp-word-list.js +531 -0
- package/dist/device-pairing/signaling.d.ts +17 -6
- package/dist/device-pairing/signaling.js +31 -8
- package/dist/device-pairing/transcript.d.ts +3 -3
- package/dist/device-pairing/transcript.js +6 -13
- package/dist/device-pairing/types.d.ts +870 -224
- package/dist/device-pairing/types.js +186 -108
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/invites/data-box-schema.d.ts +15 -0
- package/dist/invites/data-box-schema.js +15 -0
- package/dist/invites/group-approval.d.ts +43 -0
- package/dist/invites/group-approval.js +155 -0
- package/dist/invites/group-approval.test.js +221 -0
- package/dist/invites/index.d.ts +5 -0
- package/dist/{group-invite → invites}/index.js +5 -3
- package/dist/invites/invite-epoch.d.ts +18 -0
- package/dist/invites/invite-epoch.js +35 -0
- package/dist/invites/invite-messages.d.ts +1354 -0
- package/dist/invites/invite-messages.js +145 -0
- package/dist/invites/invite-token.d.ts +334 -0
- package/dist/invites/invite-token.js +178 -0
- package/dist/invites/invite-token.test.d.ts +1 -0
- package/dist/invites/invite-token.test.js +123 -0
- package/dist/rpc-types.d.ts +5 -4
- package/dist/rpc-types.js +2 -1
- package/dist/system-ids.d.ts +22 -0
- package/dist/system-ids.js +23 -1
- package/dist/types/peer-device.d.ts +15 -1
- package/dist/types/peer-device.js +15 -1
- package/package.json +1 -1
- package/dist/device-pairing/invitation.d.ts +0 -13
- package/dist/device-pairing/invitation.js +0 -85
- package/dist/group-invite/group-invite.d.ts +0 -53
- package/dist/group-invite/group-invite.js +0 -231
- package/dist/group-invite/group-invite.pvars.d.ts +0 -94
- package/dist/group-invite/group-invite.pvars.js +0 -139
- package/dist/group-invite/group-invite.types.d.ts +0 -124
- package/dist/group-invite/group-invite.types.js +0 -8
- package/dist/group-invite/index.d.ts +0 -3
- package/dist/user-connect/connection-code.d.ts +0 -67
- package/dist/user-connect/connection-code.js +0 -176
- package/dist/user-connect/connection-code.test.js +0 -213
- package/dist/user-connect/index.d.ts +0 -3
- package/dist/user-connect/index.js +0 -19
- package/dist/user-connect/user-connect.pvars.d.ts +0 -20
- package/dist/user-connect/user-connect.pvars.js +0 -63
- package/dist/user-connect/user-connect.pvars.test.js +0 -46
- package/dist/user-connect/user-connect.types.d.ts +0 -94
- package/dist/user-connect/user-connect.types.js +0 -68
- /package/dist/{user-connect/connection-code.test.d.ts → device-pairing/cpace.test.d.ts} +0 -0
- /package/dist/{user-connect/user-connect.pvars.test.d.ts → invites/group-approval.test.d.ts} +0 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inviteEnvelopeSchema = exports.inviteMessageSchema = exports.inviteAcceptedSchema = exports.inviteDeclineSchema = exports.groupApprovalSchema = exports.groupJoinRequestSchema = exports.contactReplySchema = void 0;
|
|
4
|
+
exports.sealInviteEnvelope = sealInviteEnvelope;
|
|
5
|
+
exports.openInviteEnvelope = openInviteEnvelope;
|
|
6
|
+
exports.inviteMessageSender = inviteMessageSender;
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const data_1 = require("../data");
|
|
9
|
+
const keys_1 = require("../keys");
|
|
10
|
+
const zod_types_1 = require("../types/zod-types");
|
|
11
|
+
const data_box_schema_1 = require("./data-box-schema");
|
|
12
|
+
const invite_token_1 = require("./invite-token");
|
|
13
|
+
const base64UrlSchema = zod_1.z.string().regex(/^[A-Za-z0-9_-]+$/);
|
|
14
|
+
// Record schemas are resolved lazily so this module does not depend on the order in which
|
|
15
|
+
// the `data` barrel finishes loading (it can be mid-load when this file is first imported).
|
|
16
|
+
const lazyGroupSchema = zod_1.z.lazy(() => data_1.groupSchema);
|
|
17
|
+
const lazyGroupMemberSchema = zod_1.z.lazy(() => data_1.groupMemberSchema);
|
|
18
|
+
const lazyUserSchema = zod_1.z.lazy(() => data_1.userSchema);
|
|
19
|
+
const lazyDeviceInfoSchema = zod_1.z.lazy(() => data_1.deviceInfoSchema);
|
|
20
|
+
/** Recipient accepted a contact token; carries their identity back to the inviter. */
|
|
21
|
+
exports.contactReplySchema = zod_1.z
|
|
22
|
+
.object({
|
|
23
|
+
kind: zod_1.z.literal("contact-reply"),
|
|
24
|
+
/** Nonce and epoch of the token that was accepted. */
|
|
25
|
+
inviteNonce: base64UrlSchema.length(22),
|
|
26
|
+
inviteEpoch: zod_1.z.number().int().min(0),
|
|
27
|
+
responder: invite_token_1.inviteIdentitySchema,
|
|
28
|
+
responderDeviceId: zod_types_1.zodPeerId,
|
|
29
|
+
})
|
|
30
|
+
.strict();
|
|
31
|
+
/** Joiner asks to be admitted under a group token (approve or open mode). */
|
|
32
|
+
exports.groupJoinRequestSchema = zod_1.z
|
|
33
|
+
.object({
|
|
34
|
+
kind: zod_1.z.literal("group-join-request"),
|
|
35
|
+
token: invite_token_1.peersInviteSchema,
|
|
36
|
+
requester: invite_token_1.inviteIdentitySchema,
|
|
37
|
+
requesterDeviceId: zod_types_1.zodPeerId,
|
|
38
|
+
})
|
|
39
|
+
.strict();
|
|
40
|
+
/**
|
|
41
|
+
* Everything a joiner needs to enter a group: the group record, their signed membership,
|
|
42
|
+
* Admin+ seed rows, the approver's identity, and the group secret boxed to the joiner.
|
|
43
|
+
*/
|
|
44
|
+
exports.groupApprovalSchema = zod_1.z
|
|
45
|
+
.object({
|
|
46
|
+
kind: zod_1.z.literal("group-approval"),
|
|
47
|
+
/** Nonce of the token or direct invite this approval answers, when there is one. */
|
|
48
|
+
inviteNonce: base64UrlSchema.length(22).optional(),
|
|
49
|
+
group: lazyGroupSchema,
|
|
50
|
+
membership: lazyGroupMemberSchema,
|
|
51
|
+
seed: zod_1.z
|
|
52
|
+
.object({
|
|
53
|
+
groupMembers: zod_1.z.array(lazyGroupMemberSchema).max(20),
|
|
54
|
+
users: zod_1.z.array(lazyUserSchema).max(20),
|
|
55
|
+
devices: zod_1.z.array(lazyDeviceInfoSchema).max(20),
|
|
56
|
+
})
|
|
57
|
+
.strict(),
|
|
58
|
+
approver: invite_token_1.inviteIdentitySchema,
|
|
59
|
+
approverDeviceId: zod_types_1.zodPeerId,
|
|
60
|
+
/** Group encryption secret boxed to the joiner's `publicBoxKey`. */
|
|
61
|
+
boxedSecret: data_box_schema_1.dataBoxSchema,
|
|
62
|
+
})
|
|
63
|
+
.strict();
|
|
64
|
+
/** A party declined or denied an invite or join request. */
|
|
65
|
+
exports.inviteDeclineSchema = zod_1.z
|
|
66
|
+
.object({
|
|
67
|
+
kind: zod_1.z.literal("invite-decline"),
|
|
68
|
+
inviteNonce: base64UrlSchema.length(22),
|
|
69
|
+
groupId: zod_types_1.zodPeerId.optional(),
|
|
70
|
+
by: invite_token_1.inviteIdentitySchema,
|
|
71
|
+
reason: zod_1.z.string().max(200).optional(),
|
|
72
|
+
})
|
|
73
|
+
.strict();
|
|
74
|
+
/**
|
|
75
|
+
* The recipient of a direct group invite or a profile-QR reply completed their side, so the
|
|
76
|
+
* other party can move its row from pending to accepted.
|
|
77
|
+
*/
|
|
78
|
+
exports.inviteAcceptedSchema = zod_1.z
|
|
79
|
+
.object({
|
|
80
|
+
kind: zod_1.z.literal("invite-accepted"),
|
|
81
|
+
inviteNonce: base64UrlSchema.length(22),
|
|
82
|
+
groupId: zod_types_1.zodPeerId.optional(),
|
|
83
|
+
by: invite_token_1.inviteIdentitySchema,
|
|
84
|
+
byDeviceId: zod_types_1.zodPeerId,
|
|
85
|
+
})
|
|
86
|
+
.strict();
|
|
87
|
+
/** Any message that travels inside an invite envelope. */
|
|
88
|
+
exports.inviteMessageSchema = zod_1.z.discriminatedUnion("kind", [
|
|
89
|
+
exports.contactReplySchema,
|
|
90
|
+
exports.groupJoinRequestSchema,
|
|
91
|
+
exports.groupApprovalSchema,
|
|
92
|
+
exports.inviteDeclineSchema,
|
|
93
|
+
exports.inviteAcceptedSchema,
|
|
94
|
+
]);
|
|
95
|
+
/**
|
|
96
|
+
* Wire format for invite traffic over the mesh (`dataContextId: "invite"`) and the mailbox.
|
|
97
|
+
* The message is signed by the sender's user key, then boxed to the recipient's user box key,
|
|
98
|
+
* so any of the recipient's devices can open it and the relay learns only the recipient id.
|
|
99
|
+
*/
|
|
100
|
+
exports.inviteEnvelopeSchema = zod_1.z
|
|
101
|
+
.object({
|
|
102
|
+
v: zod_1.z.literal(1),
|
|
103
|
+
to: zod_types_1.zodPeerId,
|
|
104
|
+
box: data_box_schema_1.dataBoxSchema,
|
|
105
|
+
})
|
|
106
|
+
.strict();
|
|
107
|
+
/** Signs a message with the sender's user key and boxes it to the recipient's user box key. */
|
|
108
|
+
function sealInviteEnvelope(message, senderKeys, recipient) {
|
|
109
|
+
const parsed = exports.inviteMessageSchema.parse(message);
|
|
110
|
+
const signed = (0, keys_1.signObjectWithSecretKey)(parsed, senderKeys.secretKey);
|
|
111
|
+
const box = (0, keys_1.boxDataWithKeys)(signed, recipient.publicBoxKey, senderKeys.secretKey);
|
|
112
|
+
return exports.inviteEnvelopeSchema.parse({ v: 1, to: recipient.userId, box });
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Opens an envelope addressed to this user and verifies the inner signature.
|
|
116
|
+
*
|
|
117
|
+
* @throws when the box cannot be opened, the signature is invalid, or the message is malformed
|
|
118
|
+
*/
|
|
119
|
+
function openInviteEnvelope(envelope, recipient) {
|
|
120
|
+
const parsed = exports.inviteEnvelopeSchema.parse(envelope);
|
|
121
|
+
if (parsed.to !== recipient.userId) {
|
|
122
|
+
throw new Error("Invite envelope is addressed to a different user");
|
|
123
|
+
}
|
|
124
|
+
const signed = (0, keys_1.openBoxWithSecretKey)(parsed.box, recipient.secretKey);
|
|
125
|
+
const message = exports.inviteMessageSchema.parse((0, keys_1.openSignedObject)(signed));
|
|
126
|
+
return {
|
|
127
|
+
message,
|
|
128
|
+
senderPublicKey: signed.publicKey,
|
|
129
|
+
senderPublicBoxKey: parsed.box.fromPublicKey,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** Returns the identity a message claims for its sender, for cross-checking against the envelope. */
|
|
133
|
+
function inviteMessageSender(message) {
|
|
134
|
+
switch (message.kind) {
|
|
135
|
+
case "contact-reply":
|
|
136
|
+
return message.responder;
|
|
137
|
+
case "group-join-request":
|
|
138
|
+
return message.requester;
|
|
139
|
+
case "group-approval":
|
|
140
|
+
return message.approver;
|
|
141
|
+
case "invite-decline":
|
|
142
|
+
case "invite-accepted":
|
|
143
|
+
return message.by;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { GroupMemberRole } from "../data/group-member-roles";
|
|
3
|
+
/** Token format version this module produces and accepts. */
|
|
4
|
+
export declare const PEERS_INVITE_VERSION = 1;
|
|
5
|
+
/** Canonical landing page that carries a token in its URL fragment. */
|
|
6
|
+
export declare const PEERS_INVITE_URL_BASE = "https://peers.app/i";
|
|
7
|
+
/** URL fragment prefix used when a token is handed to the web app directly. */
|
|
8
|
+
export declare const PEERS_INVITE_APP_FRAGMENT_PREFIX = "#invite=";
|
|
9
|
+
/** Default and maximum lifetimes for each invite shape. */
|
|
10
|
+
export declare const INVITE_EXPIRY: {
|
|
11
|
+
readonly contactDefaultMs: number;
|
|
12
|
+
readonly profileDefaultMs: number;
|
|
13
|
+
readonly groupApproveDefaultMs: number;
|
|
14
|
+
readonly groupOpenDefaultMs: number;
|
|
15
|
+
readonly groupMaxMs: number;
|
|
16
|
+
readonly contactMaxMs: number;
|
|
17
|
+
};
|
|
18
|
+
/** Public identity of a party to an invite. */
|
|
19
|
+
export declare const inviteIdentitySchema: z.ZodObject<{
|
|
20
|
+
userId: z.ZodEffects<z.ZodString, string, string>;
|
|
21
|
+
publicKey: z.ZodString;
|
|
22
|
+
publicBoxKey: z.ZodString;
|
|
23
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
24
|
+
}, "strict", z.ZodTypeAny, {
|
|
25
|
+
publicKey: string;
|
|
26
|
+
publicBoxKey: string;
|
|
27
|
+
userId: string;
|
|
28
|
+
displayName?: string | undefined;
|
|
29
|
+
}, {
|
|
30
|
+
publicKey: string;
|
|
31
|
+
publicBoxKey: string;
|
|
32
|
+
userId: string;
|
|
33
|
+
displayName?: string | undefined;
|
|
34
|
+
}>;
|
|
35
|
+
/** Public identity of a party to an invite. */
|
|
36
|
+
export type InviteIdentity = z.infer<typeof inviteIdentitySchema>;
|
|
37
|
+
/** How joiners are admitted by a group token. */
|
|
38
|
+
export declare const groupInviteModeSchema: z.ZodEnum<["approve", "open"]>;
|
|
39
|
+
/** How joiners are admitted by a group token. */
|
|
40
|
+
export type GroupInviteMode = z.infer<typeof groupInviteModeSchema>;
|
|
41
|
+
/** Group facts embedded in a group token so the joiner can preview it offline. */
|
|
42
|
+
export declare const inviteGroupSchema: z.ZodObject<{
|
|
43
|
+
groupId: z.ZodEffects<z.ZodString, string, string>;
|
|
44
|
+
name: z.ZodString;
|
|
45
|
+
publicKey: z.ZodString;
|
|
46
|
+
publicBoxKey: z.ZodString;
|
|
47
|
+
role: z.ZodNativeEnum<typeof GroupMemberRole>;
|
|
48
|
+
mode: z.ZodEnum<["approve", "open"]>;
|
|
49
|
+
}, "strict", z.ZodTypeAny, {
|
|
50
|
+
name: string;
|
|
51
|
+
role: GroupMemberRole;
|
|
52
|
+
publicKey: string;
|
|
53
|
+
groupId: string;
|
|
54
|
+
publicBoxKey: string;
|
|
55
|
+
mode: "open" | "approve";
|
|
56
|
+
}, {
|
|
57
|
+
name: string;
|
|
58
|
+
role: GroupMemberRole;
|
|
59
|
+
publicKey: string;
|
|
60
|
+
groupId: string;
|
|
61
|
+
publicBoxKey: string;
|
|
62
|
+
mode: "open" | "approve";
|
|
63
|
+
}>;
|
|
64
|
+
/** Group facts embedded in a group token. */
|
|
65
|
+
export type InviteGroup = z.infer<typeof inviteGroupSchema>;
|
|
66
|
+
/** Number of accepts a token allows; profile QRs and reusable group links are unlimited. */
|
|
67
|
+
export declare const inviteUsesSchema: z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<"unlimited">]>;
|
|
68
|
+
/** Unsigned contents of a PeersInvite v1 token. */
|
|
69
|
+
export declare const peersInviteContentsSchema: z.ZodEffects<z.ZodObject<{
|
|
70
|
+
v: z.ZodLiteral<1>;
|
|
71
|
+
type: z.ZodEnum<["contact", "group"]>;
|
|
72
|
+
inviter: z.ZodObject<{
|
|
73
|
+
userId: z.ZodEffects<z.ZodString, string, string>;
|
|
74
|
+
publicKey: z.ZodString;
|
|
75
|
+
publicBoxKey: z.ZodString;
|
|
76
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
77
|
+
}, "strict", z.ZodTypeAny, {
|
|
78
|
+
publicKey: string;
|
|
79
|
+
publicBoxKey: string;
|
|
80
|
+
userId: string;
|
|
81
|
+
displayName?: string | undefined;
|
|
82
|
+
}, {
|
|
83
|
+
publicKey: string;
|
|
84
|
+
publicBoxKey: string;
|
|
85
|
+
userId: string;
|
|
86
|
+
displayName?: string | undefined;
|
|
87
|
+
}>;
|
|
88
|
+
/** ISO 8601 instant after which the token is invalid. */
|
|
89
|
+
expiresAt: z.ZodString;
|
|
90
|
+
/** 128-bit random value; identifies the token and links replies to it. */
|
|
91
|
+
nonce: z.ZodString;
|
|
92
|
+
/** Inviter's invite epoch at issue time; a bumped epoch revokes the token. */
|
|
93
|
+
epoch: z.ZodNumber;
|
|
94
|
+
uses: z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<"unlimited">]>;
|
|
95
|
+
group: z.ZodOptional<z.ZodObject<{
|
|
96
|
+
groupId: z.ZodEffects<z.ZodString, string, string>;
|
|
97
|
+
name: z.ZodString;
|
|
98
|
+
publicKey: z.ZodString;
|
|
99
|
+
publicBoxKey: z.ZodString;
|
|
100
|
+
role: z.ZodNativeEnum<typeof GroupMemberRole>;
|
|
101
|
+
mode: z.ZodEnum<["approve", "open"]>;
|
|
102
|
+
}, "strict", z.ZodTypeAny, {
|
|
103
|
+
name: string;
|
|
104
|
+
role: GroupMemberRole;
|
|
105
|
+
publicKey: string;
|
|
106
|
+
groupId: string;
|
|
107
|
+
publicBoxKey: string;
|
|
108
|
+
mode: "open" | "approve";
|
|
109
|
+
}, {
|
|
110
|
+
name: string;
|
|
111
|
+
role: GroupMemberRole;
|
|
112
|
+
publicKey: string;
|
|
113
|
+
groupId: string;
|
|
114
|
+
publicBoxKey: string;
|
|
115
|
+
mode: "open" | "approve";
|
|
116
|
+
}>>;
|
|
117
|
+
}, "strict", z.ZodTypeAny, {
|
|
118
|
+
type: "group" | "contact";
|
|
119
|
+
nonce: string;
|
|
120
|
+
expiresAt: string;
|
|
121
|
+
v: 1;
|
|
122
|
+
inviter: {
|
|
123
|
+
publicKey: string;
|
|
124
|
+
publicBoxKey: string;
|
|
125
|
+
userId: string;
|
|
126
|
+
displayName?: string | undefined;
|
|
127
|
+
};
|
|
128
|
+
epoch: number;
|
|
129
|
+
uses: 1 | "unlimited";
|
|
130
|
+
group?: {
|
|
131
|
+
name: string;
|
|
132
|
+
role: GroupMemberRole;
|
|
133
|
+
publicKey: string;
|
|
134
|
+
groupId: string;
|
|
135
|
+
publicBoxKey: string;
|
|
136
|
+
mode: "open" | "approve";
|
|
137
|
+
} | undefined;
|
|
138
|
+
}, {
|
|
139
|
+
type: "group" | "contact";
|
|
140
|
+
nonce: string;
|
|
141
|
+
expiresAt: string;
|
|
142
|
+
v: 1;
|
|
143
|
+
inviter: {
|
|
144
|
+
publicKey: string;
|
|
145
|
+
publicBoxKey: string;
|
|
146
|
+
userId: string;
|
|
147
|
+
displayName?: string | undefined;
|
|
148
|
+
};
|
|
149
|
+
epoch: number;
|
|
150
|
+
uses: 1 | "unlimited";
|
|
151
|
+
group?: {
|
|
152
|
+
name: string;
|
|
153
|
+
role: GroupMemberRole;
|
|
154
|
+
publicKey: string;
|
|
155
|
+
groupId: string;
|
|
156
|
+
publicBoxKey: string;
|
|
157
|
+
mode: "open" | "approve";
|
|
158
|
+
} | undefined;
|
|
159
|
+
}>, {
|
|
160
|
+
type: "group" | "contact";
|
|
161
|
+
nonce: string;
|
|
162
|
+
expiresAt: string;
|
|
163
|
+
v: 1;
|
|
164
|
+
inviter: {
|
|
165
|
+
publicKey: string;
|
|
166
|
+
publicBoxKey: string;
|
|
167
|
+
userId: string;
|
|
168
|
+
displayName?: string | undefined;
|
|
169
|
+
};
|
|
170
|
+
epoch: number;
|
|
171
|
+
uses: 1 | "unlimited";
|
|
172
|
+
group?: {
|
|
173
|
+
name: string;
|
|
174
|
+
role: GroupMemberRole;
|
|
175
|
+
publicKey: string;
|
|
176
|
+
groupId: string;
|
|
177
|
+
publicBoxKey: string;
|
|
178
|
+
mode: "open" | "approve";
|
|
179
|
+
} | undefined;
|
|
180
|
+
}, {
|
|
181
|
+
type: "group" | "contact";
|
|
182
|
+
nonce: string;
|
|
183
|
+
expiresAt: string;
|
|
184
|
+
v: 1;
|
|
185
|
+
inviter: {
|
|
186
|
+
publicKey: string;
|
|
187
|
+
publicBoxKey: string;
|
|
188
|
+
userId: string;
|
|
189
|
+
displayName?: string | undefined;
|
|
190
|
+
};
|
|
191
|
+
epoch: number;
|
|
192
|
+
uses: 1 | "unlimited";
|
|
193
|
+
group?: {
|
|
194
|
+
name: string;
|
|
195
|
+
role: GroupMemberRole;
|
|
196
|
+
publicKey: string;
|
|
197
|
+
groupId: string;
|
|
198
|
+
publicBoxKey: string;
|
|
199
|
+
mode: "open" | "approve";
|
|
200
|
+
} | undefined;
|
|
201
|
+
}>;
|
|
202
|
+
/** Unsigned contents of a PeersInvite v1 token. */
|
|
203
|
+
export type PeersInviteContents = z.infer<typeof peersInviteContentsSchema>;
|
|
204
|
+
/** Signed PeersInvite v1 token: contents plus the inviter's detached Ed25519 signature. */
|
|
205
|
+
export declare const peersInviteSchema: z.ZodObject<{
|
|
206
|
+
v: z.ZodLiteral<1>;
|
|
207
|
+
type: z.ZodEnum<["contact", "group"]>;
|
|
208
|
+
inviter: z.ZodObject<{
|
|
209
|
+
userId: z.ZodEffects<z.ZodString, string, string>;
|
|
210
|
+
publicKey: z.ZodString;
|
|
211
|
+
publicBoxKey: z.ZodString;
|
|
212
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
213
|
+
}, "strict", z.ZodTypeAny, {
|
|
214
|
+
publicKey: string;
|
|
215
|
+
publicBoxKey: string;
|
|
216
|
+
userId: string;
|
|
217
|
+
displayName?: string | undefined;
|
|
218
|
+
}, {
|
|
219
|
+
publicKey: string;
|
|
220
|
+
publicBoxKey: string;
|
|
221
|
+
userId: string;
|
|
222
|
+
displayName?: string | undefined;
|
|
223
|
+
}>;
|
|
224
|
+
expiresAt: z.ZodString;
|
|
225
|
+
nonce: z.ZodString;
|
|
226
|
+
epoch: z.ZodNumber;
|
|
227
|
+
uses: z.ZodUnion<[z.ZodLiteral<1>, z.ZodLiteral<"unlimited">]>;
|
|
228
|
+
group: z.ZodOptional<z.ZodObject<{
|
|
229
|
+
groupId: z.ZodEffects<z.ZodString, string, string>;
|
|
230
|
+
name: z.ZodString;
|
|
231
|
+
publicKey: z.ZodString;
|
|
232
|
+
publicBoxKey: z.ZodString;
|
|
233
|
+
role: z.ZodNativeEnum<typeof GroupMemberRole>;
|
|
234
|
+
mode: z.ZodEnum<["approve", "open"]>;
|
|
235
|
+
}, "strict", z.ZodTypeAny, {
|
|
236
|
+
name: string;
|
|
237
|
+
role: GroupMemberRole;
|
|
238
|
+
publicKey: string;
|
|
239
|
+
groupId: string;
|
|
240
|
+
publicBoxKey: string;
|
|
241
|
+
mode: "open" | "approve";
|
|
242
|
+
}, {
|
|
243
|
+
name: string;
|
|
244
|
+
role: GroupMemberRole;
|
|
245
|
+
publicKey: string;
|
|
246
|
+
groupId: string;
|
|
247
|
+
publicBoxKey: string;
|
|
248
|
+
mode: "open" | "approve";
|
|
249
|
+
}>>;
|
|
250
|
+
signature: z.ZodString;
|
|
251
|
+
}, "strict", z.ZodTypeAny, {
|
|
252
|
+
type: "group" | "contact";
|
|
253
|
+
nonce: string;
|
|
254
|
+
signature: string;
|
|
255
|
+
expiresAt: string;
|
|
256
|
+
v: 1;
|
|
257
|
+
inviter: {
|
|
258
|
+
publicKey: string;
|
|
259
|
+
publicBoxKey: string;
|
|
260
|
+
userId: string;
|
|
261
|
+
displayName?: string | undefined;
|
|
262
|
+
};
|
|
263
|
+
epoch: number;
|
|
264
|
+
uses: 1 | "unlimited";
|
|
265
|
+
group?: {
|
|
266
|
+
name: string;
|
|
267
|
+
role: GroupMemberRole;
|
|
268
|
+
publicKey: string;
|
|
269
|
+
groupId: string;
|
|
270
|
+
publicBoxKey: string;
|
|
271
|
+
mode: "open" | "approve";
|
|
272
|
+
} | undefined;
|
|
273
|
+
}, {
|
|
274
|
+
type: "group" | "contact";
|
|
275
|
+
nonce: string;
|
|
276
|
+
signature: string;
|
|
277
|
+
expiresAt: string;
|
|
278
|
+
v: 1;
|
|
279
|
+
inviter: {
|
|
280
|
+
publicKey: string;
|
|
281
|
+
publicBoxKey: string;
|
|
282
|
+
userId: string;
|
|
283
|
+
displayName?: string | undefined;
|
|
284
|
+
};
|
|
285
|
+
epoch: number;
|
|
286
|
+
uses: 1 | "unlimited";
|
|
287
|
+
group?: {
|
|
288
|
+
name: string;
|
|
289
|
+
role: GroupMemberRole;
|
|
290
|
+
publicKey: string;
|
|
291
|
+
groupId: string;
|
|
292
|
+
publicBoxKey: string;
|
|
293
|
+
mode: "open" | "approve";
|
|
294
|
+
} | undefined;
|
|
295
|
+
}>;
|
|
296
|
+
/** Signed PeersInvite v1 token. */
|
|
297
|
+
export type PeersInvite = z.infer<typeof peersInviteSchema>;
|
|
298
|
+
/** Generates a fresh 128-bit token nonce. */
|
|
299
|
+
export declare function newInviteNonce(): string;
|
|
300
|
+
/** Signs token contents with the inviter's secret key. */
|
|
301
|
+
export declare function createInviteToken(contents: Omit<PeersInviteContents, "v">, inviterSecretKey: string): PeersInvite;
|
|
302
|
+
/** Encodes a token as unpadded base64url for URLs, QR codes, and copy/paste. */
|
|
303
|
+
export declare function encodeInviteToken(token: PeersInvite): string;
|
|
304
|
+
/** Options that tighten {@link parseInviteToken}. */
|
|
305
|
+
export interface ParseInviteTokenOptions {
|
|
306
|
+
/** Reject tokens of any other type. */
|
|
307
|
+
expectType?: PeersInvite["type"];
|
|
308
|
+
/** Clock used for expiry; defaults to `Date.now()`. */
|
|
309
|
+
now?: number;
|
|
310
|
+
/** Skip the expiry check (e.g. to show why a token is stale). */
|
|
311
|
+
allowExpired?: boolean;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Parses a pasted token, invite URL, or `peers://` link and verifies its signature and expiry.
|
|
315
|
+
*
|
|
316
|
+
* @throws with a user-safe message that never echoes the input
|
|
317
|
+
*/
|
|
318
|
+
export declare function parseInviteToken(input: string, options?: ParseInviteTokenOptions): PeersInvite;
|
|
319
|
+
/** Whether a token's expiry has passed. */
|
|
320
|
+
export declare function isInviteExpired(token: Pick<PeersInvite, "expiresAt">, now?: number): boolean;
|
|
321
|
+
/**
|
|
322
|
+
* Pulls the base64url token out of any of the shapes users paste: the bare token,
|
|
323
|
+
* `https://peers.app/i#<token>`, `/?app#invite=<token>`, or `peers://i#<token>`.
|
|
324
|
+
*/
|
|
325
|
+
export declare function extractInviteTokenText(input: string): string;
|
|
326
|
+
/** Builds the shareable landing-page URL; the token travels only in the fragment. */
|
|
327
|
+
export declare function inviteUrl(token: PeersInvite, base?: string): string;
|
|
328
|
+
/** Builds the desktop deep link handled by the `peers://` protocol handler. */
|
|
329
|
+
export declare function inviteDeepLink(token: PeersInvite): string;
|
|
330
|
+
/** Clamps a requested lifetime to the allowed range for the token shape. */
|
|
331
|
+
export declare function clampInviteExpiry(requestedMs: number | undefined, defaults: {
|
|
332
|
+
defaultMs: number;
|
|
333
|
+
maxMs: number;
|
|
334
|
+
}): number;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.peersInviteSchema = exports.peersInviteContentsSchema = exports.inviteUsesSchema = exports.inviteGroupSchema = exports.groupInviteModeSchema = exports.inviteIdentitySchema = exports.INVITE_EXPIRY = exports.PEERS_INVITE_APP_FRAGMENT_PREFIX = exports.PEERS_INVITE_URL_BASE = exports.PEERS_INVITE_VERSION = void 0;
|
|
4
|
+
exports.newInviteNonce = newInviteNonce;
|
|
5
|
+
exports.createInviteToken = createInviteToken;
|
|
6
|
+
exports.encodeInviteToken = encodeInviteToken;
|
|
7
|
+
exports.parseInviteToken = parseInviteToken;
|
|
8
|
+
exports.isInviteExpired = isInviteExpired;
|
|
9
|
+
exports.extractInviteTokenText = extractInviteTokenText;
|
|
10
|
+
exports.inviteUrl = inviteUrl;
|
|
11
|
+
exports.inviteDeepLink = inviteDeepLink;
|
|
12
|
+
exports.clampInviteExpiry = clampInviteExpiry;
|
|
13
|
+
const zod_1 = require("zod");
|
|
14
|
+
const group_member_roles_1 = require("../data/group-member-roles");
|
|
15
|
+
const keys_1 = require("../keys");
|
|
16
|
+
const zod_types_1 = require("../types/zod-types");
|
|
17
|
+
const base64UrlSchema = zod_1.z.string().regex(/^[A-Za-z0-9_-]+$/);
|
|
18
|
+
/** Token format version this module produces and accepts. */
|
|
19
|
+
exports.PEERS_INVITE_VERSION = 1;
|
|
20
|
+
/** Canonical landing page that carries a token in its URL fragment. */
|
|
21
|
+
exports.PEERS_INVITE_URL_BASE = "https://peers.app/i";
|
|
22
|
+
/** URL fragment prefix used when a token is handed to the web app directly. */
|
|
23
|
+
exports.PEERS_INVITE_APP_FRAGMENT_PREFIX = "#invite=";
|
|
24
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
25
|
+
/** Default and maximum lifetimes for each invite shape. */
|
|
26
|
+
exports.INVITE_EXPIRY = {
|
|
27
|
+
contactDefaultMs: 7 * DAY_MS,
|
|
28
|
+
profileDefaultMs: 365 * DAY_MS,
|
|
29
|
+
groupApproveDefaultMs: 7 * DAY_MS,
|
|
30
|
+
groupOpenDefaultMs: DAY_MS,
|
|
31
|
+
groupMaxMs: 30 * DAY_MS,
|
|
32
|
+
contactMaxMs: 365 * DAY_MS,
|
|
33
|
+
};
|
|
34
|
+
/** Public identity of a party to an invite. */
|
|
35
|
+
exports.inviteIdentitySchema = zod_1.z
|
|
36
|
+
.object({
|
|
37
|
+
userId: zod_types_1.zodPeerId,
|
|
38
|
+
publicKey: base64UrlSchema.length(43),
|
|
39
|
+
publicBoxKey: base64UrlSchema.length(43),
|
|
40
|
+
displayName: zod_1.z.string().max(100).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
/** How joiners are admitted by a group token. */
|
|
44
|
+
exports.groupInviteModeSchema = zod_1.z.enum(["approve", "open"]);
|
|
45
|
+
/** Group facts embedded in a group token so the joiner can preview it offline. */
|
|
46
|
+
exports.inviteGroupSchema = zod_1.z
|
|
47
|
+
.object({
|
|
48
|
+
groupId: zod_types_1.zodPeerId,
|
|
49
|
+
name: zod_1.z.string().max(200),
|
|
50
|
+
publicKey: base64UrlSchema.length(43),
|
|
51
|
+
publicBoxKey: base64UrlSchema.length(43),
|
|
52
|
+
role: zod_1.z.nativeEnum(group_member_roles_1.GroupMemberRole),
|
|
53
|
+
mode: exports.groupInviteModeSchema,
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
/** Number of accepts a token allows; profile QRs and reusable group links are unlimited. */
|
|
57
|
+
exports.inviteUsesSchema = zod_1.z.union([zod_1.z.literal(1), zod_1.z.literal("unlimited")]);
|
|
58
|
+
/** Unsigned contents of a PeersInvite v1 token. */
|
|
59
|
+
exports.peersInviteContentsSchema = zod_1.z
|
|
60
|
+
.object({
|
|
61
|
+
v: zod_1.z.literal(exports.PEERS_INVITE_VERSION),
|
|
62
|
+
type: zod_1.z.enum(["contact", "group"]),
|
|
63
|
+
inviter: exports.inviteIdentitySchema,
|
|
64
|
+
/** ISO 8601 instant after which the token is invalid. */
|
|
65
|
+
expiresAt: zod_1.z.string().datetime(),
|
|
66
|
+
/** 128-bit random value; identifies the token and links replies to it. */
|
|
67
|
+
nonce: base64UrlSchema.length(22),
|
|
68
|
+
/** Inviter's invite epoch at issue time; a bumped epoch revokes the token. */
|
|
69
|
+
epoch: zod_1.z.number().int().min(0),
|
|
70
|
+
uses: exports.inviteUsesSchema,
|
|
71
|
+
group: exports.inviteGroupSchema.optional(),
|
|
72
|
+
})
|
|
73
|
+
.strict()
|
|
74
|
+
.refine((value) => (value.type === "group") === (value.group !== undefined), {
|
|
75
|
+
message: "Group tokens must carry group facts and contact tokens must not",
|
|
76
|
+
});
|
|
77
|
+
/** Signed PeersInvite v1 token: contents plus the inviter's detached Ed25519 signature. */
|
|
78
|
+
exports.peersInviteSchema = zod_1.z
|
|
79
|
+
.object({
|
|
80
|
+
v: zod_1.z.literal(exports.PEERS_INVITE_VERSION),
|
|
81
|
+
type: zod_1.z.enum(["contact", "group"]),
|
|
82
|
+
inviter: exports.inviteIdentitySchema,
|
|
83
|
+
expiresAt: zod_1.z.string().datetime(),
|
|
84
|
+
nonce: base64UrlSchema.length(22),
|
|
85
|
+
epoch: zod_1.z.number().int().min(0),
|
|
86
|
+
uses: exports.inviteUsesSchema,
|
|
87
|
+
group: exports.inviteGroupSchema.optional(),
|
|
88
|
+
signature: base64UrlSchema.length(86),
|
|
89
|
+
})
|
|
90
|
+
.strict();
|
|
91
|
+
/** Generates a fresh 128-bit token nonce. */
|
|
92
|
+
function newInviteNonce() {
|
|
93
|
+
return (0, keys_1.newToken)(16);
|
|
94
|
+
}
|
|
95
|
+
/** Signs token contents with the inviter's secret key. */
|
|
96
|
+
function createInviteToken(contents, inviterSecretKey) {
|
|
97
|
+
const parsed = exports.peersInviteContentsSchema.parse({ v: exports.PEERS_INVITE_VERSION, ...contents });
|
|
98
|
+
const signed = (0, keys_1.signObjectWithSecretKey)(parsed, inviterSecretKey);
|
|
99
|
+
if (signed.publicKey !== parsed.inviter.publicKey) {
|
|
100
|
+
throw new Error("Invite must be signed by the inviter's own key");
|
|
101
|
+
}
|
|
102
|
+
return exports.peersInviteSchema.parse({ ...parsed, signature: signed.signature });
|
|
103
|
+
}
|
|
104
|
+
/** Encodes a token as unpadded base64url for URLs, QR codes, and copy/paste. */
|
|
105
|
+
function encodeInviteToken(token) {
|
|
106
|
+
return (0, keys_1.encodeBase64)(new TextEncoder().encode(JSON.stringify(exports.peersInviteSchema.parse(token))));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Parses a pasted token, invite URL, or `peers://` link and verifies its signature and expiry.
|
|
110
|
+
*
|
|
111
|
+
* @throws with a user-safe message that never echoes the input
|
|
112
|
+
*/
|
|
113
|
+
function parseInviteToken(input, options = {}) {
|
|
114
|
+
const encoded = extractInviteTokenText(input);
|
|
115
|
+
let token;
|
|
116
|
+
try {
|
|
117
|
+
const json = new TextDecoder().decode((0, keys_1.decodeBase64)(encoded));
|
|
118
|
+
token = exports.peersInviteSchema.parse(JSON.parse(json));
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new Error("Invite is not a valid Peers invite");
|
|
122
|
+
}
|
|
123
|
+
const { signature, ...contents } = token;
|
|
124
|
+
try {
|
|
125
|
+
(0, keys_1.openSignedObject)({
|
|
126
|
+
contents: exports.peersInviteContentsSchema.parse(contents),
|
|
127
|
+
signature,
|
|
128
|
+
publicKey: token.inviter.publicKey,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
throw new Error("Invite signature is not valid");
|
|
133
|
+
}
|
|
134
|
+
if (options.expectType && token.type !== options.expectType) {
|
|
135
|
+
throw new Error(`Expected a ${options.expectType} invite`);
|
|
136
|
+
}
|
|
137
|
+
if (!options.allowExpired && isInviteExpired(token, options.now)) {
|
|
138
|
+
throw new Error("Invite has expired");
|
|
139
|
+
}
|
|
140
|
+
return token;
|
|
141
|
+
}
|
|
142
|
+
/** Whether a token's expiry has passed. */
|
|
143
|
+
function isInviteExpired(token, now = Date.now()) {
|
|
144
|
+
return Date.parse(token.expiresAt) <= now;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Pulls the base64url token out of any of the shapes users paste: the bare token,
|
|
148
|
+
* `https://peers.app/i#<token>`, `/?app#invite=<token>`, or `peers://i#<token>`.
|
|
149
|
+
*/
|
|
150
|
+
function extractInviteTokenText(input) {
|
|
151
|
+
const trimmed = input.trim();
|
|
152
|
+
const hashIndex = trimmed.indexOf("#");
|
|
153
|
+
let candidate = hashIndex >= 0 ? trimmed.slice(hashIndex + 1) : trimmed;
|
|
154
|
+
if (candidate.startsWith(exports.PEERS_INVITE_APP_FRAGMENT_PREFIX.slice(1))) {
|
|
155
|
+
candidate = candidate.slice(exports.PEERS_INVITE_APP_FRAGMENT_PREFIX.length - 1);
|
|
156
|
+
}
|
|
157
|
+
candidate = candidate.split(/[?&\s]/)[0];
|
|
158
|
+
if (!/^[A-Za-z0-9_-]{16,}$/.test(candidate)) {
|
|
159
|
+
throw new Error("Invite is not a valid Peers invite");
|
|
160
|
+
}
|
|
161
|
+
return candidate;
|
|
162
|
+
}
|
|
163
|
+
/** Builds the shareable landing-page URL; the token travels only in the fragment. */
|
|
164
|
+
function inviteUrl(token, base = exports.PEERS_INVITE_URL_BASE) {
|
|
165
|
+
return `${base}#${encodeInviteToken(token)}`;
|
|
166
|
+
}
|
|
167
|
+
/** Builds the desktop deep link handled by the `peers://` protocol handler. */
|
|
168
|
+
function inviteDeepLink(token) {
|
|
169
|
+
return `peers://i#${encodeInviteToken(token)}`;
|
|
170
|
+
}
|
|
171
|
+
/** Clamps a requested lifetime to the allowed range for the token shape. */
|
|
172
|
+
function clampInviteExpiry(requestedMs, defaults) {
|
|
173
|
+
const value = requestedMs ?? defaults.defaultMs;
|
|
174
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
175
|
+
return defaults.defaultMs;
|
|
176
|
+
}
|
|
177
|
+
return Math.min(value, defaults.maxMs);
|
|
178
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|