@freqhole/haruspex 0.0.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/flows/add-peer/machine.d.ts +36 -0
- package/dist/flows/add-peer/machine.d.ts.map +1 -0
- package/dist/flows/add-peer/machine.js +500 -0
- package/dist/flows/add-peer/machine.test.d.ts +2 -0
- package/dist/flows/add-peer/machine.test.d.ts.map +1 -0
- package/dist/flows/add-peer/machine.test.js +421 -0
- package/dist/flows/add-peer/runner.d.ts +16 -0
- package/dist/flows/add-peer/runner.d.ts.map +1 -0
- package/dist/flows/add-peer/runner.js +349 -0
- package/dist/flows/add-peer/runner.test.d.ts +2 -0
- package/dist/flows/add-peer/runner.test.d.ts.map +1 -0
- package/dist/flows/add-peer/runner.test.js +205 -0
- package/dist/flows/add-peer/types.d.ts +271 -0
- package/dist/flows/add-peer/types.d.ts.map +1 -0
- package/dist/flows/add-peer/types.js +10 -0
- package/dist/flows/index.d.ts +6 -0
- package/dist/flows/index.d.ts.map +1 -0
- package/dist/flows/index.js +6 -0
- package/dist/identity/idbStore.d.ts +51 -0
- package/dist/identity/idbStore.d.ts.map +1 -0
- package/dist/identity/idbStore.js +183 -0
- package/dist/identity/idbStore.test.d.ts +2 -0
- package/dist/identity/idbStore.test.d.ts.map +1 -0
- package/dist/identity/idbStore.test.js +103 -0
- package/dist/identity/index.d.ts +8 -0
- package/dist/identity/index.d.ts.map +1 -0
- package/dist/identity/index.js +3 -0
- package/dist/identity/resolve.d.ts +31 -0
- package/dist/identity/resolve.d.ts.map +1 -0
- package/dist/identity/resolve.js +37 -0
- package/dist/identity/resolve.test.d.ts +2 -0
- package/dist/identity/resolve.test.d.ts.map +1 -0
- package/dist/identity/resolve.test.js +105 -0
- package/dist/identity/types.d.ts +21 -0
- package/dist/identity/types.d.ts.map +1 -0
- package/dist/identity/types.js +1 -0
- package/dist/identity/webLocks.d.ts +22 -0
- package/dist/identity/webLocks.d.ts.map +1 -0
- package/dist/identity/webLocks.js +67 -0
- package/dist/identity/webLocks.test.d.ts +2 -0
- package/dist/identity/webLocks.test.d.ts.map +1 -0
- package/dist/identity/webLocks.test.js +125 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +7 -0
- package/dist/knock/index.d.ts +10 -0
- package/dist/knock/index.d.ts.map +1 -0
- package/dist/knock/index.js +5 -0
- package/dist/knock/pending-checker.d.ts +19 -0
- package/dist/knock/pending-checker.d.ts.map +1 -0
- package/dist/knock/pending-checker.js +27 -0
- package/dist/knock/pending-checker.test.d.ts +2 -0
- package/dist/knock/pending-checker.test.d.ts.map +1 -0
- package/dist/knock/pending-checker.test.js +93 -0
- package/dist/knock/requester.d.ts +26 -0
- package/dist/knock/requester.d.ts.map +1 -0
- package/dist/knock/requester.js +72 -0
- package/dist/knock/requester.test.d.ts +2 -0
- package/dist/knock/requester.test.d.ts.map +1 -0
- package/dist/knock/requester.test.js +108 -0
- package/dist/knock/responder.d.ts +13 -0
- package/dist/knock/responder.d.ts.map +1 -0
- package/dist/knock/responder.js +40 -0
- package/dist/knock/responder.test.d.ts +2 -0
- package/dist/knock/responder.test.d.ts.map +1 -0
- package/dist/knock/responder.test.js +89 -0
- package/dist/knock/store.d.ts +11 -0
- package/dist/knock/store.d.ts.map +1 -0
- package/dist/knock/store.js +211 -0
- package/dist/knock/store.test.d.ts +2 -0
- package/dist/knock/store.test.d.ts.map +1 -0
- package/dist/knock/store.test.js +195 -0
- package/dist/knock/types.d.ts +144 -0
- package/dist/knock/types.d.ts.map +1 -0
- package/dist/knock/types.js +26 -0
- package/dist/permissions/access.d.ts +54 -0
- package/dist/permissions/access.d.ts.map +1 -0
- package/dist/permissions/access.js +56 -0
- package/dist/permissions/access.test.d.ts +2 -0
- package/dist/permissions/access.test.d.ts.map +1 -0
- package/dist/permissions/access.test.js +72 -0
- package/dist/permissions/index.d.ts +5 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +2 -0
- package/dist/permissions/role-table.d.ts +11 -0
- package/dist/permissions/role-table.d.ts.map +1 -0
- package/dist/permissions/role-table.js +16 -0
- package/dist/protocol/client.d.ts +67 -0
- package/dist/protocol/client.d.ts.map +1 -0
- package/dist/protocol/client.js +195 -0
- package/dist/protocol/client.test.d.ts +2 -0
- package/dist/protocol/client.test.d.ts.map +1 -0
- package/dist/protocol/client.test.js +282 -0
- package/dist/protocol/codec.d.ts +45 -0
- package/dist/protocol/codec.d.ts.map +1 -0
- package/dist/protocol/codec.js +111 -0
- package/dist/protocol/codec.test.d.ts +2 -0
- package/dist/protocol/codec.test.d.ts.map +1 -0
- package/dist/protocol/codec.test.js +93 -0
- package/dist/protocol/extensions.d.ts +33 -0
- package/dist/protocol/extensions.d.ts.map +1 -0
- package/dist/protocol/extensions.js +36 -0
- package/dist/protocol/extensions.test.d.ts +2 -0
- package/dist/protocol/extensions.test.d.ts.map +1 -0
- package/dist/protocol/extensions.test.js +65 -0
- package/dist/protocol/fixtures.test.d.ts +2 -0
- package/dist/protocol/fixtures.test.d.ts.map +1 -0
- package/dist/protocol/fixtures.test.js +51 -0
- package/dist/protocol/index.d.ts +9 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +4 -0
- package/dist/protocol/messages.d.ts +480 -0
- package/dist/protocol/messages.d.ts.map +1 -0
- package/dist/protocol/messages.js +255 -0
- package/dist/protocol/messages.test.d.ts +2 -0
- package/dist/protocol/messages.test.d.ts.map +1 -0
- package/dist/protocol/messages.test.js +75 -0
- package/dist/share/codec.d.ts +58 -0
- package/dist/share/codec.d.ts.map +1 -0
- package/dist/share/codec.js +222 -0
- package/dist/share/codec.test.d.ts +2 -0
- package/dist/share/codec.test.d.ts.map +1 -0
- package/dist/share/codec.test.js +150 -0
- package/dist/share/detect.d.ts +24 -0
- package/dist/share/detect.d.ts.map +1 -0
- package/dist/share/detect.js +25 -0
- package/dist/share/detect.test.d.ts +2 -0
- package/dist/share/detect.test.d.ts.map +1 -0
- package/dist/share/detect.test.js +31 -0
- package/dist/share/index.d.ts +7 -0
- package/dist/share/index.d.ts.map +1 -0
- package/dist/share/index.js +3 -0
- package/dist/share/peer-addr.d.ts +32 -0
- package/dist/share/peer-addr.d.ts.map +1 -0
- package/dist/share/peer-addr.js +72 -0
- package/dist/share/peer-addr.test.d.ts +2 -0
- package/dist/share/peer-addr.test.d.ts.map +1 -0
- package/dist/share/peer-addr.test.js +58 -0
- package/dist/solid/auth-form.d.ts +43 -0
- package/dist/solid/auth-form.d.ts.map +1 -0
- package/dist/solid/auth-form.js +55 -0
- package/dist/solid/auth-form.test.d.ts +2 -0
- package/dist/solid/auth-form.test.d.ts.map +1 -0
- package/dist/solid/auth-form.test.js +131 -0
- package/dist/solid/auth-status.d.ts +21 -0
- package/dist/solid/auth-status.d.ts.map +1 -0
- package/dist/solid/auth-status.js +58 -0
- package/dist/solid/auth-status.test.d.ts +2 -0
- package/dist/solid/auth-status.test.d.ts.map +1 -0
- package/dist/solid/auth-status.test.js +60 -0
- package/dist/solid/index.d.ts +5 -0
- package/dist/solid/index.d.ts.map +1 -0
- package/dist/solid/index.js +10 -0
- package/dist/solid/knock-inbox.d.ts +61 -0
- package/dist/solid/knock-inbox.d.ts.map +1 -0
- package/dist/solid/knock-inbox.js +123 -0
- package/dist/solid/knock-inbox.test.d.ts +2 -0
- package/dist/solid/knock-inbox.test.d.ts.map +1 -0
- package/dist/solid/knock-inbox.test.js +153 -0
- package/dist/solid/reauth-flow.d.ts +26 -0
- package/dist/solid/reauth-flow.d.ts.map +1 -0
- package/dist/solid/reauth-flow.js +34 -0
- package/dist/solid/reauth-flow.test.d.ts +2 -0
- package/dist/solid/reauth-flow.test.d.ts.map +1 -0
- package/dist/solid/reauth-flow.test.js +91 -0
- package/dist/state/endpoint-control.d.ts +22 -0
- package/dist/state/endpoint-control.d.ts.map +1 -0
- package/dist/state/endpoint-control.js +34 -0
- package/dist/state/endpoint-control.test.d.ts +2 -0
- package/dist/state/endpoint-control.test.d.ts.map +1 -0
- package/dist/state/endpoint-control.test.js +63 -0
- package/dist/state/friend-directory.d.ts +37 -0
- package/dist/state/friend-directory.d.ts.map +1 -0
- package/dist/state/friend-directory.js +44 -0
- package/dist/state/friend-directory.test.d.ts +2 -0
- package/dist/state/friend-directory.test.d.ts.map +1 -0
- package/dist/state/friend-directory.test.js +63 -0
- package/dist/state/index.d.ts +6 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +3 -0
- package/dist/state/peer-names.d.ts +7 -0
- package/dist/state/peer-names.d.ts.map +1 -0
- package/dist/state/peer-names.js +21 -0
- package/dist/state/peer-names.test.d.ts +2 -0
- package/dist/state/peer-names.test.d.ts.map +1 -0
- package/dist/state/peer-names.test.js +29 -0
- package/dist/testing/idb-harness.d.ts +9 -0
- package/dist/testing/idb-harness.d.ts.map +1 -0
- package/dist/testing/idb-harness.js +19 -0
- package/dist/testing/idb-harness.test.d.ts +2 -0
- package/dist/testing/idb-harness.test.d.ts.map +1 -0
- package/dist/testing/idb-harness.test.js +18 -0
- package/dist/testing/identity-fixtures.d.ts +10 -0
- package/dist/testing/identity-fixtures.d.ts.map +1 -0
- package/dist/testing/identity-fixtures.js +21 -0
- package/dist/testing/identity-fixtures.test.d.ts +2 -0
- package/dist/testing/identity-fixtures.test.d.ts.map +1 -0
- package/dist/testing/identity-fixtures.test.js +28 -0
- package/dist/testing/index.d.ts +7 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/knock-fixtures.d.ts +13 -0
- package/dist/testing/knock-fixtures.d.ts.map +1 -0
- package/dist/testing/knock-fixtures.js +45 -0
- package/dist/testing/knock-fixtures.test.d.ts +2 -0
- package/dist/testing/knock-fixtures.test.d.ts.map +1 -0
- package/dist/testing/knock-fixtures.test.js +65 -0
- package/dist/testing/protocol-fixtures.d.ts +7 -0
- package/dist/testing/protocol-fixtures.d.ts.map +1 -0
- package/dist/testing/protocol-fixtures.js +28 -0
- package/dist/testing/protocol-fixtures.test.d.ts +2 -0
- package/dist/testing/protocol-fixtures.test.d.ts.map +1 -0
- package/dist/testing/protocol-fixtures.test.js +19 -0
- package/dist/testing/state-fixtures.d.ts +33 -0
- package/dist/testing/state-fixtures.d.ts.map +1 -0
- package/dist/testing/state-fixtures.js +67 -0
- package/dist/testing/state-fixtures.test.d.ts +2 -0
- package/dist/testing/state-fixtures.test.d.ts.map +1 -0
- package/dist/testing/state-fixtures.test.js +68 -0
- package/dist/testing/webauthn-mocks.d.ts +27 -0
- package/dist/testing/webauthn-mocks.d.ts.map +1 -0
- package/dist/testing/webauthn-mocks.js +78 -0
- package/dist/testing/webauthn-mocks.test.d.ts +2 -0
- package/dist/testing/webauthn-mocks.test.d.ts.map +1 -0
- package/dist/testing/webauthn-mocks.test.js +40 -0
- package/dist/webauthn/ceremony.d.ts +76 -0
- package/dist/webauthn/ceremony.d.ts.map +1 -0
- package/dist/webauthn/ceremony.js +136 -0
- package/dist/webauthn/ceremony.test.d.ts +2 -0
- package/dist/webauthn/ceremony.test.d.ts.map +1 -0
- package/dist/webauthn/ceremony.test.js +245 -0
- package/dist/webauthn/codec.d.ts +39 -0
- package/dist/webauthn/codec.d.ts.map +1 -0
- package/dist/webauthn/codec.js +118 -0
- package/dist/webauthn/codec.test.d.ts +2 -0
- package/dist/webauthn/codec.test.d.ts.map +1 -0
- package/dist/webauthn/codec.test.js +126 -0
- package/dist/webauthn/index.d.ts +7 -0
- package/dist/webauthn/index.d.ts.map +1 -0
- package/dist/webauthn/index.js +3 -0
- package/dist/webauthn/transport.d.ts +58 -0
- package/dist/webauthn/transport.d.ts.map +1 -0
- package/dist/webauthn/transport.js +6 -0
- package/dist/webauthn/types.d.ts +78 -0
- package/dist/webauthn/types.d.ts.map +1 -0
- package/dist/webauthn/types.js +29 -0
- package/package.json +83 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// the unified friendz protocol message set - the normative wire-message
|
|
2
|
+
// shapes for peer-to-peer presence, friend requests, knocks, and identity
|
|
3
|
+
// updates. every message is validated against the same 20 fixture files
|
|
4
|
+
// the rust crate's message enum was generated from
|
|
5
|
+
// (haruspex/rust/fixtures/protocol/), so a message accepted here parses
|
|
6
|
+
// identically on both sides of the wire.
|
|
7
|
+
//
|
|
8
|
+
// normalization:
|
|
9
|
+
// - the wire discriminant is a `type` field, kebab-case values.
|
|
10
|
+
// - every other field is camelCase.
|
|
11
|
+
// - every message carries `v` (defaulting to 1 when the sender omits it,
|
|
12
|
+
// for a transition window where not every peer sends it yet).
|
|
13
|
+
// - identity naming: `nodeId` everywhere; `username` for handle-like
|
|
14
|
+
// names; avatars use a blob-id reference (`avatarBlobId`) on the
|
|
15
|
+
// hello/hello-ok handshake, and an inline data url (`avatarDataUrl`)
|
|
16
|
+
// on profile-response/identity-update.
|
|
17
|
+
// - knock outcome status is `pending | accepted | denied`.
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
const wireVersion = z.number().int().nonnegative().default(1);
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// shared enums
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/** the grantable role vocabulary carried on the wire (knock-outcome, acl-change). */
|
|
24
|
+
export const WireRoleSchema = z.enum(["viewer", "member", "admin", "root"]);
|
|
25
|
+
/** resolution of a knock request. */
|
|
26
|
+
export const WireKnockStatusSchema = z.enum(["pending", "accepted", "denied"]);
|
|
27
|
+
/** whether a peer may browse without knocking first, or must knock. */
|
|
28
|
+
export const BrowseCapabilitySchema = z.enum(["public", "knock"]);
|
|
29
|
+
/** capability bag carried on the hello/hello-ok handshake. */
|
|
30
|
+
export const CapabilitiesSchema = z.object({
|
|
31
|
+
browse: BrowseCapabilitySchema,
|
|
32
|
+
});
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// knock scope
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* the wire shape of a knock's scope: what access is being requested.
|
|
38
|
+
* - account: acceptance creates or links a user identity
|
|
39
|
+
* - browse: list access, no specific resource named
|
|
40
|
+
* - resource: access to one named resource, with an optional requested role
|
|
41
|
+
*/
|
|
42
|
+
export const WireKnockScopeSchema = z.discriminatedUnion("kind", [
|
|
43
|
+
z.object({
|
|
44
|
+
kind: z.literal("account"),
|
|
45
|
+
requestedUsername: z.string().optional(),
|
|
46
|
+
}),
|
|
47
|
+
z.object({
|
|
48
|
+
kind: z.literal("browse"),
|
|
49
|
+
}),
|
|
50
|
+
z.object({
|
|
51
|
+
kind: z.literal("resource"),
|
|
52
|
+
resourceId: z.string(),
|
|
53
|
+
requestedRole: WireRoleSchema.optional(),
|
|
54
|
+
}),
|
|
55
|
+
]);
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// gossip digest sub-shapes
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
/** a pending-knock entry gossiped in a gossip-digest message. */
|
|
60
|
+
export const GossipDigestPendingKnockSchema = z.object({
|
|
61
|
+
knockId: z.string(),
|
|
62
|
+
nodeId: z.string(),
|
|
63
|
+
username: z.string().optional(),
|
|
64
|
+
message: z.string(),
|
|
65
|
+
scope: WireKnockScopeSchema,
|
|
66
|
+
knockedAt: z.string(),
|
|
67
|
+
});
|
|
68
|
+
/** a profile-doc pointer entry in a gossip digest. */
|
|
69
|
+
export const GossipDigestProfileEntrySchema = z.object({
|
|
70
|
+
peerNodeId: z.string(),
|
|
71
|
+
profileDocId: z.string(),
|
|
72
|
+
updatedAt: z.string(),
|
|
73
|
+
});
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// core message variants
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
const ProfileRequestSchema = z.object({
|
|
78
|
+
type: z.literal("profile-request"),
|
|
79
|
+
v: wireVersion,
|
|
80
|
+
});
|
|
81
|
+
const ProfileResponseSchema = z.object({
|
|
82
|
+
type: z.literal("profile-response"),
|
|
83
|
+
v: wireVersion,
|
|
84
|
+
username: z.string(),
|
|
85
|
+
bio: z.string(),
|
|
86
|
+
avatarDataUrl: z.string(),
|
|
87
|
+
accentColor: z.number().optional(),
|
|
88
|
+
profileDocId: z.string().optional(),
|
|
89
|
+
profileUpdatedAt: z.string().optional(),
|
|
90
|
+
});
|
|
91
|
+
const FriendRequestSchema = z.object({
|
|
92
|
+
type: z.literal("friend-request"),
|
|
93
|
+
v: wireVersion,
|
|
94
|
+
fromNodeId: z.string(),
|
|
95
|
+
fromUsername: z.string(),
|
|
96
|
+
isHub: z.boolean().optional(),
|
|
97
|
+
});
|
|
98
|
+
const FriendAcceptSchema = z.object({
|
|
99
|
+
type: z.literal("friend-accept"),
|
|
100
|
+
v: wireVersion,
|
|
101
|
+
fromNodeId: z.string(),
|
|
102
|
+
fromUsername: z.string(),
|
|
103
|
+
isHub: z.boolean().optional(),
|
|
104
|
+
});
|
|
105
|
+
const FriendAcceptAckSchema = z.object({
|
|
106
|
+
type: z.literal("friend-accept-ack"),
|
|
107
|
+
v: wireVersion,
|
|
108
|
+
fromNodeId: z.string(),
|
|
109
|
+
});
|
|
110
|
+
const FriendRejectSchema = z.object({
|
|
111
|
+
type: z.literal("friend-reject"),
|
|
112
|
+
v: wireVersion,
|
|
113
|
+
fromNodeId: z.string(),
|
|
114
|
+
});
|
|
115
|
+
const HeartbeatSchema = z.object({
|
|
116
|
+
type: z.literal("heartbeat"),
|
|
117
|
+
v: wireVersion,
|
|
118
|
+
nodeId: z.string(),
|
|
119
|
+
username: z.string(),
|
|
120
|
+
appPayload: z.unknown().optional(),
|
|
121
|
+
});
|
|
122
|
+
const OfflineAnnouncementSchema = z.object({
|
|
123
|
+
type: z.literal("offline-announcement"),
|
|
124
|
+
v: wireVersion,
|
|
125
|
+
nodeId: z.string(),
|
|
126
|
+
});
|
|
127
|
+
const HelloSchema = z.object({
|
|
128
|
+
type: z.literal("hello"),
|
|
129
|
+
v: wireVersion,
|
|
130
|
+
nodeId: z.string(),
|
|
131
|
+
username: z.string().optional(),
|
|
132
|
+
avatarBlobId: z.string().optional(),
|
|
133
|
+
capabilities: CapabilitiesSchema,
|
|
134
|
+
});
|
|
135
|
+
const HelloOkSchema = z.object({
|
|
136
|
+
type: z.literal("hello-ok"),
|
|
137
|
+
v: wireVersion,
|
|
138
|
+
nodeId: z.string(),
|
|
139
|
+
username: z.string().optional(),
|
|
140
|
+
avatarBlobId: z.string().optional(),
|
|
141
|
+
capabilities: CapabilitiesSchema,
|
|
142
|
+
});
|
|
143
|
+
const KnockRequestSchema = z.object({
|
|
144
|
+
type: z.literal("knock-request"),
|
|
145
|
+
v: wireVersion,
|
|
146
|
+
knockId: z.string(),
|
|
147
|
+
nodeId: z.string(),
|
|
148
|
+
username: z.string().optional(),
|
|
149
|
+
message: z.string(),
|
|
150
|
+
scope: WireKnockScopeSchema,
|
|
151
|
+
});
|
|
152
|
+
const KnockAckSchema = z.object({
|
|
153
|
+
type: z.literal("knock-ack"),
|
|
154
|
+
v: wireVersion,
|
|
155
|
+
knockId: z.string(),
|
|
156
|
+
ackerNodeId: z.string(),
|
|
157
|
+
resourceId: z.string().optional(),
|
|
158
|
+
});
|
|
159
|
+
const KnockOutcomeSchema = z.object({
|
|
160
|
+
type: z.literal("knock-outcome"),
|
|
161
|
+
v: wireVersion,
|
|
162
|
+
knockId: z.string().optional(),
|
|
163
|
+
status: WireKnockStatusSchema,
|
|
164
|
+
grantedRole: WireRoleSchema.optional(),
|
|
165
|
+
grantedResourceIds: z.array(z.string()).default([]),
|
|
166
|
+
byNodeId: z.string().optional(),
|
|
167
|
+
});
|
|
168
|
+
const IdentityUpdateSchema = z.object({
|
|
169
|
+
type: z.literal("identity-update"),
|
|
170
|
+
v: wireVersion,
|
|
171
|
+
nodeId: z.string(),
|
|
172
|
+
username: z.string().optional(),
|
|
173
|
+
avatarDataUrl: z.string().optional(),
|
|
174
|
+
});
|
|
175
|
+
const AclChangeSchema = z.object({
|
|
176
|
+
type: z.literal("acl-change"),
|
|
177
|
+
v: wireVersion,
|
|
178
|
+
resourceId: z.string(),
|
|
179
|
+
resourceTitle: z.string().optional(),
|
|
180
|
+
targetNodeId: z.string(),
|
|
181
|
+
newRole: WireRoleSchema.optional(),
|
|
182
|
+
changedBy: z.string(),
|
|
183
|
+
changedByUsername: z.string(),
|
|
184
|
+
});
|
|
185
|
+
const GossipDigestSchema = z.object({
|
|
186
|
+
type: z.literal("gossip-digest"),
|
|
187
|
+
v: wireVersion,
|
|
188
|
+
pendingKnocks: z.array(GossipDigestPendingKnockSchema).default([]),
|
|
189
|
+
profiles: z.array(GossipDigestProfileEntrySchema).default([]),
|
|
190
|
+
appPayload: z.unknown().optional(),
|
|
191
|
+
});
|
|
192
|
+
const BlobSeekSchema = z.object({
|
|
193
|
+
type: z.literal("blob-seek"),
|
|
194
|
+
v: wireVersion,
|
|
195
|
+
needed: z.array(z.string()).default([]),
|
|
196
|
+
});
|
|
197
|
+
const BlobOfferSchema = z.object({
|
|
198
|
+
type: z.literal("blob-offer"),
|
|
199
|
+
v: wireVersion,
|
|
200
|
+
available: z.array(z.string()).default([]),
|
|
201
|
+
});
|
|
202
|
+
const ErrorMessageSchema = z.object({
|
|
203
|
+
type: z.literal("error"),
|
|
204
|
+
v: wireVersion,
|
|
205
|
+
code: z.string(),
|
|
206
|
+
message: z.string(),
|
|
207
|
+
});
|
|
208
|
+
/** the discriminated union of every core (non-namespaced) friendz message. */
|
|
209
|
+
export const CoreMessageSchema = z.discriminatedUnion("type", [
|
|
210
|
+
ProfileRequestSchema,
|
|
211
|
+
ProfileResponseSchema,
|
|
212
|
+
FriendRequestSchema,
|
|
213
|
+
FriendAcceptSchema,
|
|
214
|
+
FriendAcceptAckSchema,
|
|
215
|
+
FriendRejectSchema,
|
|
216
|
+
HeartbeatSchema,
|
|
217
|
+
OfflineAnnouncementSchema,
|
|
218
|
+
HelloSchema,
|
|
219
|
+
HelloOkSchema,
|
|
220
|
+
KnockRequestSchema,
|
|
221
|
+
KnockAckSchema,
|
|
222
|
+
KnockOutcomeSchema,
|
|
223
|
+
IdentityUpdateSchema,
|
|
224
|
+
AclChangeSchema,
|
|
225
|
+
GossipDigestSchema,
|
|
226
|
+
BlobSeekSchema,
|
|
227
|
+
BlobOfferSchema,
|
|
228
|
+
ErrorMessageSchema,
|
|
229
|
+
]);
|
|
230
|
+
/** every core message's wire `type` discriminant, in enum-declaration order. */
|
|
231
|
+
export const CORE_MESSAGE_TYPES = [
|
|
232
|
+
"profile-request",
|
|
233
|
+
"profile-response",
|
|
234
|
+
"friend-request",
|
|
235
|
+
"friend-accept",
|
|
236
|
+
"friend-accept-ack",
|
|
237
|
+
"friend-reject",
|
|
238
|
+
"heartbeat",
|
|
239
|
+
"offline-announcement",
|
|
240
|
+
"hello",
|
|
241
|
+
"hello-ok",
|
|
242
|
+
"knock-request",
|
|
243
|
+
"knock-ack",
|
|
244
|
+
"knock-outcome",
|
|
245
|
+
"identity-update",
|
|
246
|
+
"acl-change",
|
|
247
|
+
"gossip-digest",
|
|
248
|
+
"blob-seek",
|
|
249
|
+
"blob-offer",
|
|
250
|
+
"error",
|
|
251
|
+
];
|
|
252
|
+
/** is `type` one of the core (non-namespaced) message types above? */
|
|
253
|
+
export function isCoreMessageType(type) {
|
|
254
|
+
return CORE_MESSAGE_TYPES.includes(type);
|
|
255
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.test.d.ts","sourceRoot":"","sources":["../../src/protocol/messages.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { CoreMessageSchema, WireKnockScopeSchema, isCoreMessageType, } from "./messages.js";
|
|
3
|
+
describe("CoreMessageSchema", () => {
|
|
4
|
+
it("defaults a missing v to 1", () => {
|
|
5
|
+
const parsed = CoreMessageSchema.parse({ type: "profile-request" });
|
|
6
|
+
expect(parsed).toEqual({ type: "profile-request", v: 1 });
|
|
7
|
+
});
|
|
8
|
+
it("accepts an explicit v", () => {
|
|
9
|
+
const parsed = CoreMessageSchema.parse({ type: "profile-request", v: 3 });
|
|
10
|
+
expect(parsed.v).toBe(3);
|
|
11
|
+
});
|
|
12
|
+
it("rejects a type with no matching variant", () => {
|
|
13
|
+
expect(() => CoreMessageSchema.parse({ type: "not-a-real-type" })).toThrow();
|
|
14
|
+
});
|
|
15
|
+
it("defaults missing array fields to []", () => {
|
|
16
|
+
const parsed = CoreMessageSchema.parse({ type: "blob-seek" });
|
|
17
|
+
expect(parsed).toMatchObject({ type: "blob-seek", needed: [] });
|
|
18
|
+
});
|
|
19
|
+
it("parses a full hello handshake with capabilities", () => {
|
|
20
|
+
const parsed = CoreMessageSchema.parse({
|
|
21
|
+
type: "hello",
|
|
22
|
+
v: 1,
|
|
23
|
+
nodeId: "node-abc123",
|
|
24
|
+
username: "alice",
|
|
25
|
+
capabilities: { browse: "knock" },
|
|
26
|
+
});
|
|
27
|
+
expect(parsed).toMatchObject({
|
|
28
|
+
type: "hello",
|
|
29
|
+
nodeId: "node-abc123",
|
|
30
|
+
capabilities: { browse: "knock" },
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
it("rejects hello with an invalid browse capability", () => {
|
|
34
|
+
expect(() => CoreMessageSchema.parse({
|
|
35
|
+
type: "hello",
|
|
36
|
+
nodeId: "node-abc123",
|
|
37
|
+
capabilities: { browse: "sometimes" },
|
|
38
|
+
})).toThrow();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe("WireKnockScopeSchema", () => {
|
|
42
|
+
it("parses an account scope with an optional requestedUsername", () => {
|
|
43
|
+
expect(WireKnockScopeSchema.parse({ kind: "account" })).toEqual({ kind: "account" });
|
|
44
|
+
expect(WireKnockScopeSchema.parse({ kind: "account", requestedUsername: "bob" })).toEqual({ kind: "account", requestedUsername: "bob" });
|
|
45
|
+
});
|
|
46
|
+
it("parses a bare browse scope", () => {
|
|
47
|
+
expect(WireKnockScopeSchema.parse({ kind: "browse" })).toEqual({ kind: "browse" });
|
|
48
|
+
});
|
|
49
|
+
it("parses a resource scope with a resourceId and optional requestedRole", () => {
|
|
50
|
+
const parsed = WireKnockScopeSchema.parse({
|
|
51
|
+
kind: "resource",
|
|
52
|
+
resourceId: "doc-1",
|
|
53
|
+
requestedRole: "member",
|
|
54
|
+
});
|
|
55
|
+
expect(parsed).toEqual({ kind: "resource", resourceId: "doc-1", requestedRole: "member" });
|
|
56
|
+
});
|
|
57
|
+
it("rejects an unknown scope kind", () => {
|
|
58
|
+
expect(() => WireKnockScopeSchema.parse({ kind: "everyone" })).toThrow();
|
|
59
|
+
});
|
|
60
|
+
it("rejects an invalid role value", () => {
|
|
61
|
+
expect(() => WireKnockScopeSchema.parse({ kind: "resource", resourceId: "doc-1", requestedRole: "owner" })).toThrow();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
describe("isCoreMessageType", () => {
|
|
65
|
+
it("is true for every core type", () => {
|
|
66
|
+
expect(isCoreMessageType("hello")).toBe(true);
|
|
67
|
+
expect(isCoreMessageType("knock-outcome")).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it("is false for a namespaced app-extension type", () => {
|
|
70
|
+
expect(isCoreMessageType("skein:canvas-invite")).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
it("is false for an unknown type", () => {
|
|
73
|
+
expect(isCoreMessageType("not-a-real-type")).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** a bare node reference: "come dial this peer", nothing else. */
|
|
2
|
+
export interface NodeSharePayload {
|
|
3
|
+
kind: "node";
|
|
4
|
+
nodeId: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
}
|
|
7
|
+
/** a node id + automerge doc id, with an optional sharing-mode hint. */
|
|
8
|
+
export interface DocSharePayload {
|
|
9
|
+
kind: "doc";
|
|
10
|
+
nodeId: string;
|
|
11
|
+
docId: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
mode?: "public" | "knock";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* an entity hosted on a source (a p2p node id and/or an http origin - at
|
|
17
|
+
* least one should be set so a recipient can resolve it). `entityKind` is
|
|
18
|
+
* app-defined (e.g. "album", "song", "canvas") - this package does not own
|
|
19
|
+
* a fixed vocabulary for it.
|
|
20
|
+
*/
|
|
21
|
+
export interface EntitySharePayload {
|
|
22
|
+
kind: "entity";
|
|
23
|
+
source: {
|
|
24
|
+
nodeId?: string;
|
|
25
|
+
httpOrigin?: string;
|
|
26
|
+
};
|
|
27
|
+
entityKind: string;
|
|
28
|
+
entityId: string;
|
|
29
|
+
parentId?: string;
|
|
30
|
+
title?: string;
|
|
31
|
+
artist?: string;
|
|
32
|
+
album?: string;
|
|
33
|
+
}
|
|
34
|
+
export type ShareTokenPayload = NodeSharePayload | DocSharePayload | EntitySharePayload;
|
|
35
|
+
/** encode a share payload as a base64url token (no padding). */
|
|
36
|
+
export declare function encodeShareToken(payload: ShareTokenPayload): string;
|
|
37
|
+
/** build a url fragment for embedding in window.location.hash. */
|
|
38
|
+
export declare function shareFragment(payload: ShareTokenPayload): string;
|
|
39
|
+
/**
|
|
40
|
+
* strip a share url/fragment down to the bare token: handles a full url
|
|
41
|
+
* ending in a `#share/<token>` fragment, a bare `share/<token>` prefix, and
|
|
42
|
+
* a trailing `&`-joined query fragment appended after the token. returns
|
|
43
|
+
* the input unchanged (trimmed) when none of those prefixes are present,
|
|
44
|
+
* so a bare token passes through untouched.
|
|
45
|
+
*/
|
|
46
|
+
export declare function extractShareToken(input: string): string;
|
|
47
|
+
/**
|
|
48
|
+
* decode a share token back to a payload. accepts a raw token, a
|
|
49
|
+
* `#share/<token>` fragment, or a full url ending in one. also decodes the
|
|
50
|
+
* legacy doc-share shapes (versioned `{v:1,n,d,t,m}` and the bare,
|
|
51
|
+
* unversioned `{n,d}` form) and the legacy entity-share shape
|
|
52
|
+
* (`{v:1,s:{n,h},k,i,...}`), mapping every one of them onto the current
|
|
53
|
+
* payload types. returns null on anything invalid rather than throwing, so
|
|
54
|
+
* callers can treat "not a share token" as just another input
|
|
55
|
+
* classification.
|
|
56
|
+
*/
|
|
57
|
+
export declare function decodeShareToken(input: string): ShareTokenPayload | null;
|
|
58
|
+
//# sourceMappingURL=codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../src/share/codec.ts"],"names":[],"mappings":"AAiBA,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,KAAK,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,iBAAiB,GAAG,gBAAgB,GAAG,eAAe,GAAG,kBAAkB,CAAC;AA2IxF,gEAAgE;AAChE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAInE;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAEhE;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAcvD;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CA0CxE"}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// unified share-link token codec.
|
|
2
|
+
//
|
|
3
|
+
// three token shapes exist in the wild for sharing a p2p entity via a
|
|
4
|
+
// pasted string or url fragment:
|
|
5
|
+
// - a node id + an automerge doc id, with optional title/mode hints
|
|
6
|
+
// - a node id alone (a plain peer reference, no doc)
|
|
7
|
+
// - a source (p2p node id and/or http origin) plus an app-defined entity
|
|
8
|
+
// kind + id, with optional parent/title/artist/album display hints
|
|
9
|
+
//
|
|
10
|
+
// this codec's own wire format is versioned explicitly (`v`) so a future
|
|
11
|
+
// payload addition never breaks a link already sitting in someone's
|
|
12
|
+
// clipboard or browser history. decoding also recognizes the legacy token
|
|
13
|
+
// shapes below, none of which carry a wire version of their own that lines
|
|
14
|
+
// up with this codec's, so they are told apart by their field shape.
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
const CURRENT_WIRE_VERSION = 2;
|
|
17
|
+
// wire schema (short keys - these end up in url bars).
|
|
18
|
+
const NodeWireSchema = z.object({
|
|
19
|
+
v: z.literal(CURRENT_WIRE_VERSION),
|
|
20
|
+
k: z.literal("node"),
|
|
21
|
+
n: z.string().min(1),
|
|
22
|
+
t: z.string().optional(),
|
|
23
|
+
});
|
|
24
|
+
const DocWireSchema = z.object({
|
|
25
|
+
v: z.literal(CURRENT_WIRE_VERSION),
|
|
26
|
+
k: z.literal("doc"),
|
|
27
|
+
n: z.string().min(1),
|
|
28
|
+
d: z.string().min(1),
|
|
29
|
+
t: z.string().optional(),
|
|
30
|
+
m: z.enum(["public", "knock"]).optional(),
|
|
31
|
+
});
|
|
32
|
+
const EntityWireSchema = z.object({
|
|
33
|
+
v: z.literal(CURRENT_WIRE_VERSION),
|
|
34
|
+
k: z.literal("entity"),
|
|
35
|
+
sn: z.string().optional(),
|
|
36
|
+
sh: z.string().optional(),
|
|
37
|
+
ek: z.string().min(1),
|
|
38
|
+
i: z.string().min(1),
|
|
39
|
+
p: z.string().optional(),
|
|
40
|
+
t: z.string().optional(),
|
|
41
|
+
a: z.string().optional(),
|
|
42
|
+
al: z.string().optional(),
|
|
43
|
+
});
|
|
44
|
+
const WireSchema = z.discriminatedUnion("k", [NodeWireSchema, DocWireSchema, EntityWireSchema]);
|
|
45
|
+
// legacy shapes this codec still decodes, so links handed out before the
|
|
46
|
+
// unified codec existed keep working.
|
|
47
|
+
const DocLegacyV1Schema = z.object({
|
|
48
|
+
v: z.literal(1),
|
|
49
|
+
n: z.string().min(1),
|
|
50
|
+
d: z.string().min(1),
|
|
51
|
+
t: z.string().optional(),
|
|
52
|
+
m: z.enum(["public", "knock"]).optional(),
|
|
53
|
+
});
|
|
54
|
+
const EntityLegacyV1Schema = z.object({
|
|
55
|
+
v: z.literal(1),
|
|
56
|
+
s: z.object({ n: z.string().optional(), h: z.string().optional() }),
|
|
57
|
+
k: z.string().min(1),
|
|
58
|
+
i: z.string().min(1),
|
|
59
|
+
p: z.string().optional(),
|
|
60
|
+
t: z.string().optional(),
|
|
61
|
+
a: z.string().optional(),
|
|
62
|
+
al: z.string().optional(),
|
|
63
|
+
});
|
|
64
|
+
// bare node id + doc id, no version marker at all.
|
|
65
|
+
const DocBareLegacySchema = z.object({
|
|
66
|
+
n: z.string().min(1),
|
|
67
|
+
d: z.string().min(1),
|
|
68
|
+
});
|
|
69
|
+
function toWire(payload) {
|
|
70
|
+
switch (payload.kind) {
|
|
71
|
+
case "node":
|
|
72
|
+
return { v: CURRENT_WIRE_VERSION, k: "node", n: payload.nodeId, t: payload.title };
|
|
73
|
+
case "doc":
|
|
74
|
+
return {
|
|
75
|
+
v: CURRENT_WIRE_VERSION,
|
|
76
|
+
k: "doc",
|
|
77
|
+
n: payload.nodeId,
|
|
78
|
+
d: payload.docId,
|
|
79
|
+
t: payload.title,
|
|
80
|
+
m: payload.mode,
|
|
81
|
+
};
|
|
82
|
+
case "entity":
|
|
83
|
+
return {
|
|
84
|
+
v: CURRENT_WIRE_VERSION,
|
|
85
|
+
k: "entity",
|
|
86
|
+
sn: payload.source.nodeId,
|
|
87
|
+
sh: payload.source.httpOrigin,
|
|
88
|
+
ek: payload.entityKind,
|
|
89
|
+
i: payload.entityId,
|
|
90
|
+
p: payload.parentId,
|
|
91
|
+
t: payload.title,
|
|
92
|
+
a: payload.artist,
|
|
93
|
+
al: payload.album,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function fromWire(wire) {
|
|
98
|
+
switch (wire.k) {
|
|
99
|
+
case "node":
|
|
100
|
+
return { kind: "node", nodeId: wire.n, title: wire.t };
|
|
101
|
+
case "doc":
|
|
102
|
+
return { kind: "doc", nodeId: wire.n, docId: wire.d, title: wire.t, mode: wire.m };
|
|
103
|
+
case "entity":
|
|
104
|
+
return {
|
|
105
|
+
kind: "entity",
|
|
106
|
+
source: { nodeId: wire.sn, httpOrigin: wire.sh },
|
|
107
|
+
entityKind: wire.ek,
|
|
108
|
+
entityId: wire.i,
|
|
109
|
+
parentId: wire.p,
|
|
110
|
+
title: wire.t,
|
|
111
|
+
artist: wire.a,
|
|
112
|
+
album: wire.al,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// base64url helpers built on TextEncoder/TextDecoder rather than raw
|
|
117
|
+
// btoa/atob, so a title containing non-latin1 characters round-trips
|
|
118
|
+
// instead of throwing.
|
|
119
|
+
function base64UrlEncode(bytes) {
|
|
120
|
+
let bin = "";
|
|
121
|
+
for (let i = 0; i < bytes.length; i++)
|
|
122
|
+
bin += String.fromCharCode(bytes[i]);
|
|
123
|
+
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
124
|
+
}
|
|
125
|
+
function base64UrlDecode(token) {
|
|
126
|
+
const pad = token.length % 4 === 0 ? "" : "=".repeat(4 - (token.length % 4));
|
|
127
|
+
const b64 = token.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
128
|
+
const bin = atob(b64);
|
|
129
|
+
const out = new Uint8Array(bin.length);
|
|
130
|
+
for (let i = 0; i < bin.length; i++)
|
|
131
|
+
out[i] = bin.charCodeAt(i);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function stripUndefined(value) {
|
|
135
|
+
const out = {};
|
|
136
|
+
for (const [key, v] of Object.entries(value)) {
|
|
137
|
+
if (v !== undefined)
|
|
138
|
+
out[key] = v;
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
/** encode a share payload as a base64url token (no padding). */
|
|
143
|
+
export function encodeShareToken(payload) {
|
|
144
|
+
const wire = stripUndefined(toWire(payload));
|
|
145
|
+
const json = JSON.stringify(wire);
|
|
146
|
+
return base64UrlEncode(new TextEncoder().encode(json));
|
|
147
|
+
}
|
|
148
|
+
/** build a url fragment for embedding in window.location.hash. */
|
|
149
|
+
export function shareFragment(payload) {
|
|
150
|
+
return `#share/${encodeShareToken(payload)}`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* strip a share url/fragment down to the bare token: handles a full url
|
|
154
|
+
* ending in a `#share/<token>` fragment, a bare `share/<token>` prefix, and
|
|
155
|
+
* a trailing `&`-joined query fragment appended after the token. returns
|
|
156
|
+
* the input unchanged (trimmed) when none of those prefixes are present,
|
|
157
|
+
* so a bare token passes through untouched.
|
|
158
|
+
*/
|
|
159
|
+
export function extractShareToken(input) {
|
|
160
|
+
let raw = input.trim();
|
|
161
|
+
const hashIdx = raw.indexOf("#share/");
|
|
162
|
+
if (hashIdx !== -1) {
|
|
163
|
+
raw = raw.slice(hashIdx + "#share/".length);
|
|
164
|
+
}
|
|
165
|
+
else if (raw.startsWith("share/")) {
|
|
166
|
+
raw = raw.slice("share/".length);
|
|
167
|
+
}
|
|
168
|
+
const ampIdx = raw.indexOf("&");
|
|
169
|
+
if (ampIdx !== -1)
|
|
170
|
+
raw = raw.slice(0, ampIdx);
|
|
171
|
+
return raw;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* decode a share token back to a payload. accepts a raw token, a
|
|
175
|
+
* `#share/<token>` fragment, or a full url ending in one. also decodes the
|
|
176
|
+
* legacy doc-share shapes (versioned `{v:1,n,d,t,m}` and the bare,
|
|
177
|
+
* unversioned `{n,d}` form) and the legacy entity-share shape
|
|
178
|
+
* (`{v:1,s:{n,h},k,i,...}`), mapping every one of them onto the current
|
|
179
|
+
* payload types. returns null on anything invalid rather than throwing, so
|
|
180
|
+
* callers can treat "not a share token" as just another input
|
|
181
|
+
* classification.
|
|
182
|
+
*/
|
|
183
|
+
export function decodeShareToken(input) {
|
|
184
|
+
const raw = extractShareToken(input);
|
|
185
|
+
if (!raw)
|
|
186
|
+
return null;
|
|
187
|
+
let json;
|
|
188
|
+
try {
|
|
189
|
+
json = JSON.parse(new TextDecoder().decode(base64UrlDecode(raw)));
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const wire = WireSchema.safeParse(json);
|
|
195
|
+
if (wire.success)
|
|
196
|
+
return fromWire(wire.data);
|
|
197
|
+
const docLegacy = DocLegacyV1Schema.safeParse(json);
|
|
198
|
+
if (docLegacy.success) {
|
|
199
|
+
const p = docLegacy.data;
|
|
200
|
+
return { kind: "doc", nodeId: p.n, docId: p.d, title: p.t, mode: p.m };
|
|
201
|
+
}
|
|
202
|
+
const entityLegacy = EntityLegacyV1Schema.safeParse(json);
|
|
203
|
+
if (entityLegacy.success) {
|
|
204
|
+
const p = entityLegacy.data;
|
|
205
|
+
return {
|
|
206
|
+
kind: "entity",
|
|
207
|
+
source: { nodeId: p.s.n, httpOrigin: p.s.h },
|
|
208
|
+
entityKind: p.k,
|
|
209
|
+
entityId: p.i,
|
|
210
|
+
parentId: p.p,
|
|
211
|
+
title: p.t,
|
|
212
|
+
artist: p.a,
|
|
213
|
+
album: p.al,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
const docBareLegacy = DocBareLegacySchema.safeParse(json);
|
|
217
|
+
if (docBareLegacy.success) {
|
|
218
|
+
const p = docBareLegacy.data;
|
|
219
|
+
return { kind: "doc", nodeId: p.n, docId: p.d };
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.test.d.ts","sourceRoot":"","sources":["../../src/share/codec.test.ts"],"names":[],"mappings":""}
|