@civiwave/vineyard-id 0.1.1

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/index.cjs ADDED
@@ -0,0 +1,277 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ authenticate: () => authenticate,
34
+ constructMessage: () => constructMessage,
35
+ extractNetworkId: () => extractNetworkId,
36
+ generateChallenge: () => generateChallenge,
37
+ issueToken: () => issueToken,
38
+ requestChallenge: () => requestChallenge,
39
+ signChallenge: () => signChallenge,
40
+ validateToken: () => validateToken,
41
+ validateTokenLite: () => validateTokenLite,
42
+ verifyDIDSignature: () => verifyDIDSignature
43
+ });
44
+ module.exports = __toCommonJS(index_exports);
45
+
46
+ // src/challenge.ts
47
+ var import_crypto = __toESM(require("crypto"), 1);
48
+ var DEFAULT_TTL_SECONDS = 300;
49
+ function generateChallenge(did, ttlSeconds = DEFAULT_TTL_SECONDS) {
50
+ const nonce = import_crypto.default.randomBytes(32).toString("hex");
51
+ const expiresAt = Date.now() + ttlSeconds * 1e3;
52
+ return { nonce, expiresAt, did };
53
+ }
54
+ function extractNetworkId(did) {
55
+ const parts = did.split(":");
56
+ if (parts.length < 4 || parts[0] !== "did" || parts[1] !== "vineyard") {
57
+ throw new Error(`Invalid did:vineyard format: ${did}`);
58
+ }
59
+ return parts[2];
60
+ }
61
+ function constructMessage(nonce, timestamp, origin, network = "0") {
62
+ return JSON.stringify({
63
+ domain: "civiwave:did:auth-challenge",
64
+ network,
65
+ nonce,
66
+ timestamp,
67
+ origin
68
+ });
69
+ }
70
+
71
+ // src/verify.ts
72
+ var import_util_crypto = require("@polkadot/util-crypto");
73
+ var import_util = require("@polkadot/util");
74
+ var DEFAULT_RESOLVER_ENDPOINT = "http://localhost:8080";
75
+ function decodePublicKeyMultibase(multibase) {
76
+ if (!multibase.startsWith("z")) {
77
+ throw new Error("Only base58btc (z-prefix) multibase is supported");
78
+ }
79
+ const encoded = multibase.slice(1);
80
+ const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
81
+ let num = BigInt(0);
82
+ for (const char of encoded) {
83
+ const idx = alphabet.indexOf(char);
84
+ if (idx === -1) throw new Error(`Invalid base58 character: ${char}`);
85
+ num = num * BigInt(58) + BigInt(idx);
86
+ }
87
+ const hex = num.toString(16).padStart(2, "0");
88
+ const bytes = new Uint8Array(
89
+ (hex.length % 2 === 0 ? hex : "0" + hex).match(/.{2}/g).map((b) => parseInt(b, 16))
90
+ );
91
+ const rawKey = bytes.slice(2);
92
+ return (0, import_util.u8aToHex)(rawKey);
93
+ }
94
+ async function resolveViaHttp(did, resolverEndpoint) {
95
+ const url = `${resolverEndpoint}/1.0/identifiers/${encodeURIComponent(did)}`;
96
+ const response = await fetch(url, {
97
+ headers: { Accept: "application/did+ld+json" }
98
+ });
99
+ if (!response.ok) {
100
+ return {
101
+ didDocument: null,
102
+ didResolutionMetadata: { error: "notFound" }
103
+ };
104
+ }
105
+ return await response.json();
106
+ }
107
+ function extractPublicKey(didDocument) {
108
+ if (!didDocument?.verificationMethod?.length) {
109
+ return null;
110
+ }
111
+ for (const vm of didDocument.verificationMethod) {
112
+ if (vm.publicKeyMultibase) {
113
+ const publicKeyHex = decodePublicKeyMultibase(vm.publicKeyMultibase);
114
+ return { publicKeyHex, keyType: vm.type };
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+ async function verifyDIDSignature(request, resolverEndpoint = DEFAULT_RESOLVER_ENDPOINT) {
120
+ try {
121
+ const resolution = await resolveViaHttp(request.did, resolverEndpoint);
122
+ if (resolution.didResolutionMetadata.error || !resolution.didDocument) {
123
+ return { valid: false };
124
+ }
125
+ const keyInfo = extractPublicKey(resolution.didDocument);
126
+ if (!keyInfo) {
127
+ return { valid: false };
128
+ }
129
+ const network = extractNetworkId(request.did);
130
+ const message = constructMessage(
131
+ request.nonce,
132
+ request.timestamp,
133
+ request.origin,
134
+ network
135
+ );
136
+ const messageU8a = (0, import_util.stringToU8a)(message);
137
+ const result = (0, import_util_crypto.signatureVerify)(
138
+ messageU8a,
139
+ request.signature,
140
+ keyInfo.publicKeyHex
141
+ );
142
+ return {
143
+ valid: result.isValid,
144
+ publicKey: keyInfo.publicKeyHex,
145
+ keyType: keyInfo.keyType
146
+ };
147
+ } catch (error) {
148
+ console.error("[did-auth] Signature verification error:", error);
149
+ return { valid: false };
150
+ }
151
+ }
152
+
153
+ // src/session.ts
154
+ var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
155
+ var DEFAULT_EXPIRES_IN = "7d";
156
+ function issueToken(did, ss58, tier, secret, expiresIn = DEFAULT_EXPIRES_IN) {
157
+ return import_jsonwebtoken.default.sign(
158
+ {
159
+ sub: did,
160
+ ss58,
161
+ tier
162
+ },
163
+ secret,
164
+ { expiresIn }
165
+ );
166
+ }
167
+ function validateToken(token, secret) {
168
+ try {
169
+ const payload = import_jsonwebtoken.default.verify(token, secret);
170
+ if (!payload.sub || !payload.ss58) {
171
+ return null;
172
+ }
173
+ return {
174
+ did: payload.sub,
175
+ ss58: payload.ss58,
176
+ tier: payload.tier || "member",
177
+ iat: payload.iat ?? 0,
178
+ exp: payload.exp ?? 0
179
+ };
180
+ } catch {
181
+ return null;
182
+ }
183
+ }
184
+ function validateTokenLite(token) {
185
+ try {
186
+ const parts = token.split(".");
187
+ if (parts.length !== 3) return null;
188
+ const payload = JSON.parse(
189
+ Buffer.from(parts[1], "base64url").toString("utf-8")
190
+ );
191
+ if (!payload.sub || !payload.ss58) return null;
192
+ if (payload.exp && payload.exp * 1e3 < Date.now()) return null;
193
+ return {
194
+ did: payload.sub,
195
+ ss58: payload.ss58,
196
+ tier: payload.tier || "member",
197
+ iat: payload.iat ?? 0,
198
+ exp: payload.exp ?? 0
199
+ };
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+
205
+ // src/client.ts
206
+ async function requestChallenge(serverUrl, did) {
207
+ const response = await fetch(`${serverUrl}/auth/did/challenge`, {
208
+ method: "POST",
209
+ headers: { "Content-Type": "application/json" },
210
+ body: JSON.stringify({ did })
211
+ });
212
+ if (!response.ok) {
213
+ const error = await response.json().catch(() => ({ error: "Challenge request failed" }));
214
+ throw new Error(error.error || "Challenge request failed");
215
+ }
216
+ const data = await response.json();
217
+ return {
218
+ nonce: data.nonce,
219
+ expiresAt: data.expiresAt,
220
+ did
221
+ };
222
+ }
223
+ async function signChallenge(nonce, timestamp, origin, signer, address, network = "0") {
224
+ if (!signer.signRaw) {
225
+ throw new Error("Signer does not support signRaw");
226
+ }
227
+ const message = constructMessage(nonce, timestamp, origin, network);
228
+ const result = await signer.signRaw({
229
+ address,
230
+ data: message,
231
+ type: "bytes"
232
+ });
233
+ return { signature: result.signature };
234
+ }
235
+ async function authenticate(serverUrl, did, signer, address) {
236
+ const challenge = await requestChallenge(serverUrl, did);
237
+ const timestamp = Date.now();
238
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
239
+ const network = extractNetworkId(did);
240
+ const { signature } = await signChallenge(
241
+ challenge.nonce,
242
+ timestamp,
243
+ origin,
244
+ signer,
245
+ address,
246
+ network
247
+ );
248
+ const response = await fetch(`${serverUrl}/auth/did/verify`, {
249
+ method: "POST",
250
+ headers: { "Content-Type": "application/json" },
251
+ body: JSON.stringify({
252
+ did,
253
+ nonce: challenge.nonce,
254
+ signature,
255
+ timestamp,
256
+ origin
257
+ })
258
+ });
259
+ if (!response.ok) {
260
+ const error = await response.json().catch(() => ({ error: "Verification failed" }));
261
+ throw new Error(error.error || "DID authentication failed");
262
+ }
263
+ return await response.json();
264
+ }
265
+ // Annotate the CommonJS export names for ESM import in node:
266
+ 0 && (module.exports = {
267
+ authenticate,
268
+ constructMessage,
269
+ extractNetworkId,
270
+ generateChallenge,
271
+ issueToken,
272
+ requestChallenge,
273
+ signChallenge,
274
+ validateToken,
275
+ validateTokenLite,
276
+ verifyDIDSignature
277
+ });
@@ -0,0 +1,208 @@
1
+ /**
2
+ * DID Authentication types for the sign-in-with-DID protocol.
3
+ */
4
+ /** A challenge issued to a DID holder for authentication. */
5
+ interface DIDChallenge {
6
+ nonce: string;
7
+ expiresAt: number;
8
+ did: string;
9
+ }
10
+ /** A signed authentication request from the client. */
11
+ interface DIDAuthRequest {
12
+ did: string;
13
+ nonce: string;
14
+ signature: string;
15
+ timestamp: number;
16
+ origin: string;
17
+ }
18
+ /** A validated DID session stored in a JWT. */
19
+ interface DIDSession {
20
+ did: string;
21
+ ss58: string;
22
+ tier: string;
23
+ iat: number;
24
+ exp: number;
25
+ }
26
+ /** Result of DID signature verification. */
27
+ interface DIDVerifyResult {
28
+ valid: boolean;
29
+ publicKey?: string;
30
+ keyType?: string;
31
+ }
32
+ /** Minimal DID Document shape needed for verification. */
33
+ interface DIDDocumentLike {
34
+ id: string;
35
+ verificationMethod?: Array<{
36
+ id: string;
37
+ type: string;
38
+ controller: string;
39
+ publicKeyMultibase?: string;
40
+ }>;
41
+ }
42
+ /** DID resolution result shape (subset of DIF Universal Resolver). */
43
+ interface DIDResolutionResultLike {
44
+ didDocument: DIDDocumentLike | null;
45
+ didResolutionMetadata: {
46
+ error?: string;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Challenge generation and message construction for DID auth.
52
+ */
53
+
54
+ /**
55
+ * Generate a cryptographic challenge for DID authentication.
56
+ *
57
+ * @param did - The DID requesting authentication
58
+ * @param ttlSeconds - Challenge time-to-live in seconds (default: 300)
59
+ * @returns A DIDChallenge with a random nonce and expiration timestamp
60
+ */
61
+ declare function generateChallenge(did: string, ttlSeconds?: number): DIDChallenge;
62
+ /**
63
+ * Extract the network ID from a did:vineyard DID.
64
+ * Format: did:vineyard:<networkId>:<ss58Address>
65
+ *
66
+ * @param did - A did:vineyard DID string
67
+ * @returns The network ID string (e.g., "0" for devnet, "1" for mainnet)
68
+ */
69
+ declare function extractNetworkId(did: string): string;
70
+ /**
71
+ * Construct the canonical message to be signed by the wallet.
72
+ *
73
+ * The message is a JSON string with domain separation to prevent
74
+ * signature replay across different protocols and chains. The network
75
+ * field binds the signature to a specific chain (devnet/testnet/mainnet),
76
+ * preventing cross-chain replay attacks.
77
+ *
78
+ * @param nonce - The challenge nonce
79
+ * @param timestamp - Client-provided timestamp
80
+ * @param origin - The requesting origin (e.g., "https://axis.civiwave.network")
81
+ * @param network - Chain network ID from the DID (e.g., "0" for devnet)
82
+ * @returns The canonical message string to be signed
83
+ */
84
+ declare function constructMessage(nonce: string, timestamp: number, origin: string, network?: string): string;
85
+
86
+ /**
87
+ * DID signature verification for authentication.
88
+ *
89
+ * Resolves the DID to obtain the public key, then verifies the signature
90
+ * against the constructed message using @polkadot/util-crypto.
91
+ */
92
+
93
+ /**
94
+ * Verify a DID authentication signature.
95
+ *
96
+ * 1. Resolves the DID to obtain the public key
97
+ * 2. Constructs the canonical message from the request parameters
98
+ * 3. Verifies the signature using @polkadot/util-crypto signatureVerify
99
+ *
100
+ * @param request - The authentication request containing DID, nonce, signature, etc.
101
+ * @param resolverEndpoint - Optional HTTP resolver endpoint URL
102
+ * @returns Verification result with validity flag and key info
103
+ */
104
+ declare function verifyDIDSignature(request: DIDAuthRequest, resolverEndpoint?: string): Promise<DIDVerifyResult>;
105
+
106
+ /**
107
+ * JWT session management for DID authentication.
108
+ *
109
+ * Issues and validates JWTs containing DID session information.
110
+ */
111
+
112
+ /**
113
+ * Issue a JWT token for an authenticated DID session.
114
+ *
115
+ * @param did - The authenticated DID (e.g., "did:vineyard:0:<ss58>")
116
+ * @param ss58 - The SS58 address extracted from the DID
117
+ * @param tier - The user's tier/role (e.g., "member", "validator")
118
+ * @param secret - JWT signing secret (from DID_AUTH_SECRET env var)
119
+ * @param expiresIn - JWT expiration (default: "7d")
120
+ * @returns Signed JWT string
121
+ */
122
+ declare function issueToken(did: string, ss58: string, tier: string, secret: string, expiresIn?: string): string;
123
+ /**
124
+ * Validate a DID JWT token and extract the session payload.
125
+ *
126
+ * @param token - The JWT string to validate
127
+ * @param secret - JWT signing secret (must match issuing secret)
128
+ * @returns The DIDSession if valid, or null if invalid/expired
129
+ */
130
+ declare function validateToken(token: string, secret: string): DIDSession | null;
131
+ /**
132
+ * Lightweight JWT validation without full jsonwebtoken dependency.
133
+ * Suitable for Edge runtime (Next.js middleware) where jsonwebtoken isn't available.
134
+ *
135
+ * WARNING: This only validates structure and expiration, NOT the signature.
136
+ * Use validateToken() for full server-side validation.
137
+ *
138
+ * @param token - The JWT string
139
+ * @returns Parsed session or null
140
+ */
141
+ declare function validateTokenLite(token: string): DIDSession | null;
142
+
143
+ /**
144
+ * Client-side DID authentication helpers.
145
+ *
146
+ * Used in browser contexts to perform the challenge-response flow
147
+ * with a Polkadot wallet extension signer.
148
+ */
149
+
150
+ /**
151
+ * Request a challenge from the server for DID authentication.
152
+ *
153
+ * @param serverUrl - The base server URL (e.g., "/api" or "http://localhost:3001/api")
154
+ * @param did - The DID to authenticate
155
+ * @returns The challenge containing nonce and expiration
156
+ */
157
+ declare function requestChallenge(serverUrl: string, did: string): Promise<DIDChallenge>;
158
+ /**
159
+ * Sign a challenge using a Polkadot wallet extension signer.
160
+ *
161
+ * @param nonce - The challenge nonce from the server
162
+ * @param timestamp - Current timestamp
163
+ * @param origin - The current page origin
164
+ * @param signer - The signer from @polkadot/extension-dapp web3FromSource()
165
+ * @param address - The SS58 address to sign with
166
+ * @returns The hex-encoded signature
167
+ */
168
+ declare function signChallenge(nonce: string, timestamp: number, origin: string, signer: {
169
+ signRaw?: (payload: {
170
+ address: string;
171
+ data: string;
172
+ type: string;
173
+ }) => Promise<{
174
+ signature: string;
175
+ }>;
176
+ }, address: string, network?: string): Promise<{
177
+ signature: string;
178
+ }>;
179
+ /**
180
+ * Perform the full DID authentication flow.
181
+ *
182
+ * 1. Request challenge from server
183
+ * 2. Sign challenge with wallet
184
+ * 3. Submit signed challenge for verification
185
+ * 4. Return the JWT token on success
186
+ *
187
+ * @param serverUrl - Base server URL for API calls
188
+ * @param did - The DID to authenticate
189
+ * @param signer - Polkadot wallet signer
190
+ * @param address - SS58 address for signing
191
+ * @returns Authentication result with token and session info
192
+ */
193
+ declare function authenticate(serverUrl: string, did: string, signer: {
194
+ signRaw?: (payload: {
195
+ address: string;
196
+ data: string;
197
+ type: string;
198
+ }) => Promise<{
199
+ signature: string;
200
+ }>;
201
+ }, address: string): Promise<{
202
+ token: string;
203
+ did: string;
204
+ ss58: string;
205
+ tier: string;
206
+ }>;
207
+
208
+ export { type DIDAuthRequest, type DIDChallenge, type DIDDocumentLike, type DIDResolutionResultLike, type DIDSession, type DIDVerifyResult, authenticate, constructMessage, extractNetworkId, generateChallenge, issueToken, requestChallenge, signChallenge, validateToken, validateTokenLite, verifyDIDSignature };
@@ -0,0 +1,208 @@
1
+ /**
2
+ * DID Authentication types for the sign-in-with-DID protocol.
3
+ */
4
+ /** A challenge issued to a DID holder for authentication. */
5
+ interface DIDChallenge {
6
+ nonce: string;
7
+ expiresAt: number;
8
+ did: string;
9
+ }
10
+ /** A signed authentication request from the client. */
11
+ interface DIDAuthRequest {
12
+ did: string;
13
+ nonce: string;
14
+ signature: string;
15
+ timestamp: number;
16
+ origin: string;
17
+ }
18
+ /** A validated DID session stored in a JWT. */
19
+ interface DIDSession {
20
+ did: string;
21
+ ss58: string;
22
+ tier: string;
23
+ iat: number;
24
+ exp: number;
25
+ }
26
+ /** Result of DID signature verification. */
27
+ interface DIDVerifyResult {
28
+ valid: boolean;
29
+ publicKey?: string;
30
+ keyType?: string;
31
+ }
32
+ /** Minimal DID Document shape needed for verification. */
33
+ interface DIDDocumentLike {
34
+ id: string;
35
+ verificationMethod?: Array<{
36
+ id: string;
37
+ type: string;
38
+ controller: string;
39
+ publicKeyMultibase?: string;
40
+ }>;
41
+ }
42
+ /** DID resolution result shape (subset of DIF Universal Resolver). */
43
+ interface DIDResolutionResultLike {
44
+ didDocument: DIDDocumentLike | null;
45
+ didResolutionMetadata: {
46
+ error?: string;
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Challenge generation and message construction for DID auth.
52
+ */
53
+
54
+ /**
55
+ * Generate a cryptographic challenge for DID authentication.
56
+ *
57
+ * @param did - The DID requesting authentication
58
+ * @param ttlSeconds - Challenge time-to-live in seconds (default: 300)
59
+ * @returns A DIDChallenge with a random nonce and expiration timestamp
60
+ */
61
+ declare function generateChallenge(did: string, ttlSeconds?: number): DIDChallenge;
62
+ /**
63
+ * Extract the network ID from a did:vineyard DID.
64
+ * Format: did:vineyard:<networkId>:<ss58Address>
65
+ *
66
+ * @param did - A did:vineyard DID string
67
+ * @returns The network ID string (e.g., "0" for devnet, "1" for mainnet)
68
+ */
69
+ declare function extractNetworkId(did: string): string;
70
+ /**
71
+ * Construct the canonical message to be signed by the wallet.
72
+ *
73
+ * The message is a JSON string with domain separation to prevent
74
+ * signature replay across different protocols and chains. The network
75
+ * field binds the signature to a specific chain (devnet/testnet/mainnet),
76
+ * preventing cross-chain replay attacks.
77
+ *
78
+ * @param nonce - The challenge nonce
79
+ * @param timestamp - Client-provided timestamp
80
+ * @param origin - The requesting origin (e.g., "https://axis.civiwave.network")
81
+ * @param network - Chain network ID from the DID (e.g., "0" for devnet)
82
+ * @returns The canonical message string to be signed
83
+ */
84
+ declare function constructMessage(nonce: string, timestamp: number, origin: string, network?: string): string;
85
+
86
+ /**
87
+ * DID signature verification for authentication.
88
+ *
89
+ * Resolves the DID to obtain the public key, then verifies the signature
90
+ * against the constructed message using @polkadot/util-crypto.
91
+ */
92
+
93
+ /**
94
+ * Verify a DID authentication signature.
95
+ *
96
+ * 1. Resolves the DID to obtain the public key
97
+ * 2. Constructs the canonical message from the request parameters
98
+ * 3. Verifies the signature using @polkadot/util-crypto signatureVerify
99
+ *
100
+ * @param request - The authentication request containing DID, nonce, signature, etc.
101
+ * @param resolverEndpoint - Optional HTTP resolver endpoint URL
102
+ * @returns Verification result with validity flag and key info
103
+ */
104
+ declare function verifyDIDSignature(request: DIDAuthRequest, resolverEndpoint?: string): Promise<DIDVerifyResult>;
105
+
106
+ /**
107
+ * JWT session management for DID authentication.
108
+ *
109
+ * Issues and validates JWTs containing DID session information.
110
+ */
111
+
112
+ /**
113
+ * Issue a JWT token for an authenticated DID session.
114
+ *
115
+ * @param did - The authenticated DID (e.g., "did:vineyard:0:<ss58>")
116
+ * @param ss58 - The SS58 address extracted from the DID
117
+ * @param tier - The user's tier/role (e.g., "member", "validator")
118
+ * @param secret - JWT signing secret (from DID_AUTH_SECRET env var)
119
+ * @param expiresIn - JWT expiration (default: "7d")
120
+ * @returns Signed JWT string
121
+ */
122
+ declare function issueToken(did: string, ss58: string, tier: string, secret: string, expiresIn?: string): string;
123
+ /**
124
+ * Validate a DID JWT token and extract the session payload.
125
+ *
126
+ * @param token - The JWT string to validate
127
+ * @param secret - JWT signing secret (must match issuing secret)
128
+ * @returns The DIDSession if valid, or null if invalid/expired
129
+ */
130
+ declare function validateToken(token: string, secret: string): DIDSession | null;
131
+ /**
132
+ * Lightweight JWT validation without full jsonwebtoken dependency.
133
+ * Suitable for Edge runtime (Next.js middleware) where jsonwebtoken isn't available.
134
+ *
135
+ * WARNING: This only validates structure and expiration, NOT the signature.
136
+ * Use validateToken() for full server-side validation.
137
+ *
138
+ * @param token - The JWT string
139
+ * @returns Parsed session or null
140
+ */
141
+ declare function validateTokenLite(token: string): DIDSession | null;
142
+
143
+ /**
144
+ * Client-side DID authentication helpers.
145
+ *
146
+ * Used in browser contexts to perform the challenge-response flow
147
+ * with a Polkadot wallet extension signer.
148
+ */
149
+
150
+ /**
151
+ * Request a challenge from the server for DID authentication.
152
+ *
153
+ * @param serverUrl - The base server URL (e.g., "/api" or "http://localhost:3001/api")
154
+ * @param did - The DID to authenticate
155
+ * @returns The challenge containing nonce and expiration
156
+ */
157
+ declare function requestChallenge(serverUrl: string, did: string): Promise<DIDChallenge>;
158
+ /**
159
+ * Sign a challenge using a Polkadot wallet extension signer.
160
+ *
161
+ * @param nonce - The challenge nonce from the server
162
+ * @param timestamp - Current timestamp
163
+ * @param origin - The current page origin
164
+ * @param signer - The signer from @polkadot/extension-dapp web3FromSource()
165
+ * @param address - The SS58 address to sign with
166
+ * @returns The hex-encoded signature
167
+ */
168
+ declare function signChallenge(nonce: string, timestamp: number, origin: string, signer: {
169
+ signRaw?: (payload: {
170
+ address: string;
171
+ data: string;
172
+ type: string;
173
+ }) => Promise<{
174
+ signature: string;
175
+ }>;
176
+ }, address: string, network?: string): Promise<{
177
+ signature: string;
178
+ }>;
179
+ /**
180
+ * Perform the full DID authentication flow.
181
+ *
182
+ * 1. Request challenge from server
183
+ * 2. Sign challenge with wallet
184
+ * 3. Submit signed challenge for verification
185
+ * 4. Return the JWT token on success
186
+ *
187
+ * @param serverUrl - Base server URL for API calls
188
+ * @param did - The DID to authenticate
189
+ * @param signer - Polkadot wallet signer
190
+ * @param address - SS58 address for signing
191
+ * @returns Authentication result with token and session info
192
+ */
193
+ declare function authenticate(serverUrl: string, did: string, signer: {
194
+ signRaw?: (payload: {
195
+ address: string;
196
+ data: string;
197
+ type: string;
198
+ }) => Promise<{
199
+ signature: string;
200
+ }>;
201
+ }, address: string): Promise<{
202
+ token: string;
203
+ did: string;
204
+ ss58: string;
205
+ tier: string;
206
+ }>;
207
+
208
+ export { type DIDAuthRequest, type DIDChallenge, type DIDDocumentLike, type DIDResolutionResultLike, type DIDSession, type DIDVerifyResult, authenticate, constructMessage, extractNetworkId, generateChallenge, issueToken, requestChallenge, signChallenge, validateToken, validateTokenLite, verifyDIDSignature };
package/dist/index.js ADDED
@@ -0,0 +1,231 @@
1
+ // src/challenge.ts
2
+ import crypto from "crypto";
3
+ var DEFAULT_TTL_SECONDS = 300;
4
+ function generateChallenge(did, ttlSeconds = DEFAULT_TTL_SECONDS) {
5
+ const nonce = crypto.randomBytes(32).toString("hex");
6
+ const expiresAt = Date.now() + ttlSeconds * 1e3;
7
+ return { nonce, expiresAt, did };
8
+ }
9
+ function extractNetworkId(did) {
10
+ const parts = did.split(":");
11
+ if (parts.length < 4 || parts[0] !== "did" || parts[1] !== "vineyard") {
12
+ throw new Error(`Invalid did:vineyard format: ${did}`);
13
+ }
14
+ return parts[2];
15
+ }
16
+ function constructMessage(nonce, timestamp, origin, network = "0") {
17
+ return JSON.stringify({
18
+ domain: "civiwave:did:auth-challenge",
19
+ network,
20
+ nonce,
21
+ timestamp,
22
+ origin
23
+ });
24
+ }
25
+
26
+ // src/verify.ts
27
+ import { signatureVerify } from "@polkadot/util-crypto";
28
+ import { u8aToHex, stringToU8a } from "@polkadot/util";
29
+ var DEFAULT_RESOLVER_ENDPOINT = "http://localhost:8080";
30
+ function decodePublicKeyMultibase(multibase) {
31
+ if (!multibase.startsWith("z")) {
32
+ throw new Error("Only base58btc (z-prefix) multibase is supported");
33
+ }
34
+ const encoded = multibase.slice(1);
35
+ const alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
36
+ let num = BigInt(0);
37
+ for (const char of encoded) {
38
+ const idx = alphabet.indexOf(char);
39
+ if (idx === -1) throw new Error(`Invalid base58 character: ${char}`);
40
+ num = num * BigInt(58) + BigInt(idx);
41
+ }
42
+ const hex = num.toString(16).padStart(2, "0");
43
+ const bytes = new Uint8Array(
44
+ (hex.length % 2 === 0 ? hex : "0" + hex).match(/.{2}/g).map((b) => parseInt(b, 16))
45
+ );
46
+ const rawKey = bytes.slice(2);
47
+ return u8aToHex(rawKey);
48
+ }
49
+ async function resolveViaHttp(did, resolverEndpoint) {
50
+ const url = `${resolverEndpoint}/1.0/identifiers/${encodeURIComponent(did)}`;
51
+ const response = await fetch(url, {
52
+ headers: { Accept: "application/did+ld+json" }
53
+ });
54
+ if (!response.ok) {
55
+ return {
56
+ didDocument: null,
57
+ didResolutionMetadata: { error: "notFound" }
58
+ };
59
+ }
60
+ return await response.json();
61
+ }
62
+ function extractPublicKey(didDocument) {
63
+ if (!didDocument?.verificationMethod?.length) {
64
+ return null;
65
+ }
66
+ for (const vm of didDocument.verificationMethod) {
67
+ if (vm.publicKeyMultibase) {
68
+ const publicKeyHex = decodePublicKeyMultibase(vm.publicKeyMultibase);
69
+ return { publicKeyHex, keyType: vm.type };
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+ async function verifyDIDSignature(request, resolverEndpoint = DEFAULT_RESOLVER_ENDPOINT) {
75
+ try {
76
+ const resolution = await resolveViaHttp(request.did, resolverEndpoint);
77
+ if (resolution.didResolutionMetadata.error || !resolution.didDocument) {
78
+ return { valid: false };
79
+ }
80
+ const keyInfo = extractPublicKey(resolution.didDocument);
81
+ if (!keyInfo) {
82
+ return { valid: false };
83
+ }
84
+ const network = extractNetworkId(request.did);
85
+ const message = constructMessage(
86
+ request.nonce,
87
+ request.timestamp,
88
+ request.origin,
89
+ network
90
+ );
91
+ const messageU8a = stringToU8a(message);
92
+ const result = signatureVerify(
93
+ messageU8a,
94
+ request.signature,
95
+ keyInfo.publicKeyHex
96
+ );
97
+ return {
98
+ valid: result.isValid,
99
+ publicKey: keyInfo.publicKeyHex,
100
+ keyType: keyInfo.keyType
101
+ };
102
+ } catch (error) {
103
+ console.error("[did-auth] Signature verification error:", error);
104
+ return { valid: false };
105
+ }
106
+ }
107
+
108
+ // src/session.ts
109
+ import jwt from "jsonwebtoken";
110
+ var DEFAULT_EXPIRES_IN = "7d";
111
+ function issueToken(did, ss58, tier, secret, expiresIn = DEFAULT_EXPIRES_IN) {
112
+ return jwt.sign(
113
+ {
114
+ sub: did,
115
+ ss58,
116
+ tier
117
+ },
118
+ secret,
119
+ { expiresIn }
120
+ );
121
+ }
122
+ function validateToken(token, secret) {
123
+ try {
124
+ const payload = jwt.verify(token, secret);
125
+ if (!payload.sub || !payload.ss58) {
126
+ return null;
127
+ }
128
+ return {
129
+ did: payload.sub,
130
+ ss58: payload.ss58,
131
+ tier: payload.tier || "member",
132
+ iat: payload.iat ?? 0,
133
+ exp: payload.exp ?? 0
134
+ };
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+ function validateTokenLite(token) {
140
+ try {
141
+ const parts = token.split(".");
142
+ if (parts.length !== 3) return null;
143
+ const payload = JSON.parse(
144
+ Buffer.from(parts[1], "base64url").toString("utf-8")
145
+ );
146
+ if (!payload.sub || !payload.ss58) return null;
147
+ if (payload.exp && payload.exp * 1e3 < Date.now()) return null;
148
+ return {
149
+ did: payload.sub,
150
+ ss58: payload.ss58,
151
+ tier: payload.tier || "member",
152
+ iat: payload.iat ?? 0,
153
+ exp: payload.exp ?? 0
154
+ };
155
+ } catch {
156
+ return null;
157
+ }
158
+ }
159
+
160
+ // src/client.ts
161
+ async function requestChallenge(serverUrl, did) {
162
+ const response = await fetch(`${serverUrl}/auth/did/challenge`, {
163
+ method: "POST",
164
+ headers: { "Content-Type": "application/json" },
165
+ body: JSON.stringify({ did })
166
+ });
167
+ if (!response.ok) {
168
+ const error = await response.json().catch(() => ({ error: "Challenge request failed" }));
169
+ throw new Error(error.error || "Challenge request failed");
170
+ }
171
+ const data = await response.json();
172
+ return {
173
+ nonce: data.nonce,
174
+ expiresAt: data.expiresAt,
175
+ did
176
+ };
177
+ }
178
+ async function signChallenge(nonce, timestamp, origin, signer, address, network = "0") {
179
+ if (!signer.signRaw) {
180
+ throw new Error("Signer does not support signRaw");
181
+ }
182
+ const message = constructMessage(nonce, timestamp, origin, network);
183
+ const result = await signer.signRaw({
184
+ address,
185
+ data: message,
186
+ type: "bytes"
187
+ });
188
+ return { signature: result.signature };
189
+ }
190
+ async function authenticate(serverUrl, did, signer, address) {
191
+ const challenge = await requestChallenge(serverUrl, did);
192
+ const timestamp = Date.now();
193
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
194
+ const network = extractNetworkId(did);
195
+ const { signature } = await signChallenge(
196
+ challenge.nonce,
197
+ timestamp,
198
+ origin,
199
+ signer,
200
+ address,
201
+ network
202
+ );
203
+ const response = await fetch(`${serverUrl}/auth/did/verify`, {
204
+ method: "POST",
205
+ headers: { "Content-Type": "application/json" },
206
+ body: JSON.stringify({
207
+ did,
208
+ nonce: challenge.nonce,
209
+ signature,
210
+ timestamp,
211
+ origin
212
+ })
213
+ });
214
+ if (!response.ok) {
215
+ const error = await response.json().catch(() => ({ error: "Verification failed" }));
216
+ throw new Error(error.error || "DID authentication failed");
217
+ }
218
+ return await response.json();
219
+ }
220
+ export {
221
+ authenticate,
222
+ constructMessage,
223
+ extractNetworkId,
224
+ generateChallenge,
225
+ issueToken,
226
+ requestChallenge,
227
+ signChallenge,
228
+ validateToken,
229
+ validateTokenLite,
230
+ verifyDIDSignature
231
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@civiwave/vineyard-id",
3
+ "description": "Sovereign Identity SDK for Civiwave — DID authentication, resolution, and verifiable credential verification against the civiwave identity-hub",
4
+ "version": "0.1.1",
5
+ "type": "module",
6
+ "main": "dist/index.cjs",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "echo \"No tests yet\""
20
+ },
21
+ "dependencies": {
22
+ "@polkadot/util": "^14.0.1",
23
+ "@polkadot/util-crypto": "^14.0.1",
24
+ "jsonwebtoken": "^9.0.2"
25
+ },
26
+ "devDependencies": {
27
+ "@types/jsonwebtoken": "9.0.5",
28
+ "@types/node": "^20.11.30",
29
+ "tsup": "^8.5.1",
30
+ "typescript": "^5.4.5"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ]
35
+ }