@arcblock/did-connect-js 4.0.1-beta.6 → 4.0.2
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 +2472 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/schema/index.cjs +413 -0
- package/dist/storage/kv.cjs +74 -0
- package/package.json +13 -9
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2472 @@
|
|
|
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
|
+
CloudflareKVStorage: () => kv_default,
|
|
34
|
+
VERSION: () => import_did_connect_core2.VERSION,
|
|
35
|
+
WalletAuthenticator: () => wallet_default,
|
|
36
|
+
WalletHandlers: () => wallet_default2,
|
|
37
|
+
attachExpress: () => attachExpress,
|
|
38
|
+
attachHono: () => attachHono,
|
|
39
|
+
createHonoRequest: () => createHonoRequest,
|
|
40
|
+
createHonoResponse: () => createHonoResponse,
|
|
41
|
+
isExpressApp: () => isExpressApp,
|
|
42
|
+
isHonoApp: () => isHonoApp,
|
|
43
|
+
parseWalletUA: () => parseWalletUA,
|
|
44
|
+
prepareBaseUrl: () => prepareBaseUrl
|
|
45
|
+
});
|
|
46
|
+
module.exports = __toCommonJS(index_exports);
|
|
47
|
+
|
|
48
|
+
// src/authenticator/wallet.ts
|
|
49
|
+
var import_node_querystring = __toESM(require("node:querystring"), 1);
|
|
50
|
+
var import_did = require("@arcblock/did");
|
|
51
|
+
var import_rsa = __toESM(require("@ocap/mcrypto/lib/crypter/rsa"), 1);
|
|
52
|
+
var import_util = require("@ocap/util");
|
|
53
|
+
var import_wallet2 = require("@ocap/wallet");
|
|
54
|
+
var import_debug2 = __toESM(require("debug"), 1);
|
|
55
|
+
|
|
56
|
+
// src/utils/helpers.ts
|
|
57
|
+
function pick(obj, keys) {
|
|
58
|
+
const result = {};
|
|
59
|
+
for (const k of keys) {
|
|
60
|
+
if (k in obj) result[k] = obj[k];
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
function omit(obj, keys) {
|
|
65
|
+
const result = {};
|
|
66
|
+
for (const k of Object.keys(obj)) {
|
|
67
|
+
if (!keys.includes(k)) result[k] = obj[k];
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function isEqual(a, b) {
|
|
72
|
+
if (a === b) return true;
|
|
73
|
+
if (a == null || b == null) return false;
|
|
74
|
+
if (typeof a !== typeof b) return false;
|
|
75
|
+
if (Array.isArray(a)) {
|
|
76
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
77
|
+
return a.every((v3, i) => isEqual(v3, b[i]));
|
|
78
|
+
}
|
|
79
|
+
if (typeof a === "object") {
|
|
80
|
+
const ka = Object.keys(a);
|
|
81
|
+
const kb = Object.keys(b);
|
|
82
|
+
if (ka.length !== kb.length) return false;
|
|
83
|
+
return ka.every((k) => isEqual(a[k], b[k]));
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
function isPlainObject(value) {
|
|
88
|
+
if (value == null || typeof value !== "object") return false;
|
|
89
|
+
const proto = Object.getPrototypeOf(value);
|
|
90
|
+
return proto === Object.prototype || proto === null;
|
|
91
|
+
}
|
|
92
|
+
function random(min, max) {
|
|
93
|
+
return min + Math.floor(Math.random() * (max - min + 1));
|
|
94
|
+
}
|
|
95
|
+
function cloneDeep(value) {
|
|
96
|
+
if (value === null || typeof value !== "object") return value;
|
|
97
|
+
if (Array.isArray(value)) return value.map((item) => cloneDeep(item));
|
|
98
|
+
const result = {};
|
|
99
|
+
for (const key of Object.keys(value)) {
|
|
100
|
+
result[key] = cloneDeep(value[key]);
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
function shuffle(arr) {
|
|
105
|
+
const a = [...arr];
|
|
106
|
+
for (let i = a.length - 1; i > 0; i--) {
|
|
107
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
108
|
+
[a[i], a[j]] = [a[j], a[i]];
|
|
109
|
+
}
|
|
110
|
+
return a;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/authenticator/base.ts
|
|
114
|
+
var Jwt = __toESM(require("@arcblock/jwt"), 1);
|
|
115
|
+
var import_wallet = require("@ocap/wallet");
|
|
116
|
+
var import_debug = __toESM(require("debug"), 1);
|
|
117
|
+
var debug = (0, import_debug.default)("@arcblock/did-connect-js:authenticator:base");
|
|
118
|
+
var DEFAULT_CHAIN_INFO = { id: "none", host: "none", type: "arcblock" };
|
|
119
|
+
var BaseAuthenticator = class {
|
|
120
|
+
_validateWallet(wallet, canSign = true) {
|
|
121
|
+
if (!wallet) {
|
|
122
|
+
throw new Error("WalletAuthenticator cannot work without wallet");
|
|
123
|
+
}
|
|
124
|
+
if (typeof wallet === "function") {
|
|
125
|
+
return wallet;
|
|
126
|
+
}
|
|
127
|
+
if ((0, import_wallet.isValid)(wallet, canSign)) {
|
|
128
|
+
if (!wallet.pk && wallet.publicKey) {
|
|
129
|
+
wallet.pk = wallet.publicKey;
|
|
130
|
+
}
|
|
131
|
+
if (!wallet.sk && wallet.secretKey) {
|
|
132
|
+
wallet.sk = wallet.secretKey;
|
|
133
|
+
}
|
|
134
|
+
return wallet;
|
|
135
|
+
}
|
|
136
|
+
if (canSign && !wallet.sk) {
|
|
137
|
+
throw new Error("WalletAuthenticator cannot work without wallet.sk");
|
|
138
|
+
}
|
|
139
|
+
if (!wallet.pk) {
|
|
140
|
+
throw new Error("WalletAuthenticator cannot work without wallet.pk");
|
|
141
|
+
}
|
|
142
|
+
if (!wallet.address) {
|
|
143
|
+
throw new Error("WalletAuthenticator cannot work without wallet.address");
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const fullWallet = (0, import_wallet.fromJSON)(wallet);
|
|
147
|
+
if (!fullWallet.pk && fullWallet.publicKey) {
|
|
148
|
+
fullWallet.pk = fullWallet.publicKey;
|
|
149
|
+
}
|
|
150
|
+
if (!fullWallet.sk && fullWallet.secretKey) {
|
|
151
|
+
fullWallet.sk = fullWallet.secretKey;
|
|
152
|
+
}
|
|
153
|
+
return fullWallet;
|
|
154
|
+
} catch (_err) {
|
|
155
|
+
return wallet;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Verify a DID auth response sent from DID Wallet
|
|
160
|
+
*
|
|
161
|
+
* @method
|
|
162
|
+
* @param {object} data
|
|
163
|
+
* @param {string} [locale=en]
|
|
164
|
+
* @param {boolean} [enforceTimestamp=true]
|
|
165
|
+
* @returns Promise<boolean>
|
|
166
|
+
*/
|
|
167
|
+
async _verify(data, fieldPk, fieldInfo, locale = "en", enforceTimestamp = true) {
|
|
168
|
+
debug("verify", data, locale);
|
|
169
|
+
const errors2 = {
|
|
170
|
+
pkMissing: {
|
|
171
|
+
en: `${fieldPk} is required to complete auth`,
|
|
172
|
+
zh: `${fieldPk} \u53C2\u6570\u7F3A\u5931`
|
|
173
|
+
},
|
|
174
|
+
tokenMissing: {
|
|
175
|
+
en: `${fieldInfo} is required to complete auth`,
|
|
176
|
+
zh: "JWT Token \u53C2\u6570\u7F3A\u5931"
|
|
177
|
+
},
|
|
178
|
+
pkFormat: {
|
|
179
|
+
en: `${fieldPk} should be either base58 or base16 format`,
|
|
180
|
+
zh: `${fieldPk} \u65E0\u6CD5\u89E3\u6790`
|
|
181
|
+
},
|
|
182
|
+
tokenInvalid: {
|
|
183
|
+
en: "Invalid JWT token",
|
|
184
|
+
zh: "\u7B7E\u540D\u65E0\u6548"
|
|
185
|
+
},
|
|
186
|
+
timeInvalid: {
|
|
187
|
+
en: "JWT token expired, make sure your device time in sync with network",
|
|
188
|
+
zh: "\u7B7E\u540D\u4E2D\u7684\u65F6\u95F4\u6233\u65E0\u6548\uFF0C\u8BF7\u786E\u4FDD\u8BBE\u5907\u548C\u7F51\u7EDC\u65F6\u95F4\u540C\u6B65"
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const pk = data[fieldPk];
|
|
192
|
+
const info = data[fieldInfo];
|
|
193
|
+
if (!pk) {
|
|
194
|
+
throw new Error(errors2.pkMissing[locale]);
|
|
195
|
+
}
|
|
196
|
+
if (!info) {
|
|
197
|
+
throw new Error(errors2.tokenMissing[locale]);
|
|
198
|
+
}
|
|
199
|
+
if (!pk) {
|
|
200
|
+
throw new Error(errors2.pkFormat[locale]);
|
|
201
|
+
}
|
|
202
|
+
if (!await Jwt.verify(info, pk)) {
|
|
203
|
+
const isValidSig = await Jwt.verify(info, pk, { tolerance: 0, enforceTimestamp: false });
|
|
204
|
+
if (enforceTimestamp) {
|
|
205
|
+
const error = isValidSig ? errors2.timeInvalid[locale] : errors2.tokenInvalid[locale];
|
|
206
|
+
throw new Error(error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return Jwt.decode(info);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
var base_default = BaseAuthenticator;
|
|
213
|
+
|
|
214
|
+
// src/schema/index.ts
|
|
215
|
+
var import_validator2 = require("@arcblock/validator");
|
|
216
|
+
var v2 = __toESM(require("valibot"), 1);
|
|
217
|
+
|
|
218
|
+
// src/schema/claims.ts
|
|
219
|
+
var import_validator = require("@arcblock/validator");
|
|
220
|
+
var import_mcrypto = require("@ocap/mcrypto");
|
|
221
|
+
var v = __toESM(require("valibot"), 1);
|
|
222
|
+
var isHttpUrl = (s) => {
|
|
223
|
+
try {
|
|
224
|
+
const url2 = new URL(s);
|
|
225
|
+
return ["http:", "https:"].includes(url2.protocol);
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var trustedIssuerSchema = v.union([
|
|
231
|
+
v.object({
|
|
232
|
+
did: (0, import_validator.vDID)(),
|
|
233
|
+
endpoint: v.pipe(v.string(), v.check(isHttpUrl, "Must be a valid HTTP/HTTPS URL"))
|
|
234
|
+
}),
|
|
235
|
+
(0, import_validator.vDID)()
|
|
236
|
+
]);
|
|
237
|
+
var optionalUrlSchema = v.optional(
|
|
238
|
+
v.pipe(
|
|
239
|
+
v.string(),
|
|
240
|
+
v.check((s) => {
|
|
241
|
+
if (s === "") return true;
|
|
242
|
+
try {
|
|
243
|
+
const url2 = new URL(s);
|
|
244
|
+
return ["http:", "https:"].includes(url2.protocol);
|
|
245
|
+
} catch {
|
|
246
|
+
return s.startsWith("/");
|
|
247
|
+
}
|
|
248
|
+
}, "Must be a valid URL")
|
|
249
|
+
),
|
|
250
|
+
""
|
|
251
|
+
);
|
|
252
|
+
var requirementSchema = v.object({
|
|
253
|
+
tokens: v.array(
|
|
254
|
+
v.object({
|
|
255
|
+
address: (0, import_validator.vDID)(),
|
|
256
|
+
value: (0, import_validator.vBNPositive)()
|
|
257
|
+
})
|
|
258
|
+
),
|
|
259
|
+
assets: v.optional(
|
|
260
|
+
v.object({
|
|
261
|
+
address: v.optional(v.array((0, import_validator.vDID)())),
|
|
262
|
+
parent: v.optional(v.array((0, import_validator.vDID)())),
|
|
263
|
+
issuer: v.optional(v.array((0, import_validator.vDID)())),
|
|
264
|
+
amount: v.optional(v.pipe(v.number(), v.check((n) => n > 0, "Must be positive"), v.minValue(1)))
|
|
265
|
+
})
|
|
266
|
+
)
|
|
267
|
+
});
|
|
268
|
+
var targetTypeSchema = v.optional(
|
|
269
|
+
v.object({
|
|
270
|
+
key: v.optional(
|
|
271
|
+
v.picklist(Object.keys(import_mcrypto.types.KeyType).map((x) => x.toLowerCase())),
|
|
272
|
+
"ed25519"
|
|
273
|
+
),
|
|
274
|
+
hash: v.optional(
|
|
275
|
+
v.picklist(Object.keys(import_mcrypto.types.HashType).map((x) => x.toLowerCase())),
|
|
276
|
+
"sha3"
|
|
277
|
+
),
|
|
278
|
+
role: v.optional(
|
|
279
|
+
v.picklist(Object.keys(import_mcrypto.types.RoleType).map((x) => x.toLowerCase().split("_").pop())),
|
|
280
|
+
"account"
|
|
281
|
+
),
|
|
282
|
+
encoding: v.optional(
|
|
283
|
+
v.picklist(Object.keys(import_mcrypto.types.EncodingType).map((x) => x.toLowerCase())),
|
|
284
|
+
"base58"
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
function withValidate(schema) {
|
|
289
|
+
return Object.assign(schema, {
|
|
290
|
+
validate: (data) => {
|
|
291
|
+
const { value, error } = (0, import_validator.vValidate)(schema, data);
|
|
292
|
+
if (error) error.toString = () => error.message;
|
|
293
|
+
return { value, error };
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
var profileItemValues = ["did", "fullName", "email", "phone", "signature", "avatar", "birthday", "url"];
|
|
298
|
+
var claims_default = (chainInfo2) => {
|
|
299
|
+
const createStandardFields = (type, description) => ({
|
|
300
|
+
type: v.optional(v.literal(type), type),
|
|
301
|
+
description: v.optional(v.pipe(v.string(), v.minLength(1)), description),
|
|
302
|
+
chainInfo: v.optional(chainInfo2),
|
|
303
|
+
mfaCode: v.optional(v.array(v.optional(v.pipe(v.number(), v.minValue(10), v.maxValue(99)))), []),
|
|
304
|
+
meta: v.optional(v.any(), {})
|
|
305
|
+
});
|
|
306
|
+
const authPrincipal = withValidate(
|
|
307
|
+
v.object({
|
|
308
|
+
...createStandardFields("authPrincipal", "Please continue with your account"),
|
|
309
|
+
target: v.optional(v.union([v.literal(""), (0, import_validator.vDID)()]), ""),
|
|
310
|
+
supervised: v.optional(v.boolean(), false),
|
|
311
|
+
targetType: targetTypeSchema
|
|
312
|
+
})
|
|
313
|
+
);
|
|
314
|
+
const keyPair = withValidate(
|
|
315
|
+
v.object({
|
|
316
|
+
...createStandardFields("keyPair", "Please create account to continue."),
|
|
317
|
+
moniker: v.pipe(v.string(), v.regex(/^[a-zA-Z0-9][-a-zA-Z0-9_]{2,128}$/)),
|
|
318
|
+
declare: v.optional(v.boolean(), true),
|
|
319
|
+
migrateFrom: v.optional(v.union([v.literal(""), (0, import_validator.vDID)()]), ""),
|
|
320
|
+
targetType: targetTypeSchema
|
|
321
|
+
})
|
|
322
|
+
);
|
|
323
|
+
const encryptionKey = withValidate(
|
|
324
|
+
v.object({
|
|
325
|
+
...createStandardFields("encryptionKey", "Please provide encryptionKey to continue."),
|
|
326
|
+
salt: v.string(),
|
|
327
|
+
delegation: v.optional(v.union([v.literal(""), v.string()]), "")
|
|
328
|
+
})
|
|
329
|
+
);
|
|
330
|
+
const profile = withValidate(
|
|
331
|
+
v.pipe(
|
|
332
|
+
v.looseObject({
|
|
333
|
+
...createStandardFields("profile", "Please provide your profile to continue.")
|
|
334
|
+
}),
|
|
335
|
+
// Rename fields → items (ignoreUndefined: true, override: true)
|
|
336
|
+
v.transform((obj) => {
|
|
337
|
+
const { fields, ...rest } = obj;
|
|
338
|
+
if (fields !== void 0) {
|
|
339
|
+
return { ...rest, items: fields };
|
|
340
|
+
}
|
|
341
|
+
return rest;
|
|
342
|
+
}),
|
|
343
|
+
v.rawTransform(({ dataset, addIssue, NEVER }) => {
|
|
344
|
+
const obj = dataset.value;
|
|
345
|
+
const items = obj.items ?? ["fullName"];
|
|
346
|
+
if (!Array.isArray(items) || items.length < 1) {
|
|
347
|
+
addIssue({ message: '"items" must contain at least 1 items' });
|
|
348
|
+
return NEVER;
|
|
349
|
+
}
|
|
350
|
+
const validSet = new Set(profileItemValues);
|
|
351
|
+
for (let i = 0; i < items.length; i++) {
|
|
352
|
+
if (!validSet.has(items[i])) {
|
|
353
|
+
addIssue({
|
|
354
|
+
message: `"items[${i}]" must be one of [${[...profileItemValues].join(", ")}]`
|
|
355
|
+
});
|
|
356
|
+
return NEVER;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return { ...obj, items };
|
|
360
|
+
})
|
|
361
|
+
)
|
|
362
|
+
);
|
|
363
|
+
const signature = withValidate(
|
|
364
|
+
v.object({
|
|
365
|
+
...createStandardFields("signature", "Sign this transaction or message to continue."),
|
|
366
|
+
typeUrl: v.picklist([
|
|
367
|
+
"fg:x:delegation",
|
|
368
|
+
"fg:t:transaction",
|
|
369
|
+
"mime:text/plain",
|
|
370
|
+
"mime:text/html",
|
|
371
|
+
"eth:transaction",
|
|
372
|
+
"eth:standard-data",
|
|
373
|
+
"eth:personal-data",
|
|
374
|
+
"eth:typed-data",
|
|
375
|
+
"eth:legacy-data"
|
|
376
|
+
]),
|
|
377
|
+
display: v.optional(v.union([v.literal(""), v.string()]), ""),
|
|
378
|
+
method: v.optional(
|
|
379
|
+
v.picklist(["none", ...Object.keys(import_mcrypto.types.HashType).map((x) => x.toLowerCase())]),
|
|
380
|
+
"sha3"
|
|
381
|
+
),
|
|
382
|
+
digest: v.optional(v.union([v.literal(""), v.string()]), ""),
|
|
383
|
+
origin: v.optional(v.union([v.literal(""), v.string()]), ""),
|
|
384
|
+
nonce: v.optional(v.union([v.literal(""), v.string()]), ""),
|
|
385
|
+
requirement: v.optional(requirementSchema)
|
|
386
|
+
})
|
|
387
|
+
);
|
|
388
|
+
const prepareTx = withValidate(
|
|
389
|
+
v.object({
|
|
390
|
+
...createStandardFields("prepareTx", "Prepare and sign this transaction to continue."),
|
|
391
|
+
display: v.optional(v.union([v.literal(""), v.string()]), ""),
|
|
392
|
+
partialTx: v.string(),
|
|
393
|
+
nonce: v.optional(v.union([v.literal(""), v.string()]), ""),
|
|
394
|
+
requirement: requirementSchema
|
|
395
|
+
})
|
|
396
|
+
);
|
|
397
|
+
const agreement = withValidate(
|
|
398
|
+
v.object({
|
|
399
|
+
...createStandardFields("agreement", "Confirm your agreement to continue."),
|
|
400
|
+
uri: v.union([
|
|
401
|
+
v.literal(""),
|
|
402
|
+
v.pipe(v.string(), v.check(isHttpUrl, "Must be a valid HTTP/HTTPS URL"))
|
|
403
|
+
]),
|
|
404
|
+
method: v.optional(
|
|
405
|
+
v.picklist(Object.keys(import_mcrypto.types.HashType).map((x) => x.toLowerCase())),
|
|
406
|
+
"sha2"
|
|
407
|
+
),
|
|
408
|
+
digest: v.string()
|
|
409
|
+
})
|
|
410
|
+
);
|
|
411
|
+
const vcFilterSchema = v.object({
|
|
412
|
+
type: v.optional(v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.minLength(1))),
|
|
413
|
+
target: v.optional((0, import_validator.vDID)()),
|
|
414
|
+
trustedIssuers: v.optional(v.pipe(v.array(trustedIssuerSchema), v.minLength(1))),
|
|
415
|
+
tag: v.optional(v.pipe(v.string(), v.check((s) => s === "" || s.length >= 1, "Must be non-empty or empty string")), ""),
|
|
416
|
+
ownerDid: v.optional(v.array((0, import_validator.vDID)()), []),
|
|
417
|
+
claimUrl: optionalUrlSchema,
|
|
418
|
+
acquireUrl: optionalUrlSchema
|
|
419
|
+
});
|
|
420
|
+
const verifiableCredential = withValidate(
|
|
421
|
+
v.object({
|
|
422
|
+
...createStandardFields("verifiableCredential", "Please present a verifiable credential to continue."),
|
|
423
|
+
optional: v.optional(v.boolean(), false),
|
|
424
|
+
claimUrl: optionalUrlSchema,
|
|
425
|
+
acquireUrl: optionalUrlSchema,
|
|
426
|
+
// v1
|
|
427
|
+
item: v.optional(v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.minLength(1))),
|
|
428
|
+
target: v.optional((0, import_validator.vDID)()),
|
|
429
|
+
trustedIssuers: v.optional(v.pipe(v.array(trustedIssuerSchema), v.minLength(1))),
|
|
430
|
+
tag: v.optional(v.pipe(v.string(), v.check((s) => s === "" || s.length >= 1, "Must be non-empty or empty string")), ""),
|
|
431
|
+
ownerDid: v.optional(v.array((0, import_validator.vDID)()), []),
|
|
432
|
+
// v2
|
|
433
|
+
filters: v.optional(v.array(vcFilterSchema))
|
|
434
|
+
})
|
|
435
|
+
);
|
|
436
|
+
const assetFilterSchema = v.object({
|
|
437
|
+
address: v.optional((0, import_validator.vDID)()),
|
|
438
|
+
trustedIssuers: v.optional(v.pipe(v.array(trustedIssuerSchema), v.minLength(1))),
|
|
439
|
+
trustedParents: v.optional(v.pipe(v.array((0, import_validator.vDID)()), v.minLength(1))),
|
|
440
|
+
tag: v.optional(v.pipe(v.string(), v.check((s) => s === "" || s.length >= 1, "Must be non-empty or empty string")), ""),
|
|
441
|
+
ownerDid: v.optional(v.array((0, import_validator.vDID)()), []),
|
|
442
|
+
consumed: v.optional(v.boolean()),
|
|
443
|
+
acquireUrl: optionalUrlSchema
|
|
444
|
+
});
|
|
445
|
+
const asset = withValidate(
|
|
446
|
+
v.object({
|
|
447
|
+
...createStandardFields("asset", "Please present an on chain asset to continue."),
|
|
448
|
+
optional: v.optional(v.boolean(), false),
|
|
449
|
+
// v1
|
|
450
|
+
address: v.optional((0, import_validator.vDID)()),
|
|
451
|
+
trustedIssuers: v.optional(v.pipe(v.array(trustedIssuerSchema), v.minLength(1))),
|
|
452
|
+
trustedParents: v.optional(v.pipe(v.array((0, import_validator.vDID)()), v.minLength(1))),
|
|
453
|
+
tag: v.optional(v.pipe(v.string(), v.check((s) => s === "" || s.length >= 1, "Must be non-empty or empty string")), ""),
|
|
454
|
+
ownerDid: v.optional(v.array((0, import_validator.vDID)()), []),
|
|
455
|
+
consumed: v.optional(v.boolean()),
|
|
456
|
+
acquireUrl: optionalUrlSchema,
|
|
457
|
+
// v2
|
|
458
|
+
filters: v.optional(v.array(assetFilterSchema))
|
|
459
|
+
})
|
|
460
|
+
);
|
|
461
|
+
const assetOrVCFilterSchema = v.object({
|
|
462
|
+
type: v.optional(v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.minLength(1))),
|
|
463
|
+
address: v.optional((0, import_validator.vDID)()),
|
|
464
|
+
trustedIssuers: v.optional(v.pipe(v.array(trustedIssuerSchema), v.minLength(1))),
|
|
465
|
+
trustedParents: v.optional(v.pipe(v.array((0, import_validator.vDID)()), v.minLength(1))),
|
|
466
|
+
tag: v.optional(v.pipe(v.string(), v.check((s) => s === "" || s.length >= 1, "Must be non-empty or empty string")), ""),
|
|
467
|
+
ownerDid: v.optional(v.array((0, import_validator.vDID)()), []),
|
|
468
|
+
consumed: v.optional(v.boolean()),
|
|
469
|
+
claimUrl: optionalUrlSchema,
|
|
470
|
+
acquireUrl: optionalUrlSchema
|
|
471
|
+
});
|
|
472
|
+
const assetOrVC = withValidate(
|
|
473
|
+
v.object({
|
|
474
|
+
...createStandardFields("assetOrVC", "Please present NFT to continue."),
|
|
475
|
+
filters: v.pipe(v.array(assetOrVCFilterSchema), v.minLength(1)),
|
|
476
|
+
optional: v.optional(v.boolean(), false)
|
|
477
|
+
})
|
|
478
|
+
);
|
|
479
|
+
return {
|
|
480
|
+
authPrincipal,
|
|
481
|
+
profile,
|
|
482
|
+
signature,
|
|
483
|
+
prepareTx,
|
|
484
|
+
agreement,
|
|
485
|
+
verifiableCredential,
|
|
486
|
+
asset,
|
|
487
|
+
assetOrVC,
|
|
488
|
+
keyPair,
|
|
489
|
+
encryptionKey
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// src/schema/index.ts
|
|
494
|
+
var isHttpUrl2 = (s) => {
|
|
495
|
+
try {
|
|
496
|
+
const url2 = new URL(s);
|
|
497
|
+
return ["http:", "https:"].includes(url2.protocol);
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
var chainInfoSchema = v2.pipe(
|
|
503
|
+
v2.object({
|
|
504
|
+
type: v2.optional(v2.picklist(["arcblock", "ethereum", "solona"]), "arcblock"),
|
|
505
|
+
id: v2.optional(v2.any()),
|
|
506
|
+
host: v2.optional(v2.any())
|
|
507
|
+
}),
|
|
508
|
+
v2.rawTransform(({ dataset, addIssue, NEVER }) => {
|
|
509
|
+
const obj = dataset.value;
|
|
510
|
+
let typeSchema;
|
|
511
|
+
switch (obj.type) {
|
|
512
|
+
case "arcblock":
|
|
513
|
+
typeSchema = v2.object({
|
|
514
|
+
type: v2.literal("arcblock"),
|
|
515
|
+
id: v2.optional(v2.string(), "none"),
|
|
516
|
+
host: v2.optional(
|
|
517
|
+
v2.pipe(
|
|
518
|
+
v2.string(),
|
|
519
|
+
v2.check(
|
|
520
|
+
(s) => s === "none" || isHttpUrl2(s),
|
|
521
|
+
"must be a valid uri with a scheme matching the http|https pattern"
|
|
522
|
+
)
|
|
523
|
+
),
|
|
524
|
+
"none"
|
|
525
|
+
)
|
|
526
|
+
});
|
|
527
|
+
break;
|
|
528
|
+
case "ethereum":
|
|
529
|
+
typeSchema = v2.object({
|
|
530
|
+
type: v2.literal("ethereum"),
|
|
531
|
+
id: v2.pipe(v2.string(), v2.regex(/^[0-9]+$/, "fails to match the numbers pattern")),
|
|
532
|
+
host: v2.optional(v2.string())
|
|
533
|
+
});
|
|
534
|
+
break;
|
|
535
|
+
case "solona":
|
|
536
|
+
typeSchema = v2.object({
|
|
537
|
+
type: v2.literal("solona"),
|
|
538
|
+
id: v2.pipe(v2.string(), v2.regex(/^[0-9]+$/, "fails to match the numbers pattern")),
|
|
539
|
+
host: v2.optional(v2.string())
|
|
540
|
+
});
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
const result = v2.safeParse(typeSchema, obj);
|
|
544
|
+
if (!result.success) {
|
|
545
|
+
for (const issue of result.issues) {
|
|
546
|
+
addIssue({ message: issue.message, path: issue.path });
|
|
547
|
+
}
|
|
548
|
+
return NEVER;
|
|
549
|
+
}
|
|
550
|
+
const output = {};
|
|
551
|
+
for (const [k, val] of Object.entries(result.output)) {
|
|
552
|
+
if (val !== void 0) output[k] = val;
|
|
553
|
+
}
|
|
554
|
+
return output;
|
|
555
|
+
})
|
|
556
|
+
);
|
|
557
|
+
var appInfoSchema = v2.looseObject({
|
|
558
|
+
name: v2.string(),
|
|
559
|
+
description: v2.string(),
|
|
560
|
+
icon: v2.pipe(v2.string(), v2.check(isHttpUrl2, "Must be a valid HTTP/HTTPS URL")),
|
|
561
|
+
link: v2.optional(v2.pipe(v2.string(), v2.check(isHttpUrl2, "Must be a valid HTTP/HTTPS URL"))),
|
|
562
|
+
path: v2.optional(v2.pipe(v2.string(), v2.check(isHttpUrl2, "Must be a valid HTTP/HTTPS URL")), "https://abtwallet.io/i/"),
|
|
563
|
+
publisher: v2.optional((0, import_validator2.vDID)()),
|
|
564
|
+
updateSubEndpoint: v2.optional(v2.boolean()),
|
|
565
|
+
subscriptionEndpoint: v2.optional(v2.string()),
|
|
566
|
+
nodeDid: v2.optional((0, import_validator2.vDID)()),
|
|
567
|
+
agentDid: v2.optional((0, import_validator2.vDID)())
|
|
568
|
+
});
|
|
569
|
+
function withValidate2(schema) {
|
|
570
|
+
return Object.assign(schema, {
|
|
571
|
+
validate: (data) => {
|
|
572
|
+
const { value, error } = (0, import_validator2.vValidate)(schema, data);
|
|
573
|
+
if (error) error.toString = () => error.message;
|
|
574
|
+
return { value, error };
|
|
575
|
+
},
|
|
576
|
+
validateAsync: (data) => {
|
|
577
|
+
const { value, error } = (0, import_validator2.vValidate)(schema, data);
|
|
578
|
+
return error ? Promise.reject(new Error(error.message)) : Promise.resolve(value);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
var chainInfo = withValidate2(chainInfoSchema);
|
|
583
|
+
var appInfo = withValidate2(appInfoSchema);
|
|
584
|
+
var claims = claims_default(chainInfoSchema);
|
|
585
|
+
|
|
586
|
+
// src/authenticator/wallet.ts
|
|
587
|
+
var debug2 = (0, import_debug2.default)("@arcblock/did-connect-js:authenticator:wallet");
|
|
588
|
+
var DEFAULT_TIMEOUT = 8e3;
|
|
589
|
+
var MFA_CODE_COUNT = 3;
|
|
590
|
+
var WalletAuthenticator = class _WalletAuthenticator extends base_default {
|
|
591
|
+
wallet;
|
|
592
|
+
appInfo;
|
|
593
|
+
memberAppInfo;
|
|
594
|
+
chainInfo;
|
|
595
|
+
delegator;
|
|
596
|
+
delegation;
|
|
597
|
+
baseUrl;
|
|
598
|
+
tokenKey;
|
|
599
|
+
timeout;
|
|
600
|
+
txEncoder;
|
|
601
|
+
static formatDisplay(display) {
|
|
602
|
+
if (!display) {
|
|
603
|
+
return "";
|
|
604
|
+
}
|
|
605
|
+
if (display?.type && display.content) {
|
|
606
|
+
return JSON.stringify(pick(display, ["type", "content"]));
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
const parsed = JSON.parse(display);
|
|
610
|
+
if (parsed?.type && parsed.content) {
|
|
611
|
+
return display;
|
|
612
|
+
}
|
|
613
|
+
return "";
|
|
614
|
+
} catch (_err) {
|
|
615
|
+
return "";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Creates an instance of DID Authenticator.
|
|
620
|
+
*
|
|
621
|
+
* @class
|
|
622
|
+
* @param {object} config
|
|
623
|
+
* @param {WalletObject|Function} config.wallet - wallet instance {@see @ocap/wallet} or a function that returns wallet instance
|
|
624
|
+
* @param {WalletObject|Function} [config.delegator] - the party that authorizes `wallet` to perform actions on behalf of `wallet`
|
|
625
|
+
* @param {string|Function} [config.delegation] - the jwt token that proves delegation relationship
|
|
626
|
+
* @param {ApplicationInfo|Function} config.appInfo - application basic info or a function that returns application info
|
|
627
|
+
* @param {ChainInfo|Function} config.chainInfo - application chain info or a function that returns chain info
|
|
628
|
+
* @param {Number} [config.timeout=8000] - timeout in milliseconds when generating claim
|
|
629
|
+
* @param {object} [config.baseUrl] - url to assemble wallet request uri, can be inferred from request object
|
|
630
|
+
* @param {string} [config.tokenKey='_t_'] - query param key for `token`
|
|
631
|
+
*/
|
|
632
|
+
constructor({
|
|
633
|
+
wallet,
|
|
634
|
+
appInfo: appInfo2,
|
|
635
|
+
memberAppInfo,
|
|
636
|
+
delegator,
|
|
637
|
+
delegation,
|
|
638
|
+
timeout = DEFAULT_TIMEOUT,
|
|
639
|
+
chainInfo: chainInfo2 = DEFAULT_CHAIN_INFO,
|
|
640
|
+
baseUrl = "",
|
|
641
|
+
tokenKey = "_t_",
|
|
642
|
+
txEncoder
|
|
643
|
+
}) {
|
|
644
|
+
super();
|
|
645
|
+
this.wallet = this._validateWallet(wallet, false);
|
|
646
|
+
this.appInfo = this._validateAppInfo(appInfo2);
|
|
647
|
+
this.memberAppInfo = this._validateAppInfo(memberAppInfo, true);
|
|
648
|
+
this.chainInfo = chainInfo2;
|
|
649
|
+
this.delegator = delegator;
|
|
650
|
+
this.delegation = delegation;
|
|
651
|
+
this.baseUrl = baseUrl;
|
|
652
|
+
this.tokenKey = tokenKey;
|
|
653
|
+
this.timeout = timeout;
|
|
654
|
+
this.txEncoder = txEncoder;
|
|
655
|
+
if (!this.appInfo.link) {
|
|
656
|
+
this.appInfo.link = this.baseUrl;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Generate a deep link url that can be displayed as QRCode for DID Wallet to consume
|
|
661
|
+
*
|
|
662
|
+
* @method
|
|
663
|
+
* @param {object} params
|
|
664
|
+
* @param {string} params.baseUrl - baseUrl inferred from request object
|
|
665
|
+
* @param {string} params.pathname - wallet callback pathname
|
|
666
|
+
* @param {string} params.token - action token
|
|
667
|
+
* @param {object} params.query - params that should be persisted in wallet callback url
|
|
668
|
+
* @returns {string}
|
|
669
|
+
*/
|
|
670
|
+
uri({
|
|
671
|
+
baseUrl,
|
|
672
|
+
pathname = "",
|
|
673
|
+
token = "",
|
|
674
|
+
query = {}
|
|
675
|
+
} = {}) {
|
|
676
|
+
const params = { ...query, [this.tokenKey]: token };
|
|
677
|
+
const payload = {
|
|
678
|
+
action: "requestAuth",
|
|
679
|
+
url: encodeURIComponent(`${this.baseUrl || baseUrl}${pathname}?${import_node_querystring.default.stringify(params)}`)
|
|
680
|
+
};
|
|
681
|
+
const uri = `https://abtwallet.io/i/?${import_node_querystring.default.stringify(payload)}`;
|
|
682
|
+
debug2("uri", { token, pathname, uri, params, payload });
|
|
683
|
+
return uri;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Compute public url to return to wallet
|
|
687
|
+
*
|
|
688
|
+
* @method
|
|
689
|
+
* @param {string} pathname
|
|
690
|
+
* @param {object} params
|
|
691
|
+
* @returns {string}
|
|
692
|
+
*/
|
|
693
|
+
getPublicUrl(pathname, params = {}, baseUrl = "") {
|
|
694
|
+
return `${this.baseUrl || baseUrl}${pathname}?${import_node_querystring.default.stringify(params)}`;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Sign a plain response, usually on auth success or error
|
|
698
|
+
*
|
|
699
|
+
* @method
|
|
700
|
+
* @param {object} params
|
|
701
|
+
* @param {object} params.response - response
|
|
702
|
+
* @param {string} params.errorMessage - error message, default to empty
|
|
703
|
+
* @param {string} params.successMessage - success message, default to empty
|
|
704
|
+
* @param {string} params.nextWorkflow - https://github.com/ArcBlock/ABT-DID-Protocol#concatenate-multiple-workflow
|
|
705
|
+
* @param {string} params.nextUrl - tell wallet do open this url in webview
|
|
706
|
+
* @param {object} params.cookies - key-value pairs to be set as cookie before open nextUrl
|
|
707
|
+
* @param {object} params.storages - key-value pairs to be set as localStorage before open nextUrl
|
|
708
|
+
* @param {string} baseUrl
|
|
709
|
+
* @param {object} request
|
|
710
|
+
* @returns {Promise<object>} { appPk, agentPk, authInfo }
|
|
711
|
+
*/
|
|
712
|
+
async signResponse({
|
|
713
|
+
response = {},
|
|
714
|
+
errorMessage = "",
|
|
715
|
+
successMessage = "",
|
|
716
|
+
nextWorkflow = "",
|
|
717
|
+
nextUrl = "",
|
|
718
|
+
cookies = {},
|
|
719
|
+
storages = {}
|
|
720
|
+
}, baseUrl, request, extraParams = {}) {
|
|
721
|
+
const context = request.context || {};
|
|
722
|
+
const infoParams = { baseUrl, request, ...context, extraParams };
|
|
723
|
+
const [wallet, delegator, delegation] = await Promise.all([
|
|
724
|
+
this.getWalletInfo(infoParams),
|
|
725
|
+
this.getDelegator(infoParams),
|
|
726
|
+
this.getDelegation(infoParams)
|
|
727
|
+
]);
|
|
728
|
+
const [appInfo2, memberAppInfo] = await Promise.all([
|
|
729
|
+
this.getAppInfo({ ...infoParams, wallet, delegator }, "appInfo"),
|
|
730
|
+
this.getAppInfo({ ...infoParams, wallet, delegator }, "memberAppInfo")
|
|
731
|
+
]);
|
|
732
|
+
const didwallet = request.context.wallet;
|
|
733
|
+
const payload = {
|
|
734
|
+
appInfo: appInfo2,
|
|
735
|
+
memberAppInfo,
|
|
736
|
+
status: errorMessage ? "error" : "ok",
|
|
737
|
+
errorMessage: errorMessage || "",
|
|
738
|
+
successMessage: successMessage || "",
|
|
739
|
+
nextWorkflow: nextWorkflow || "",
|
|
740
|
+
nextUrl: nextUrl || "",
|
|
741
|
+
cookies: cookies || {},
|
|
742
|
+
storages: storages || "",
|
|
743
|
+
response
|
|
744
|
+
};
|
|
745
|
+
if (delegator) {
|
|
746
|
+
payload.iss = (0, import_util.toDid)(delegator.address);
|
|
747
|
+
payload.agentDid = (0, import_util.toDid)(wallet.address);
|
|
748
|
+
payload.verifiableClaims = [{ type: "certificate", content: delegation }];
|
|
749
|
+
}
|
|
750
|
+
const result = {
|
|
751
|
+
appPk: (0, import_util.toBase58)(wallet.pk),
|
|
752
|
+
authInfo: await wallet.signJWT(payload, true, didwallet ? didwallet.jwt : void 0)
|
|
753
|
+
};
|
|
754
|
+
if (delegator) {
|
|
755
|
+
result.appPk = (0, import_util.toBase58)(delegator.pk);
|
|
756
|
+
result.agentPk = (0, import_util.toBase58)(wallet.pk);
|
|
757
|
+
}
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Sign a auth response that returned to wallet: tell the wallet the appInfo/chainInfo
|
|
762
|
+
*
|
|
763
|
+
* @method
|
|
764
|
+
* @param {object} params
|
|
765
|
+
* @param {object} params.claims - info required by application to complete the auth
|
|
766
|
+
* @param {string} params.pathname - pathname to assemble callback url
|
|
767
|
+
* @param {string} params.baseUrl - baseUrl
|
|
768
|
+
* @param {object} params.challenge - random challenge to be included in the body
|
|
769
|
+
* @param {object} params.extraParams - extra query params and locale
|
|
770
|
+
* @param {object} params.request
|
|
771
|
+
* @param {object} params.context
|
|
772
|
+
* @param {string} params.context.token - action token
|
|
773
|
+
* @param {number} params.context.currentStep - current step
|
|
774
|
+
* @param {string} [params.context.sharedKey] - shared key between app and wallet
|
|
775
|
+
* @param {string} [params.context.encryptionKey] - encryption key from wallet
|
|
776
|
+
* @param {Function} [params.context.mfaCode] - function used to generate mfa code
|
|
777
|
+
* @param {string} params.context.userDid - decoded from req.query, base58
|
|
778
|
+
* @param {string} params.context.userPk - decoded from req.query, base58
|
|
779
|
+
* @param {string} params.context.didwallet - DID Wallet os and version
|
|
780
|
+
* @returns {Promise<object>} { appPk, agentPk, sharedKey, authInfo }
|
|
781
|
+
*/
|
|
782
|
+
async sign({
|
|
783
|
+
context,
|
|
784
|
+
request,
|
|
785
|
+
claims: claims2,
|
|
786
|
+
pathname = "",
|
|
787
|
+
baseUrl = "",
|
|
788
|
+
challenge = "",
|
|
789
|
+
extraParams = {}
|
|
790
|
+
}) {
|
|
791
|
+
const claimsInfo = await this.tryWithTimeout(
|
|
792
|
+
() => this.genRequestedClaims({
|
|
793
|
+
claims: claims2,
|
|
794
|
+
context: { baseUrl, request, ...context },
|
|
795
|
+
extraParams
|
|
796
|
+
})
|
|
797
|
+
);
|
|
798
|
+
if (claimsInfo.filter((x) => x.mfaCode && x.mfaCode.length > 0).length > 1) {
|
|
799
|
+
throw new Error("Multiple MFA is not supported when sending more than 1 claim");
|
|
800
|
+
}
|
|
801
|
+
const tmp = claimsInfo.find((x) => isEqual(this._isValidChainInfo(x.chainInfo), DEFAULT_CHAIN_INFO) === false);
|
|
802
|
+
const infoParams = { baseUrl, request, ...context, extraParams };
|
|
803
|
+
const [wallet, delegator, delegation, chainInfo2] = await Promise.all([
|
|
804
|
+
this.getWalletInfo(infoParams),
|
|
805
|
+
this.getDelegator(infoParams),
|
|
806
|
+
this.getDelegation(infoParams),
|
|
807
|
+
this.getChainInfo(infoParams, tmp?.chainInfo)
|
|
808
|
+
]);
|
|
809
|
+
const [appInfo2, memberAppInfo] = await Promise.all([
|
|
810
|
+
this.getAppInfo({ ...infoParams, wallet, delegator }, "appInfo"),
|
|
811
|
+
this.getAppInfo({ ...infoParams, wallet, delegator }, "memberAppInfo")
|
|
812
|
+
]);
|
|
813
|
+
const payload = {
|
|
814
|
+
action: "responseAuth",
|
|
815
|
+
challenge,
|
|
816
|
+
appInfo: appInfo2,
|
|
817
|
+
memberAppInfo,
|
|
818
|
+
chainInfo: chainInfo2,
|
|
819
|
+
requestedClaims: claimsInfo.map((x) => {
|
|
820
|
+
delete x.chainInfo;
|
|
821
|
+
return x;
|
|
822
|
+
}),
|
|
823
|
+
url: `${this.baseUrl || baseUrl}${pathname}?${import_node_querystring.default.stringify({ [this.tokenKey]: context.token })}`
|
|
824
|
+
};
|
|
825
|
+
if (delegator) {
|
|
826
|
+
payload.iss = (0, import_util.toDid)(delegator.address);
|
|
827
|
+
payload.agentDid = (0, import_util.toDid)(wallet.address);
|
|
828
|
+
payload.verifiableClaims = [{ type: "certificate", content: delegation }];
|
|
829
|
+
}
|
|
830
|
+
const version = context.didwallet ? context.didwallet.jwt : void 0;
|
|
831
|
+
const result = {
|
|
832
|
+
appPk: (0, import_util.toBase58)(wallet.pk),
|
|
833
|
+
authInfo: await wallet.signJWT(payload, true, version),
|
|
834
|
+
sensitive: claimsInfo.every((x) => ["keyPair", "encryptionKey"].includes(x.type))
|
|
835
|
+
};
|
|
836
|
+
if (result.sensitive && context.sharedKey && context.encryptionKey) {
|
|
837
|
+
try {
|
|
838
|
+
const pk = (0, import_util.fromBase58)(context.encryptionKey).toString("utf8");
|
|
839
|
+
result.sharedKey = import_rsa.default.encrypt(context.sharedKey, pk, "base58");
|
|
840
|
+
} catch (err) {
|
|
841
|
+
console.error("Failed to encrypt shared key", err);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
if (delegator) {
|
|
845
|
+
result.appPk = (0, import_util.toBase58)(delegator.pk);
|
|
846
|
+
result.agentPk = (0, import_util.toBase58)(wallet.pk);
|
|
847
|
+
}
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
850
|
+
/**
|
|
851
|
+
* Determine chainInfo on the fly
|
|
852
|
+
*
|
|
853
|
+
* @param {object} params - contains the context of this request
|
|
854
|
+
* @param {object|undefined} [info=undefined] - chain info object or function
|
|
855
|
+
* @returns {Promise<ChainInfo>}
|
|
856
|
+
* @memberof WalletAuthenticator
|
|
857
|
+
*/
|
|
858
|
+
async getChainInfo(params, info) {
|
|
859
|
+
if (info && this._isValidChainInfo(info)) {
|
|
860
|
+
return info;
|
|
861
|
+
}
|
|
862
|
+
if (typeof this.chainInfo === "function") {
|
|
863
|
+
const result = await this.tryWithTimeout(() => this.chainInfo(params));
|
|
864
|
+
if (this._isValidChainInfo(result)) {
|
|
865
|
+
return result;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (this.chainInfo && this._isValidChainInfo(this.chainInfo)) {
|
|
869
|
+
return this.chainInfo;
|
|
870
|
+
}
|
|
871
|
+
return DEFAULT_CHAIN_INFO;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Determine appInfo/memberAppInfo on the fly
|
|
875
|
+
*
|
|
876
|
+
* @param {object} params - contains the context of this request
|
|
877
|
+
* @param {string} key - appInfo | memberAppInfo
|
|
878
|
+
* @returns {Promise<ApplicationInfo>}
|
|
879
|
+
* @memberof WalletAuthenticator
|
|
880
|
+
*/
|
|
881
|
+
async getAppInfo(params, key = "appInfo") {
|
|
882
|
+
if (typeof this[key] === "function") {
|
|
883
|
+
const info = await this.tryWithTimeout(() => this[key](params));
|
|
884
|
+
if (info) {
|
|
885
|
+
if (!info.link) {
|
|
886
|
+
info.link = params.baseUrl;
|
|
887
|
+
}
|
|
888
|
+
if (!info.publisher) {
|
|
889
|
+
info.publisher = (0, import_util.toDid)(params.delegator ? params.delegator.address : params.wallet.address);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return this._validateAppInfo(info, key === "memberAppInfo");
|
|
893
|
+
}
|
|
894
|
+
if (this[key] && !this[key].publisher) {
|
|
895
|
+
this[key].publisher = (0, import_util.toDid)(params.delegator ? params.delegator.address : params.wallet.address);
|
|
896
|
+
}
|
|
897
|
+
return this[key];
|
|
898
|
+
}
|
|
899
|
+
async getWalletInfo(params) {
|
|
900
|
+
if (typeof this.wallet === "function") {
|
|
901
|
+
const result = await this.tryWithTimeout(() => this.wallet(params));
|
|
902
|
+
return this._validateWallet(result, false);
|
|
903
|
+
}
|
|
904
|
+
return this.wallet;
|
|
905
|
+
}
|
|
906
|
+
async getDelegator(params) {
|
|
907
|
+
if (typeof this.delegator === "function") {
|
|
908
|
+
const result = await this.tryWithTimeout(() => this.delegator(params));
|
|
909
|
+
return result ? this._validateWallet(result, false) : null;
|
|
910
|
+
}
|
|
911
|
+
return this.delegator;
|
|
912
|
+
}
|
|
913
|
+
async getDelegation(params) {
|
|
914
|
+
if (typeof this.delegation === "function") {
|
|
915
|
+
const result = await this.tryWithTimeout(() => this.delegation(params));
|
|
916
|
+
return result;
|
|
917
|
+
}
|
|
918
|
+
return this.delegation;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Verify a DID auth response sent from DID Wallet
|
|
922
|
+
*
|
|
923
|
+
* @method
|
|
924
|
+
* @param {object} data
|
|
925
|
+
* @param {string} [locale=en]
|
|
926
|
+
* @param {boolean} [enforceTimestamp=true]
|
|
927
|
+
* @returns Promise<boolean>
|
|
928
|
+
*/
|
|
929
|
+
async verify(data, locale = "en", enforceTimestamp = true) {
|
|
930
|
+
const {
|
|
931
|
+
iss,
|
|
932
|
+
iat,
|
|
933
|
+
challenge = "",
|
|
934
|
+
action = "responseAuth",
|
|
935
|
+
requestedClaims
|
|
936
|
+
} = await this._verify(data, "userPk", "userInfo", locale, enforceTimestamp);
|
|
937
|
+
debug2("verify.context", { userPk: data.userPk, userDid: (0, import_did.toAddress)(iss), action, challenge });
|
|
938
|
+
debug2("verify.claims", requestedClaims);
|
|
939
|
+
return {
|
|
940
|
+
token: data.token,
|
|
941
|
+
userDid: (0, import_did.toAddress)(iss),
|
|
942
|
+
userPk: data.userPk,
|
|
943
|
+
claims: requestedClaims,
|
|
944
|
+
action,
|
|
945
|
+
challenge,
|
|
946
|
+
timestamp: iat
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
// ---------------------------------------
|
|
950
|
+
// Request claim related methods
|
|
951
|
+
// ---------------------------------------
|
|
952
|
+
genRequestedClaims({
|
|
953
|
+
claims: claims2,
|
|
954
|
+
context,
|
|
955
|
+
extraParams
|
|
956
|
+
}) {
|
|
957
|
+
return Promise.all(
|
|
958
|
+
Object.keys(claims2).map(async (x) => {
|
|
959
|
+
let name = x;
|
|
960
|
+
let claim = claims2[x];
|
|
961
|
+
if (Array.isArray(claims2[x])) {
|
|
962
|
+
[name, claim] = claims2[x];
|
|
963
|
+
}
|
|
964
|
+
if (!claims[name]) {
|
|
965
|
+
throw new Error(`Unsupported claim type ${name}`);
|
|
966
|
+
}
|
|
967
|
+
const fn = typeof this[name] === "function" ? name : "getClaimInfo";
|
|
968
|
+
const result = await this[fn]({ claim, context, extraParams });
|
|
969
|
+
if (result.mfa && typeof context.mfaCode === "function") {
|
|
970
|
+
result.mfaCode = [await context.mfaCode()];
|
|
971
|
+
while (result.mfaCode.length < MFA_CODE_COUNT) {
|
|
972
|
+
const noise = random(10, 99);
|
|
973
|
+
if (result.mfaCode.includes(noise) === false) {
|
|
974
|
+
result.mfaCode.push(noise);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
result.mfaCode = shuffle(result.mfaCode);
|
|
978
|
+
}
|
|
979
|
+
const { value, error } = claims[name].validate(result);
|
|
980
|
+
if (error) {
|
|
981
|
+
throw new Error(`Invalid ${name} claim: ${error.message}`);
|
|
982
|
+
}
|
|
983
|
+
return value;
|
|
984
|
+
})
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
async getClaimInfo({
|
|
988
|
+
claim,
|
|
989
|
+
context,
|
|
990
|
+
extraParams
|
|
991
|
+
}) {
|
|
992
|
+
const { userDid, userPk, didwallet } = context;
|
|
993
|
+
const result = typeof claim === "function" ? await claim({
|
|
994
|
+
userDid: userDid ? (0, import_did.toAddress)(userDid) : "",
|
|
995
|
+
userPk: userPk || "",
|
|
996
|
+
didwallet,
|
|
997
|
+
extraParams,
|
|
998
|
+
context
|
|
999
|
+
}) : claim;
|
|
1000
|
+
const infoParams = { ...context, ...extraParams };
|
|
1001
|
+
const chainInfo2 = await this.getChainInfo(infoParams, result.chainInfo);
|
|
1002
|
+
result.chainInfo = chainInfo2;
|
|
1003
|
+
return result;
|
|
1004
|
+
}
|
|
1005
|
+
// Request wallet to sign something: transaction/text/html/image
|
|
1006
|
+
async signature({
|
|
1007
|
+
claim,
|
|
1008
|
+
context,
|
|
1009
|
+
extraParams
|
|
1010
|
+
}) {
|
|
1011
|
+
const {
|
|
1012
|
+
data,
|
|
1013
|
+
type = "mime:text/plain",
|
|
1014
|
+
digest = "",
|
|
1015
|
+
method = "sha3",
|
|
1016
|
+
// set this to `none` to instruct wallet not to hash before signing
|
|
1017
|
+
wallet,
|
|
1018
|
+
sender,
|
|
1019
|
+
display,
|
|
1020
|
+
description: desc,
|
|
1021
|
+
chainInfo: chainInfo2,
|
|
1022
|
+
meta = {},
|
|
1023
|
+
mfa = false,
|
|
1024
|
+
nonce = "",
|
|
1025
|
+
requirement = { tokens: [], assets: {} }
|
|
1026
|
+
} = await this.getClaimInfo({
|
|
1027
|
+
claim,
|
|
1028
|
+
context,
|
|
1029
|
+
extraParams
|
|
1030
|
+
});
|
|
1031
|
+
debug2("claim.signature", { data, digest, type, sender, context, nonce, requirement });
|
|
1032
|
+
if (!data && !digest) {
|
|
1033
|
+
throw new Error("Signature claim requires either data or digest to be provided");
|
|
1034
|
+
}
|
|
1035
|
+
const description = desc || "Sign this transaction to continue.";
|
|
1036
|
+
if (type.endsWith("Tx")) {
|
|
1037
|
+
if (!chainInfo2.host) {
|
|
1038
|
+
throw new Error("Invalid chainInfo when trying to encoding transaction");
|
|
1039
|
+
}
|
|
1040
|
+
if (!this.txEncoder) {
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
`Transaction encoding for "${type}" requires a txEncoder. Pass txEncoder to WalletAuthenticator or install @ocap/client.`
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
if (!data.pk) {
|
|
1046
|
+
data.pk = context.userPk;
|
|
1047
|
+
}
|
|
1048
|
+
try {
|
|
1049
|
+
const txBuffer = await this.txEncoder({
|
|
1050
|
+
type,
|
|
1051
|
+
data,
|
|
1052
|
+
wallet: wallet || (0, import_wallet2.fromAddress)(sender || context.userDid),
|
|
1053
|
+
chainHost: chainInfo2.host
|
|
1054
|
+
});
|
|
1055
|
+
return {
|
|
1056
|
+
type: "signature",
|
|
1057
|
+
description,
|
|
1058
|
+
typeUrl: "fg:t:transaction",
|
|
1059
|
+
origin: (0, import_util.toBase58)(txBuffer),
|
|
1060
|
+
method,
|
|
1061
|
+
display: _WalletAuthenticator.formatDisplay(display),
|
|
1062
|
+
digest: "",
|
|
1063
|
+
chainInfo: chainInfo2,
|
|
1064
|
+
meta,
|
|
1065
|
+
mfa,
|
|
1066
|
+
nonce,
|
|
1067
|
+
requirement
|
|
1068
|
+
};
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
throw new Error(`Failed to encode transaction: ${err.message}`);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
if (type === "fg:t:transaction") {
|
|
1074
|
+
return {
|
|
1075
|
+
type: "signature",
|
|
1076
|
+
description,
|
|
1077
|
+
typeUrl: "fg:t:transaction",
|
|
1078
|
+
origin: (0, import_util.toBase58)(data),
|
|
1079
|
+
display: _WalletAuthenticator.formatDisplay(display),
|
|
1080
|
+
method,
|
|
1081
|
+
digest: "",
|
|
1082
|
+
chainInfo: chainInfo2,
|
|
1083
|
+
meta,
|
|
1084
|
+
mfa,
|
|
1085
|
+
nonce,
|
|
1086
|
+
requirement
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
return {
|
|
1090
|
+
type: "signature",
|
|
1091
|
+
description: desc || "Sign this message to continue.",
|
|
1092
|
+
origin: data ? (0, import_util.toBase58)(data) : "",
|
|
1093
|
+
typeUrl: type,
|
|
1094
|
+
display: _WalletAuthenticator.formatDisplay(display),
|
|
1095
|
+
method,
|
|
1096
|
+
digest,
|
|
1097
|
+
chainInfo: chainInfo2,
|
|
1098
|
+
meta,
|
|
1099
|
+
mfa,
|
|
1100
|
+
nonce,
|
|
1101
|
+
requirement
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
// Request wallet to complete and sign a partial tx to broadcasting
|
|
1105
|
+
// Usually used in payment scenarios
|
|
1106
|
+
// The wallet can leverage multiple input capabilities of the chain
|
|
1107
|
+
async prepareTx({
|
|
1108
|
+
claim,
|
|
1109
|
+
context,
|
|
1110
|
+
extraParams
|
|
1111
|
+
}) {
|
|
1112
|
+
const {
|
|
1113
|
+
partialTx,
|
|
1114
|
+
requirement = { tokens: [], assets: {} },
|
|
1115
|
+
type,
|
|
1116
|
+
display,
|
|
1117
|
+
wallet,
|
|
1118
|
+
sender,
|
|
1119
|
+
description: desc,
|
|
1120
|
+
chainInfo: chainInfo2,
|
|
1121
|
+
meta = {},
|
|
1122
|
+
mfa = false,
|
|
1123
|
+
nonce = ""
|
|
1124
|
+
} = await this.getClaimInfo({
|
|
1125
|
+
claim,
|
|
1126
|
+
context,
|
|
1127
|
+
extraParams
|
|
1128
|
+
});
|
|
1129
|
+
debug2("claim.prepareTx", { partialTx, requirement, type, sender, context });
|
|
1130
|
+
if (!partialTx || !requirement) {
|
|
1131
|
+
throw new Error("prepareTx claim requires both partialTx and requirement to be provided");
|
|
1132
|
+
}
|
|
1133
|
+
const description = desc || "Prepare and sign this transaction to continue.";
|
|
1134
|
+
if (type?.endsWith("Tx")) {
|
|
1135
|
+
if (!chainInfo2.host) {
|
|
1136
|
+
throw new Error("Invalid chainInfo when trying to encoding partial transaction");
|
|
1137
|
+
}
|
|
1138
|
+
if (!this.txEncoder) {
|
|
1139
|
+
throw new Error(
|
|
1140
|
+
`Transaction encoding for "${type}" requires a txEncoder. Pass txEncoder to WalletAuthenticator or install @ocap/client.`
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
if (!partialTx.pk) {
|
|
1144
|
+
partialTx.pk = context.userPk;
|
|
1145
|
+
}
|
|
1146
|
+
try {
|
|
1147
|
+
const txBuffer = await this.txEncoder({
|
|
1148
|
+
type,
|
|
1149
|
+
data: partialTx,
|
|
1150
|
+
wallet: wallet || (0, import_wallet2.fromAddress)(sender || context.userDid),
|
|
1151
|
+
chainHost: chainInfo2.host
|
|
1152
|
+
});
|
|
1153
|
+
return {
|
|
1154
|
+
type: "prepareTx",
|
|
1155
|
+
description,
|
|
1156
|
+
partialTx: (0, import_util.toBase58)(txBuffer),
|
|
1157
|
+
display: _WalletAuthenticator.formatDisplay(display),
|
|
1158
|
+
requirement,
|
|
1159
|
+
chainInfo: chainInfo2,
|
|
1160
|
+
meta,
|
|
1161
|
+
mfa,
|
|
1162
|
+
nonce
|
|
1163
|
+
};
|
|
1164
|
+
} catch (err) {
|
|
1165
|
+
throw new Error(`Failed to encode partial transaction: ${err.message}`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
return {
|
|
1169
|
+
type: "prepareTx",
|
|
1170
|
+
description,
|
|
1171
|
+
partialTx: (0, import_util.toBase58)(partialTx),
|
|
1172
|
+
requirement,
|
|
1173
|
+
display: _WalletAuthenticator.formatDisplay(display),
|
|
1174
|
+
chainInfo: chainInfo2,
|
|
1175
|
+
meta,
|
|
1176
|
+
mfa,
|
|
1177
|
+
nonce
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
_validateAppInfo(info, allowEmpty = false) {
|
|
1181
|
+
if (typeof info === "function") {
|
|
1182
|
+
return info;
|
|
1183
|
+
}
|
|
1184
|
+
if (!info) {
|
|
1185
|
+
if (allowEmpty === false) {
|
|
1186
|
+
throw new Error("Wallet authenticator can not work with invalid appInfo: empty");
|
|
1187
|
+
}
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
const { value, error } = appInfo.validate(info);
|
|
1191
|
+
if (error) {
|
|
1192
|
+
throw new Error(`Wallet authenticator can not work with invalid appInfo: ${error.message}`);
|
|
1193
|
+
}
|
|
1194
|
+
return value;
|
|
1195
|
+
}
|
|
1196
|
+
_isValidChainInfo(x) {
|
|
1197
|
+
const { error } = chainInfo.validate(x);
|
|
1198
|
+
return !error;
|
|
1199
|
+
}
|
|
1200
|
+
tryWithTimeout(asyncFn, label = "") {
|
|
1201
|
+
if (typeof asyncFn !== "function") {
|
|
1202
|
+
throw new Error("asyncFn must be a valid function when calling tryWithTimeout");
|
|
1203
|
+
}
|
|
1204
|
+
const timeout = Number(this.timeout) || DEFAULT_TIMEOUT;
|
|
1205
|
+
const inferredLabel = label || asyncFn.name || asyncFn.toString();
|
|
1206
|
+
const invocationStack = new Error(`Timeout at: ${inferredLabel}`).stack;
|
|
1207
|
+
return new Promise(async (resolve, reject) => {
|
|
1208
|
+
const timer = setTimeout(() => {
|
|
1209
|
+
const error = new Error(`Async operation (${inferredLabel}) did not complete within ${timeout} ms`);
|
|
1210
|
+
error.stack = invocationStack;
|
|
1211
|
+
error.name = "TIMEOUT";
|
|
1212
|
+
reject(error);
|
|
1213
|
+
}, timeout);
|
|
1214
|
+
try {
|
|
1215
|
+
const result = await asyncFn();
|
|
1216
|
+
resolve(result);
|
|
1217
|
+
} catch (err) {
|
|
1218
|
+
reject(err);
|
|
1219
|
+
} finally {
|
|
1220
|
+
clearTimeout(timer);
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
var wallet_default = WalletAuthenticator;
|
|
1226
|
+
|
|
1227
|
+
// src/handlers/wallet.ts
|
|
1228
|
+
var import_debug4 = __toESM(require("debug"), 1);
|
|
1229
|
+
|
|
1230
|
+
// src/adapters/detect.ts
|
|
1231
|
+
function isExpressApp(app) {
|
|
1232
|
+
return typeof app?.set === "function" && typeof app?.engine === "function";
|
|
1233
|
+
}
|
|
1234
|
+
function isHonoApp(app) {
|
|
1235
|
+
return typeof app?.fetch === "function" && !isExpressApp(app);
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/adapters/express.ts
|
|
1239
|
+
function attachExpress({ app, prefix, action, handlers }) {
|
|
1240
|
+
const pathname = `${prefix}/${action}/auth`;
|
|
1241
|
+
const {
|
|
1242
|
+
generateSession,
|
|
1243
|
+
checkSession,
|
|
1244
|
+
expireSession,
|
|
1245
|
+
onAuthRequest,
|
|
1246
|
+
onAuthResponse,
|
|
1247
|
+
ensureContext,
|
|
1248
|
+
ensureSignedJson
|
|
1249
|
+
} = handlers;
|
|
1250
|
+
app.get(`${prefix}/${action}/token`, generateSession);
|
|
1251
|
+
app.post(`${prefix}/${action}/token`, generateSession);
|
|
1252
|
+
app.get(`${prefix}/${action}/status`, ensureContext, checkSession);
|
|
1253
|
+
app.get(`${prefix}/${action}/timeout`, ensureContext, expireSession);
|
|
1254
|
+
app.get(pathname, ensureContext, ensureSignedJson, onAuthRequest);
|
|
1255
|
+
app.post(pathname, ensureContext, ensureSignedJson, onAuthResponse);
|
|
1256
|
+
app.get(`${pathname}/submit`, ensureContext, ensureSignedJson, onAuthResponse);
|
|
1257
|
+
return { generateSession, expireSession, checkSession, onAuthRequest, onAuthResponse };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/adapters/hono.ts
|
|
1261
|
+
function parseCookieHeader(header) {
|
|
1262
|
+
const cookies = {};
|
|
1263
|
+
if (!header) return cookies;
|
|
1264
|
+
for (const pair of header.split(";")) {
|
|
1265
|
+
const idx = pair.indexOf("=");
|
|
1266
|
+
if (idx > 0) {
|
|
1267
|
+
const key = pair.slice(0, idx).trim();
|
|
1268
|
+
const val = pair.slice(idx + 1).trim();
|
|
1269
|
+
cookies[key] = decodeURIComponent(val);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return cookies;
|
|
1273
|
+
}
|
|
1274
|
+
function negotiateLanguage(acceptHeader, ...langs) {
|
|
1275
|
+
if (!acceptHeader || langs.length === 0) return langs[0] || false;
|
|
1276
|
+
const lower = acceptHeader.toLowerCase();
|
|
1277
|
+
for (const lang of langs) {
|
|
1278
|
+
const prefix = lang.toLowerCase().split("-")[0];
|
|
1279
|
+
if (lower.includes(lang.toLowerCase()) || lower.includes(prefix)) {
|
|
1280
|
+
return lang;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return langs[0] || false;
|
|
1284
|
+
}
|
|
1285
|
+
function createHonoRequest(c, bodyCache = {}) {
|
|
1286
|
+
const url2 = new URL(c.req.url);
|
|
1287
|
+
const rawHeaders = {};
|
|
1288
|
+
if (c.req.raw?.headers) {
|
|
1289
|
+
c.req.raw.headers.forEach((value, key) => {
|
|
1290
|
+
rawHeaders[key.toLowerCase()] = value;
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
body: bodyCache,
|
|
1295
|
+
query: Object.fromEntries(url2.searchParams.entries()),
|
|
1296
|
+
params: typeof c.req.param === "function" ? c.req.param() : {},
|
|
1297
|
+
headers: rawHeaders,
|
|
1298
|
+
cookies: parseCookieHeader(rawHeaders.cookie || ""),
|
|
1299
|
+
protocol: url2.protocol.replace(":", ""),
|
|
1300
|
+
originalUrl: url2.pathname + url2.search,
|
|
1301
|
+
get(name) {
|
|
1302
|
+
return rawHeaders[name.toLowerCase()];
|
|
1303
|
+
},
|
|
1304
|
+
acceptsLanguages(...langs) {
|
|
1305
|
+
return negotiateLanguage(rawHeaders["accept-language"] || "", ...langs);
|
|
1306
|
+
},
|
|
1307
|
+
raw: c
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function createHonoResponse() {
|
|
1311
|
+
let result = null;
|
|
1312
|
+
const response = {
|
|
1313
|
+
jsonp(data) {
|
|
1314
|
+
result = { statusCode: 200, body: data };
|
|
1315
|
+
},
|
|
1316
|
+
json(data) {
|
|
1317
|
+
result = { statusCode: 200, body: data };
|
|
1318
|
+
},
|
|
1319
|
+
status(code) {
|
|
1320
|
+
return {
|
|
1321
|
+
json(data) {
|
|
1322
|
+
result = { statusCode: code, body: data };
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
},
|
|
1326
|
+
_getResult() {
|
|
1327
|
+
return result;
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
return response;
|
|
1331
|
+
}
|
|
1332
|
+
function wrapHandler(middlewares, handler) {
|
|
1333
|
+
return async (c) => {
|
|
1334
|
+
let body = {};
|
|
1335
|
+
try {
|
|
1336
|
+
const text = await c.req.text();
|
|
1337
|
+
if (text) {
|
|
1338
|
+
const contentType = c.req.header("content-type") || "";
|
|
1339
|
+
if (contentType.includes("json")) {
|
|
1340
|
+
body = JSON.parse(text);
|
|
1341
|
+
} else if (contentType.includes("urlencoded")) {
|
|
1342
|
+
body = Object.fromEntries(new URLSearchParams(text).entries());
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} catch {
|
|
1346
|
+
}
|
|
1347
|
+
const req = createHonoRequest(c, body);
|
|
1348
|
+
const res = createHonoResponse();
|
|
1349
|
+
for (const mw of middlewares) {
|
|
1350
|
+
let nextCalled = false;
|
|
1351
|
+
await mw(req, res, () => {
|
|
1352
|
+
nextCalled = true;
|
|
1353
|
+
});
|
|
1354
|
+
if (!nextCalled) {
|
|
1355
|
+
const r2 = res._getResult();
|
|
1356
|
+
if (r2) return c.json(r2.body, r2.statusCode);
|
|
1357
|
+
return c.json({ error: "Unknown error" }, 500);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
await handler(req, res);
|
|
1361
|
+
const r = res._getResult();
|
|
1362
|
+
return c.json(r?.body ?? {}, r?.statusCode ?? 200);
|
|
1363
|
+
};
|
|
1364
|
+
}
|
|
1365
|
+
function attachHono({ app, prefix, action, handlers }) {
|
|
1366
|
+
const pathname = `${prefix}/${action}/auth`;
|
|
1367
|
+
const {
|
|
1368
|
+
generateSession,
|
|
1369
|
+
checkSession,
|
|
1370
|
+
expireSession,
|
|
1371
|
+
onAuthRequest,
|
|
1372
|
+
onAuthResponse,
|
|
1373
|
+
ensureContext,
|
|
1374
|
+
ensureSignedJson
|
|
1375
|
+
} = handlers;
|
|
1376
|
+
app.use(`${prefix}/${action}/*`, async (c, next) => {
|
|
1377
|
+
c.header("Access-Control-Allow-Origin", "*");
|
|
1378
|
+
c.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1379
|
+
c.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
1380
|
+
if (c.req.method === "OPTIONS") {
|
|
1381
|
+
return c.text("", 204);
|
|
1382
|
+
}
|
|
1383
|
+
await next();
|
|
1384
|
+
});
|
|
1385
|
+
app.get(`${prefix}/${action}/token`, wrapHandler([], generateSession));
|
|
1386
|
+
app.post(`${prefix}/${action}/token`, wrapHandler([], generateSession));
|
|
1387
|
+
app.get(`${prefix}/${action}/status`, wrapHandler([ensureContext], checkSession));
|
|
1388
|
+
app.get(`${prefix}/${action}/timeout`, wrapHandler([ensureContext], expireSession));
|
|
1389
|
+
app.get(pathname, wrapHandler([ensureContext, ensureSignedJson], onAuthRequest));
|
|
1390
|
+
app.post(pathname, wrapHandler([ensureContext, ensureSignedJson], onAuthResponse));
|
|
1391
|
+
app.get(`${pathname}/submit`, wrapHandler([ensureContext, ensureSignedJson], onAuthResponse));
|
|
1392
|
+
return { generateSession, expireSession, checkSession, onAuthRequest, onAuthResponse };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/handlers/base.ts
|
|
1396
|
+
var import_node_events = require("node:events");
|
|
1397
|
+
|
|
1398
|
+
// src/protocol.ts
|
|
1399
|
+
var import_jwt = require("@arcblock/jwt");
|
|
1400
|
+
var import_aes = __toESM(require("@ocap/mcrypto/lib/crypter/aes"), 1);
|
|
1401
|
+
var import_util2 = require("@ocap/util");
|
|
1402
|
+
var import_did_connect_core = require("@arcblock/did-connect-core");
|
|
1403
|
+
var VERSION = "1.0.0";
|
|
1404
|
+
var decrypt = (data, config = {}, dataKey = "userInfo") => {
|
|
1405
|
+
try {
|
|
1406
|
+
(0, import_jwt.decode)(data[dataKey]);
|
|
1407
|
+
return data;
|
|
1408
|
+
} catch {
|
|
1409
|
+
}
|
|
1410
|
+
if (config.sharedKey && data.version === VERSION) {
|
|
1411
|
+
data[dataKey] = import_aes.default.decrypt((0, import_util2.fromBase58)(data[dataKey]), config.sharedKey, "buffer").toString("utf8");
|
|
1412
|
+
}
|
|
1413
|
+
return data;
|
|
1414
|
+
};
|
|
1415
|
+
var encrypt = (data, config = {}, dataKey = "authInfo") => {
|
|
1416
|
+
const { clientVersion, sharedKey } = config || {};
|
|
1417
|
+
if (data.sensitive && sharedKey && clientVersion === VERSION) {
|
|
1418
|
+
data.version = VERSION;
|
|
1419
|
+
data[dataKey] = import_aes.default.encrypt(data[dataKey], sharedKey, "base58");
|
|
1420
|
+
}
|
|
1421
|
+
delete data.sensitive;
|
|
1422
|
+
return data;
|
|
1423
|
+
};
|
|
1424
|
+
var PROTECTED_KEYS = ["challenge", "nonce", "sharedKey", "encryptionKey"];
|
|
1425
|
+
var SESSION_STATUS = {
|
|
1426
|
+
CREATED: "created",
|
|
1427
|
+
SUCCEED: "succeed",
|
|
1428
|
+
ERROR: "error",
|
|
1429
|
+
BUSY: "busy",
|
|
1430
|
+
SCANNED: "scanned",
|
|
1431
|
+
FORBIDDEN: "forbidden"
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
// src/handlers/base.ts
|
|
1435
|
+
var BaseHandler = class extends import_node_events.EventEmitter {
|
|
1436
|
+
authenticator;
|
|
1437
|
+
tokenStorage;
|
|
1438
|
+
pathTransformer;
|
|
1439
|
+
onConnect;
|
|
1440
|
+
/**
|
|
1441
|
+
* Creates an instance of DID Auth Handlers.
|
|
1442
|
+
*
|
|
1443
|
+
* @class
|
|
1444
|
+
* @param {object} config
|
|
1445
|
+
* @param {function} config.pathTransformer - function to transform path when generate action;
|
|
1446
|
+
* @param {object} config.tokenStorage - function to generate action token
|
|
1447
|
+
* @param {object} config.authenticator - Authenticator instance that can to jwt sign/verify
|
|
1448
|
+
* @param {function} [config.onConnect=noop] - function called when wallet selected did
|
|
1449
|
+
*/
|
|
1450
|
+
constructor({
|
|
1451
|
+
pathTransformer,
|
|
1452
|
+
tokenStorage,
|
|
1453
|
+
authenticator,
|
|
1454
|
+
onConnect
|
|
1455
|
+
}) {
|
|
1456
|
+
super();
|
|
1457
|
+
this.authenticator = authenticator;
|
|
1458
|
+
this.tokenStorage = tokenStorage;
|
|
1459
|
+
this.tokenStorage.on("create", (data) => this.emit("created", data));
|
|
1460
|
+
this.tokenStorage.on("destroy", (token) => this.emit("deleted", { token }));
|
|
1461
|
+
this.tokenStorage.on("update", async (data) => {
|
|
1462
|
+
if (!data) {
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
const payload = await this.tokenStorage.read(data.token);
|
|
1466
|
+
this.emit("updated", omit(payload, PROTECTED_KEYS));
|
|
1467
|
+
});
|
|
1468
|
+
if (typeof pathTransformer === "function") {
|
|
1469
|
+
this.pathTransformer = pathTransformer;
|
|
1470
|
+
} else {
|
|
1471
|
+
this.pathTransformer = (v3) => v3;
|
|
1472
|
+
}
|
|
1473
|
+
if (typeof onConnect === "function") {
|
|
1474
|
+
this.onConnect = onConnect;
|
|
1475
|
+
} else {
|
|
1476
|
+
this.onConnect = () => {
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
};
|
|
1481
|
+
var base_default2 = BaseHandler;
|
|
1482
|
+
|
|
1483
|
+
// src/handlers/util.ts
|
|
1484
|
+
var import_did2 = require("@arcblock/did");
|
|
1485
|
+
var Mcrypto = __toESM(require("@ocap/mcrypto"), 1);
|
|
1486
|
+
var import_util3 = require("@ocap/util");
|
|
1487
|
+
var import_fast_json_stable_stringify = __toESM(require("fast-json-stable-stringify"), 1);
|
|
1488
|
+
var import_url = __toESM(require("url"), 1);
|
|
1489
|
+
|
|
1490
|
+
// src/utils/sealed-box.ts
|
|
1491
|
+
var import_salsa = require("@noble/ciphers/salsa.js");
|
|
1492
|
+
var import_ed25519 = require("@noble/curves/ed25519.js");
|
|
1493
|
+
var import_blake2 = require("@noble/hashes/blake2.js");
|
|
1494
|
+
var import_utils = require("@noble/hashes/utils.js");
|
|
1495
|
+
var SIGMA = new Uint32Array([1634760805, 857760878, 2036477234, 1797285236]);
|
|
1496
|
+
function cryptoBoxBeforenm(sharedSecret) {
|
|
1497
|
+
const k = new Uint32Array(sharedSecret.buffer, sharedSecret.byteOffset, 8);
|
|
1498
|
+
const i = new Uint32Array(4);
|
|
1499
|
+
const out = new Uint32Array(8);
|
|
1500
|
+
(0, import_salsa.hsalsa)(SIGMA, k, i, out);
|
|
1501
|
+
return new Uint8Array(out.buffer);
|
|
1502
|
+
}
|
|
1503
|
+
function seal(message, recipientPk) {
|
|
1504
|
+
const ephSk = import_ed25519.x25519.utils.randomSecretKey();
|
|
1505
|
+
const ephPk = import_ed25519.x25519.getPublicKey(ephSk);
|
|
1506
|
+
const nonce = (0, import_blake2.blake2b)((0, import_utils.concatBytes)(ephPk, recipientPk), { dkLen: 24 });
|
|
1507
|
+
const key = cryptoBoxBeforenm(import_ed25519.x25519.getSharedSecret(ephSk, recipientPk));
|
|
1508
|
+
const encrypted = (0, import_salsa.xsalsa20poly1305)(key, nonce).encrypt(message);
|
|
1509
|
+
return (0, import_utils.concatBytes)(ephPk, encrypted);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/handlers/util.ts
|
|
1513
|
+
var import_debug3 = __toESM(require("debug"), 1);
|
|
1514
|
+
var ABSOLUTE_URL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/;
|
|
1515
|
+
var WINDOWS_PATH_REGEX = /^[a-zA-Z]:\\/;
|
|
1516
|
+
var isUrl = (input) => {
|
|
1517
|
+
if (typeof input !== "string") {
|
|
1518
|
+
return false;
|
|
1519
|
+
}
|
|
1520
|
+
if (WINDOWS_PATH_REGEX.test(input)) {
|
|
1521
|
+
return false;
|
|
1522
|
+
}
|
|
1523
|
+
return ABSOLUTE_URL_REGEX.test(input);
|
|
1524
|
+
};
|
|
1525
|
+
var _debug = (0, import_debug3.default)("@arcblock/did-connect-js:handlers:util");
|
|
1526
|
+
var sha3 = Mcrypto.Hasher.SHA3.hash256;
|
|
1527
|
+
var SUPPORTED_LOCALES = ["en", "zh"];
|
|
1528
|
+
function normalizeLocale(value) {
|
|
1529
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
1530
|
+
if (typeof raw !== "string") return void 0;
|
|
1531
|
+
const base = raw.trim().toLowerCase().replace("_", "-").split("-")[0];
|
|
1532
|
+
return SUPPORTED_LOCALES.includes(base) ? base : void 0;
|
|
1533
|
+
}
|
|
1534
|
+
var getLocale = (req) => {
|
|
1535
|
+
const queryLocale = normalizeLocale(req.query.locale);
|
|
1536
|
+
if (queryLocale) return queryLocale;
|
|
1537
|
+
const cookieLocale = normalizeLocale(req.cookies?.arc_lang);
|
|
1538
|
+
if (cookieLocale) return cookieLocale;
|
|
1539
|
+
const acceptedLocale = normalizeLocale(req.acceptsLanguages("en-US", "zh-CN"));
|
|
1540
|
+
return acceptedLocale || "en";
|
|
1541
|
+
};
|
|
1542
|
+
var getSessionId = () => Date.now().toString();
|
|
1543
|
+
var noop = () => ({});
|
|
1544
|
+
var noTouch = (x) => x;
|
|
1545
|
+
var errors = {
|
|
1546
|
+
tokenMissing: {
|
|
1547
|
+
en: "Session Id is required to check status",
|
|
1548
|
+
zh: "\u7F3A\u5C11\u4F1A\u8BDD ID \u53C2\u6570"
|
|
1549
|
+
},
|
|
1550
|
+
didMismatch: {
|
|
1551
|
+
en: "Login user and wallet user mismatch, please relogin and try again",
|
|
1552
|
+
zh: "\u767B\u5F55\u7528\u6237\u548C\u626B\u7801\u7528\u6237\u4E0D\u5339\u914D\uFF0C\u4E3A\u4FDD\u969C\u5B89\u5168\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u5E94\u7528"
|
|
1553
|
+
},
|
|
1554
|
+
mfaMismatch: {
|
|
1555
|
+
en: "Dynamic verification code mismatch, please try again later",
|
|
1556
|
+
zh: "\u52A8\u6001\u9A8C\u8BC1\u7801\u4E0D\u5339\u914D\uFF0C\u8BF7\u91CD\u8BD5"
|
|
1557
|
+
},
|
|
1558
|
+
challengeMismatch: {
|
|
1559
|
+
en: "Challenge mismatch",
|
|
1560
|
+
zh: "\u968F\u673A\u6821\u9A8C\u7801\u4E0D\u5339\u914D"
|
|
1561
|
+
},
|
|
1562
|
+
token404: {
|
|
1563
|
+
en: "Session not found or expired",
|
|
1564
|
+
zh: "\u4F1A\u8BDD\u4E0D\u5B58\u5728\u6216\u5DF2\u8FC7\u671F"
|
|
1565
|
+
},
|
|
1566
|
+
didMissing: {
|
|
1567
|
+
en: "userDid is required to start auth",
|
|
1568
|
+
zh: "userDid \u53C2\u6570\u7F3A\u5931\uFF0C\u8BF7\u52FF\u5C1D\u8BD5\u8FDE\u63A5\u591A\u4E2A\u4E0D\u540C\u7684\u94B1\u5305"
|
|
1569
|
+
},
|
|
1570
|
+
pkMissing: {
|
|
1571
|
+
en: "userPk is required to start auth",
|
|
1572
|
+
zh: "userPk \u53C2\u6570\u7F3A\u5931\uFF0C\u8BF7\u52FF\u5C1D\u8BD5\u8FDE\u63A5\u591A\u4E2A\u4E0D\u540C\u7684\u94B1\u5305"
|
|
1573
|
+
},
|
|
1574
|
+
authClaim: {
|
|
1575
|
+
en: "authPrincipal claim is not configured correctly",
|
|
1576
|
+
zh: "authPrincipal \u58F0\u660E\u914D\u7F6E\u4E0D\u6B63\u786E"
|
|
1577
|
+
},
|
|
1578
|
+
userDeclined: {
|
|
1579
|
+
en: "You have declined the authentication request",
|
|
1580
|
+
zh: "\u6388\u6743\u8BF7\u6C42\u88AB\u62D2\u7EDD"
|
|
1581
|
+
},
|
|
1582
|
+
userBusy: {
|
|
1583
|
+
en: "Busy processing another DID Connect request",
|
|
1584
|
+
zh: "\u6B63\u5728\u5904\u7406\u5176\u4ED6\u8BF7\u6C42"
|
|
1585
|
+
}
|
|
1586
|
+
};
|
|
1587
|
+
var preparePathname = (path, req) => {
|
|
1588
|
+
const delimiter = path.replace(/\/retrieve$/, "").replace(/\/auth$/, "");
|
|
1589
|
+
const fullPath = import_url.default.parse(req.originalUrl).pathname;
|
|
1590
|
+
const [prefix] = fullPath.split(delimiter);
|
|
1591
|
+
const cleanPath = [prefix, path].join("/").replace(/\/+/g, "/");
|
|
1592
|
+
return cleanPath;
|
|
1593
|
+
};
|
|
1594
|
+
var getBaseUrl = (req) => {
|
|
1595
|
+
if (req.headers["x-path-prefix"]) {
|
|
1596
|
+
return `/${req.headers["x-path-prefix"]}/`.replace(/\/+/g, "/");
|
|
1597
|
+
}
|
|
1598
|
+
return "/";
|
|
1599
|
+
};
|
|
1600
|
+
var prepareBaseUrl = (req, params) => {
|
|
1601
|
+
const pathPrefix = getBaseUrl(req).replace(/\/$/, "");
|
|
1602
|
+
const [hostname = "", port = 80] = (req.get("x-forwarded-host") || req.get("x-real-hostname") || req.get("host") || "").split(":");
|
|
1603
|
+
const finalPort = (params?.["x-real-port"] ?? null) || req.get("X-Real-Port") || port || "";
|
|
1604
|
+
return import_url.default.format({
|
|
1605
|
+
protocol: params?.["x-real-protocol"] || req.get("X-Real-protocol") || req.get("X-Forwarded-Proto") || req.protocol,
|
|
1606
|
+
hostname,
|
|
1607
|
+
port: Number(finalPort) === 80 ? "" : finalPort,
|
|
1608
|
+
pathname: pathPrefix
|
|
1609
|
+
});
|
|
1610
|
+
};
|
|
1611
|
+
var base64UrlUnescape = (str) => (str + "===".slice((str.length + 3) % 4)).replace(/-/g, "+").replace(/_/g, "/");
|
|
1612
|
+
var decodeEncKey = (str) => new Uint8Array(Buffer.from(base64UrlUnescape(str), "base64"));
|
|
1613
|
+
var getStepChallenge = () => (0, import_util3.stripHexPrefix)(Mcrypto.getRandomBytes(16)).toUpperCase();
|
|
1614
|
+
var parseWalletUA = (userAgent) => {
|
|
1615
|
+
const ua = (userAgent || "").toString().toLowerCase();
|
|
1616
|
+
let os = "";
|
|
1617
|
+
let version = "";
|
|
1618
|
+
if (ua.indexOf("android") > -1) {
|
|
1619
|
+
os = "android";
|
|
1620
|
+
} else if (ua.indexOf("iphone") > -1) {
|
|
1621
|
+
os = "ios";
|
|
1622
|
+
} else if (ua.indexOf("ipad") > -1) {
|
|
1623
|
+
os = "ios";
|
|
1624
|
+
} else if (ua.indexOf("ipod") > -1) {
|
|
1625
|
+
os = "ios";
|
|
1626
|
+
} else if (ua.indexOf("arcwallet") === 0) {
|
|
1627
|
+
os = "web";
|
|
1628
|
+
} else if (ua.indexOf("abtwallet") === 0) {
|
|
1629
|
+
os = "web";
|
|
1630
|
+
}
|
|
1631
|
+
const match = ua.split(/\s+/).find((x) => x.startsWith("arcwallet/") || x.startsWith("abtwallet/"));
|
|
1632
|
+
if (match) {
|
|
1633
|
+
const tmp = match.split("/");
|
|
1634
|
+
const coerced = tmp.length > 1 ? (tmp[1].match(/(\d+\.\d+\.\d+)/) || [])[1] : void 0;
|
|
1635
|
+
if (coerced) {
|
|
1636
|
+
version = coerced;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return { os, version, jwt: "1.1.0" };
|
|
1640
|
+
};
|
|
1641
|
+
var isDeepLink = (str) => str.startsWith("https://abtwallet.io/i/") || str.startsWith("https://didwallet.io/i/");
|
|
1642
|
+
var isConnectedOnly = (params, sessionUserDid = "") => {
|
|
1643
|
+
if (typeof params.forceConnected === "string" && (0, import_did2.isValid)(params.forceConnected)) {
|
|
1644
|
+
return params.forceConnected;
|
|
1645
|
+
}
|
|
1646
|
+
if (!sessionUserDid) {
|
|
1647
|
+
return false;
|
|
1648
|
+
}
|
|
1649
|
+
if ((0, import_did2.isValid)(sessionUserDid) === false) {
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
if (params.connectedDid !== sessionUserDid) {
|
|
1653
|
+
return false;
|
|
1654
|
+
}
|
|
1655
|
+
if (typeof params.forceConnected === "undefined") {
|
|
1656
|
+
return true;
|
|
1657
|
+
}
|
|
1658
|
+
if (typeof params.forceConnected === "string") {
|
|
1659
|
+
try {
|
|
1660
|
+
return !!JSON.parse(params.forceConnected);
|
|
1661
|
+
} catch {
|
|
1662
|
+
return false;
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return !!params.forceConnected;
|
|
1666
|
+
};
|
|
1667
|
+
function createHandlers({
|
|
1668
|
+
action,
|
|
1669
|
+
pathname,
|
|
1670
|
+
claims: claims2,
|
|
1671
|
+
onStart,
|
|
1672
|
+
onConnect,
|
|
1673
|
+
onAuth,
|
|
1674
|
+
onDecline,
|
|
1675
|
+
onComplete,
|
|
1676
|
+
onExpire,
|
|
1677
|
+
onError,
|
|
1678
|
+
pathTransformer,
|
|
1679
|
+
tokenStorage,
|
|
1680
|
+
authenticator,
|
|
1681
|
+
authPrincipal,
|
|
1682
|
+
persistentDynamicClaims = false,
|
|
1683
|
+
getSignParams = noop,
|
|
1684
|
+
getPathName = noTouch,
|
|
1685
|
+
options
|
|
1686
|
+
}) {
|
|
1687
|
+
const { tokenKey, encKey, versionKey, cleanupDelay } = options;
|
|
1688
|
+
const defaultSteps = (Array.isArray(claims2) ? claims2 : [claims2]).filter(Boolean);
|
|
1689
|
+
if (defaultSteps.length > 0) {
|
|
1690
|
+
const keys = Object.keys(defaultSteps[0]);
|
|
1691
|
+
const firstClaim = defaultSteps[0][keys[0]];
|
|
1692
|
+
if (Array.isArray(firstClaim)) {
|
|
1693
|
+
if (firstClaim[0] === "authPrincipal") {
|
|
1694
|
+
authPrincipal = false;
|
|
1695
|
+
}
|
|
1696
|
+
} else if (keys[0] === "authPrincipal") {
|
|
1697
|
+
authPrincipal = false;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
if (authPrincipal) {
|
|
1701
|
+
let target = "";
|
|
1702
|
+
let description = "Please continue with your account";
|
|
1703
|
+
let chainInfo2;
|
|
1704
|
+
let targetType;
|
|
1705
|
+
if (typeof authPrincipal === "string") {
|
|
1706
|
+
if ((0, import_did2.isValid)(authPrincipal)) {
|
|
1707
|
+
target = authPrincipal;
|
|
1708
|
+
} else {
|
|
1709
|
+
description = authPrincipal;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
if (typeof authPrincipal === "object") {
|
|
1713
|
+
target = authPrincipal.target ?? target;
|
|
1714
|
+
description = authPrincipal.description ?? description;
|
|
1715
|
+
targetType = authPrincipal.targetType ?? targetType;
|
|
1716
|
+
if (authPrincipal.chainInfo && authenticator._isValidChainInfo(authPrincipal.chainInfo)) {
|
|
1717
|
+
chainInfo2 = authPrincipal.chainInfo;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
const supervised = defaultSteps.length === 0;
|
|
1721
|
+
defaultSteps.unshift({
|
|
1722
|
+
authPrincipal: {
|
|
1723
|
+
skippable: true,
|
|
1724
|
+
description,
|
|
1725
|
+
target,
|
|
1726
|
+
chainInfo: chainInfo2,
|
|
1727
|
+
targetType,
|
|
1728
|
+
supervised
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
const canSkipConnect = defaultSteps[0]?.authPrincipal?.skippable;
|
|
1733
|
+
const createExtraParams = (locale, params, extra = {}) => {
|
|
1734
|
+
const finalParams = { ...params, ...extra || {} };
|
|
1735
|
+
return {
|
|
1736
|
+
locale,
|
|
1737
|
+
action,
|
|
1738
|
+
...Object.keys(finalParams).filter((x) => !["userDid", "userInfo", "userSession", "appSession", "userPk", "token"].includes(x)).reduce((obj, x) => {
|
|
1739
|
+
obj[x] = finalParams[x];
|
|
1740
|
+
return obj;
|
|
1741
|
+
}, {})
|
|
1742
|
+
};
|
|
1743
|
+
};
|
|
1744
|
+
const createSessionUpdater = (token, params) => async (key, value, secure = false) => {
|
|
1745
|
+
const getUpdate = (k, v3) => {
|
|
1746
|
+
if (secure && params[encKey]) {
|
|
1747
|
+
const encrypted = seal(Buffer.from((0, import_fast_json_stable_stringify.default)(v3)), decodeEncKey(params[encKey]));
|
|
1748
|
+
return { [k]: Buffer.from(encrypted).toString("base64") };
|
|
1749
|
+
}
|
|
1750
|
+
return { [k]: v3 };
|
|
1751
|
+
};
|
|
1752
|
+
if (typeof key === "object") {
|
|
1753
|
+
secure = value;
|
|
1754
|
+
const keys = Object.keys(key);
|
|
1755
|
+
const updates = Object.assign({}, ...keys.map((k) => getUpdate(k, key[k])));
|
|
1756
|
+
return tokenStorage.update(token, updates);
|
|
1757
|
+
}
|
|
1758
|
+
return tokenStorage.update(token, omit(getUpdate(key, value), PROTECTED_KEYS));
|
|
1759
|
+
};
|
|
1760
|
+
const createMfaCodeGenerator = (token) => async () => {
|
|
1761
|
+
const mfaCode = random(10, 99);
|
|
1762
|
+
await tokenStorage.update(token, { mfaCode });
|
|
1763
|
+
return mfaCode;
|
|
1764
|
+
};
|
|
1765
|
+
const onProcessError = ({
|
|
1766
|
+
req,
|
|
1767
|
+
res,
|
|
1768
|
+
stage,
|
|
1769
|
+
err
|
|
1770
|
+
}) => {
|
|
1771
|
+
const { token, store } = req.context || {};
|
|
1772
|
+
if (token) {
|
|
1773
|
+
tokenStorage.update(token, { status: SESSION_STATUS.ERROR, error: err.message, mfaCode: 0 });
|
|
1774
|
+
}
|
|
1775
|
+
res.jsonp({ error: err.message });
|
|
1776
|
+
onError({ token, extraParams: store?.extraParams ?? {}, stage, err });
|
|
1777
|
+
};
|
|
1778
|
+
const _preparePathname = (str, req) => {
|
|
1779
|
+
const auto = preparePathname(str, req);
|
|
1780
|
+
const custom = pathTransformer(auto);
|
|
1781
|
+
return custom;
|
|
1782
|
+
};
|
|
1783
|
+
const generateSession = async (req, res) => {
|
|
1784
|
+
try {
|
|
1785
|
+
const params = {
|
|
1786
|
+
"x-real-port": req.get("x-real-port"),
|
|
1787
|
+
"x-real-protocol": req.get("x-real-protocol"),
|
|
1788
|
+
deviceDid: req.get("x-device-did"),
|
|
1789
|
+
connectedDid: req.cookies?.connected_did ?? "",
|
|
1790
|
+
connectedPk: req.cookies?.connected_pk ?? "",
|
|
1791
|
+
...req.body,
|
|
1792
|
+
...req.query,
|
|
1793
|
+
...req.params
|
|
1794
|
+
};
|
|
1795
|
+
params.forceConnected = isConnectedOnly(params, req.get("x-user-did"));
|
|
1796
|
+
const token = sha3(getSessionId()).replace(/^0x/, "").slice(0, 8);
|
|
1797
|
+
await tokenStorage.create(token, SESSION_STATUS.CREATED);
|
|
1798
|
+
let sourceToken = params.sourceToken || "";
|
|
1799
|
+
const sourceTokenState = sourceToken ? await tokenStorage.read(sourceToken) : null;
|
|
1800
|
+
if (sourceTokenState) {
|
|
1801
|
+
if ([SESSION_STATUS.SUCCEED].includes(sourceTokenState.status)) {
|
|
1802
|
+
sourceToken = "";
|
|
1803
|
+
} else {
|
|
1804
|
+
await tokenStorage.update(sourceToken, { destToken: token });
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
const finalPath = _preparePathname(getPathName(pathname, req), req);
|
|
1808
|
+
const baseUrl = prepareBaseUrl(req, params);
|
|
1809
|
+
const uri = await authenticator.uri({ token, pathname: finalPath, baseUrl, query: {} });
|
|
1810
|
+
const challenge = getStepChallenge();
|
|
1811
|
+
const didwallet = parseWalletUA(req.query["user-agent"] || req.headers["user-agent"]);
|
|
1812
|
+
const extraParams = createExtraParams(getLocale(req), params);
|
|
1813
|
+
const hookParams = {
|
|
1814
|
+
req,
|
|
1815
|
+
request: req,
|
|
1816
|
+
challenge,
|
|
1817
|
+
baseUrl,
|
|
1818
|
+
deepLink: uri,
|
|
1819
|
+
extraParams,
|
|
1820
|
+
updateSession: createSessionUpdater(token, extraParams),
|
|
1821
|
+
didwallet
|
|
1822
|
+
};
|
|
1823
|
+
const [wallet, delegator] = await Promise.all([
|
|
1824
|
+
authenticator.getWalletInfo({ baseUrl, request: req, extraParams }),
|
|
1825
|
+
authenticator.getDelegator({ baseUrl, request: req, extraParams })
|
|
1826
|
+
]);
|
|
1827
|
+
const [appInfo2, memberAppInfo] = await Promise.all([
|
|
1828
|
+
authenticator.getAppInfo({ baseUrl, request: req, wallet, delegator, extraParams }, "appInfo"),
|
|
1829
|
+
authenticator.getAppInfo({ baseUrl, request: req, wallet, delegator, extraParams }, "memberAppInfo")
|
|
1830
|
+
]);
|
|
1831
|
+
await tokenStorage.update(token, {
|
|
1832
|
+
currentStep: 0,
|
|
1833
|
+
mfaSupported: !didwallet.os,
|
|
1834
|
+
// If we are mobile (both webview and mobile browsers , os is truthy), mfa should be disabled
|
|
1835
|
+
challenge,
|
|
1836
|
+
sharedKey: getStepChallenge(),
|
|
1837
|
+
// used for wallet to encrypt userInfo
|
|
1838
|
+
extraParams: params,
|
|
1839
|
+
appInfo: appInfo2,
|
|
1840
|
+
memberAppInfo,
|
|
1841
|
+
sourceToken
|
|
1842
|
+
});
|
|
1843
|
+
const extra = await onStart(hookParams);
|
|
1844
|
+
res.jsonp({ token, status: SESSION_STATUS.CREATED, url: uri, appInfo: appInfo2, memberAppInfo, extra: extra || {} });
|
|
1845
|
+
} catch (err) {
|
|
1846
|
+
onProcessError({ req, res, stage: "generate-token", err });
|
|
1847
|
+
}
|
|
1848
|
+
};
|
|
1849
|
+
const checkSession = async (req, res) => {
|
|
1850
|
+
try {
|
|
1851
|
+
const { locale, token, store, params } = req.context;
|
|
1852
|
+
if (!token) {
|
|
1853
|
+
res.status(400).json({ error: errors.tokenMissing[locale] });
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (!store) {
|
|
1857
|
+
res.status(400).json({ error: errors.token404[locale] });
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
if (store.status === SESSION_STATUS.FORBIDDEN) {
|
|
1861
|
+
res.status(403).json({ error: errors.didMismatch[locale] });
|
|
1862
|
+
return;
|
|
1863
|
+
}
|
|
1864
|
+
if (store.status === SESSION_STATUS.SUCCEED) {
|
|
1865
|
+
setTimeout(() => {
|
|
1866
|
+
tokenStorage.delete(token).catch(console.error);
|
|
1867
|
+
}, cleanupDelay);
|
|
1868
|
+
const extraParams = createExtraParams(locale, params, store?.extraParams ?? {});
|
|
1869
|
+
await onComplete({
|
|
1870
|
+
req,
|
|
1871
|
+
request: req,
|
|
1872
|
+
userDid: store.did,
|
|
1873
|
+
userPk: store.pk,
|
|
1874
|
+
extraParams,
|
|
1875
|
+
updateSession: createSessionUpdater(token, extraParams)
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
res.status(200).json(
|
|
1879
|
+
Object.keys(store).filter((x) => PROTECTED_KEYS.includes(x) === false).reduce((acc, key) => {
|
|
1880
|
+
acc[key] = store[key];
|
|
1881
|
+
return acc;
|
|
1882
|
+
}, {})
|
|
1883
|
+
);
|
|
1884
|
+
} catch (err) {
|
|
1885
|
+
onProcessError({ req, res, stage: "check-token-status", err });
|
|
1886
|
+
}
|
|
1887
|
+
};
|
|
1888
|
+
const expireSession = async (req, res) => {
|
|
1889
|
+
try {
|
|
1890
|
+
const { locale, token, store } = req.context;
|
|
1891
|
+
if (!token) {
|
|
1892
|
+
res.status(400).json({ error: errors.tokenMissing[locale] });
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
if (!store) {
|
|
1896
|
+
res.status(400).json({ error: errors.token404[locale] });
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
onExpire({ token, extraParams: store?.extraParams ?? {}, status: "expired" });
|
|
1900
|
+
if (store.status !== SESSION_STATUS.SCANNED) {
|
|
1901
|
+
await tokenStorage.delete(token);
|
|
1902
|
+
}
|
|
1903
|
+
res.status(200).json({ token });
|
|
1904
|
+
} catch (err) {
|
|
1905
|
+
onProcessError({ req, res, stage: "mark-token-timeout", err });
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
const checkUser = async ({ context, userDid, userPk }) => {
|
|
1909
|
+
const { locale, token, store } = context;
|
|
1910
|
+
const isConnected = store.currentStep > 0;
|
|
1911
|
+
if (isConnected) {
|
|
1912
|
+
if (!userDid) {
|
|
1913
|
+
return errors.didMissing[locale];
|
|
1914
|
+
}
|
|
1915
|
+
if (!userPk) {
|
|
1916
|
+
return errors.pkMissing[locale];
|
|
1917
|
+
}
|
|
1918
|
+
if (userDid !== store.did) {
|
|
1919
|
+
await tokenStorage.update(token, { status: SESSION_STATUS.FORBIDDEN });
|
|
1920
|
+
return errors.didMismatch[locale];
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
return false;
|
|
1924
|
+
};
|
|
1925
|
+
const onAuthRequest = async (req, res) => {
|
|
1926
|
+
const { locale, token, store, params, didwallet } = req.context;
|
|
1927
|
+
const extraParams = createExtraParams(locale, params, store?.extraParams ?? {});
|
|
1928
|
+
const userDid = params.userDid || store.did || extraParams.connectedDid;
|
|
1929
|
+
const userPk = params.userPk || store.pk || extraParams.connectedPk;
|
|
1930
|
+
const error = await checkUser({ context: req.context, userDid, userPk });
|
|
1931
|
+
if (error) {
|
|
1932
|
+
return res.jsonp({ error });
|
|
1933
|
+
}
|
|
1934
|
+
if (params[versionKey] && store.clientVersion !== params[versionKey]) {
|
|
1935
|
+
store.clientVersion = params[versionKey];
|
|
1936
|
+
store.encryptionKey = params[encKey];
|
|
1937
|
+
await tokenStorage.update(token, { clientVersion: params[versionKey], encryptionKey: params[encKey] });
|
|
1938
|
+
}
|
|
1939
|
+
try {
|
|
1940
|
+
const steps = [...cloneDeep(defaultSteps)];
|
|
1941
|
+
const shouldSkipConnect = canSkipConnect && !!extraParams.connectedDid;
|
|
1942
|
+
if (shouldSkipConnect) {
|
|
1943
|
+
steps[0].authPrincipal.supervised = false;
|
|
1944
|
+
}
|
|
1945
|
+
if (extraParams.forceConnected) {
|
|
1946
|
+
let target = extraParams.connectedDid;
|
|
1947
|
+
if (typeof extraParams.forceConnected === "string" && (0, import_did2.isValid)(extraParams.forceConnected)) {
|
|
1948
|
+
target = extraParams.forceConnected;
|
|
1949
|
+
}
|
|
1950
|
+
if ((0, import_did2.isValid)(target)) {
|
|
1951
|
+
steps[0].authPrincipal.target = target;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
if (store.status !== SESSION_STATUS.SCANNED) {
|
|
1955
|
+
await tokenStorage.update(token, { status: SESSION_STATUS.SCANNED, connectedWallet: didwallet });
|
|
1956
|
+
}
|
|
1957
|
+
if (store.dynamic || shouldSkipConnect) {
|
|
1958
|
+
const newClaims = await onConnect({
|
|
1959
|
+
req,
|
|
1960
|
+
request: req,
|
|
1961
|
+
userDid,
|
|
1962
|
+
userPk,
|
|
1963
|
+
didwallet,
|
|
1964
|
+
challenge: store.challenge,
|
|
1965
|
+
pathname: _preparePathname(getPathName(pathname, req), req),
|
|
1966
|
+
baseUrl: prepareBaseUrl(req, extraParams),
|
|
1967
|
+
extraParams,
|
|
1968
|
+
updateSession: createSessionUpdater(token, extraParams)
|
|
1969
|
+
});
|
|
1970
|
+
if (newClaims) {
|
|
1971
|
+
if (Array.isArray(newClaims)) {
|
|
1972
|
+
steps.push(...newClaims);
|
|
1973
|
+
} else {
|
|
1974
|
+
steps.push(newClaims);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
const signParams = await getSignParams(req);
|
|
1979
|
+
const signedClaim = await authenticator.sign(
|
|
1980
|
+
Object.assign(signParams, {
|
|
1981
|
+
context: {
|
|
1982
|
+
token,
|
|
1983
|
+
userDid,
|
|
1984
|
+
userPk,
|
|
1985
|
+
didwallet,
|
|
1986
|
+
...pick(store, ["currentStep", "sharedKey", "encryptionKey"]),
|
|
1987
|
+
mfaCode: store.mfaSupported ? createMfaCodeGenerator(token) : void 0
|
|
1988
|
+
},
|
|
1989
|
+
claims: steps[store.currentStep],
|
|
1990
|
+
pathname: _preparePathname(getPathName(pathname, req), req),
|
|
1991
|
+
baseUrl: prepareBaseUrl(req, extraParams),
|
|
1992
|
+
extraParams,
|
|
1993
|
+
challenge: store.challenge,
|
|
1994
|
+
appInfo: store.appInfo,
|
|
1995
|
+
memberAppInfo: store.memberAppInfo,
|
|
1996
|
+
request: req
|
|
1997
|
+
})
|
|
1998
|
+
);
|
|
1999
|
+
res.jsonp(encrypt(signedClaim, store));
|
|
2000
|
+
} catch (err) {
|
|
2001
|
+
onProcessError({ req, res, stage: "send-auth-claim", err });
|
|
2002
|
+
}
|
|
2003
|
+
};
|
|
2004
|
+
const onAuthResponse = async (req, res) => {
|
|
2005
|
+
const { locale, token, store, params, didwallet } = req.context;
|
|
2006
|
+
try {
|
|
2007
|
+
const {
|
|
2008
|
+
userDid,
|
|
2009
|
+
userPk,
|
|
2010
|
+
action: userAction,
|
|
2011
|
+
challenge: userChallenge,
|
|
2012
|
+
claims: claimResponse,
|
|
2013
|
+
timestamp
|
|
2014
|
+
} = await authenticator.verify(decrypt(params, store), locale);
|
|
2015
|
+
if (!store.did || !store.pk) {
|
|
2016
|
+
await tokenStorage.update(token, { did: userDid, pk: userPk });
|
|
2017
|
+
}
|
|
2018
|
+
const extraParams = createExtraParams(locale, params, store?.extraParams ?? {});
|
|
2019
|
+
const cbParams = {
|
|
2020
|
+
step: store.currentStep,
|
|
2021
|
+
req,
|
|
2022
|
+
request: req,
|
|
2023
|
+
userDid,
|
|
2024
|
+
userPk,
|
|
2025
|
+
challenge: store.challenge,
|
|
2026
|
+
didwallet,
|
|
2027
|
+
claims: claimResponse,
|
|
2028
|
+
baseUrl: prepareBaseUrl(req, extraParams),
|
|
2029
|
+
extraParams,
|
|
2030
|
+
updateSession: createSessionUpdater(token, extraParams),
|
|
2031
|
+
timestamp
|
|
2032
|
+
};
|
|
2033
|
+
const steps = [...defaultSteps];
|
|
2034
|
+
const shouldSkipConnect = canSkipConnect && !!extraParams.connectedDid;
|
|
2035
|
+
if (store.dynamic || shouldSkipConnect) {
|
|
2036
|
+
const newClaims = await onConnect(cbParams);
|
|
2037
|
+
if (newClaims) {
|
|
2038
|
+
if (Array.isArray(newClaims)) {
|
|
2039
|
+
steps.push(...newClaims);
|
|
2040
|
+
} else {
|
|
2041
|
+
steps.push(newClaims);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
} else if (persistentDynamicClaims && Array.isArray(store.dynamicClaims)) {
|
|
2045
|
+
steps.push(...store.dynamicClaims);
|
|
2046
|
+
}
|
|
2047
|
+
if (userAction === "declineAuth") {
|
|
2048
|
+
await tokenStorage.update(token, {
|
|
2049
|
+
status: SESSION_STATUS.ERROR,
|
|
2050
|
+
error: errors.userDeclined[locale],
|
|
2051
|
+
mfaCode: 0,
|
|
2052
|
+
currentStep: steps.length - 1
|
|
2053
|
+
});
|
|
2054
|
+
const result = await onDecline(cbParams);
|
|
2055
|
+
return res.jsonp({ ...result || {} });
|
|
2056
|
+
}
|
|
2057
|
+
if (userAction === "busy") {
|
|
2058
|
+
await tokenStorage.update(token, {
|
|
2059
|
+
status: SESSION_STATUS.BUSY,
|
|
2060
|
+
error: errors.userBusy[locale],
|
|
2061
|
+
mfaCode: 0,
|
|
2062
|
+
currentStep: steps.length - 1
|
|
2063
|
+
});
|
|
2064
|
+
return res.jsonp({});
|
|
2065
|
+
}
|
|
2066
|
+
if (store.mfaCode && !claimResponse.some((x) => isEqual(x.mfaCode, [store.mfaCode]))) {
|
|
2067
|
+
return onProcessError({ req, res, stage: "verify-mfa-code", err: new Error(errors.mfaMismatch[locale]) });
|
|
2068
|
+
}
|
|
2069
|
+
if (!userChallenge) {
|
|
2070
|
+
return res.jsonp({ error: errors.challengeMismatch[locale] });
|
|
2071
|
+
}
|
|
2072
|
+
if (userChallenge !== store.challenge) {
|
|
2073
|
+
return res.jsonp({ error: errors.challengeMismatch[locale] });
|
|
2074
|
+
}
|
|
2075
|
+
const error = await checkUser({ context: req.context, userDid, userPk });
|
|
2076
|
+
if (error) {
|
|
2077
|
+
return res.jsonp({ error });
|
|
2078
|
+
}
|
|
2079
|
+
const isConnected = store.currentStep > 0;
|
|
2080
|
+
if (isConnected === false) {
|
|
2081
|
+
const newClaims = await onConnect(cbParams);
|
|
2082
|
+
if (newClaims) {
|
|
2083
|
+
await tokenStorage.update(token, {
|
|
2084
|
+
dynamic: !persistentDynamicClaims,
|
|
2085
|
+
dynamicClaims: persistentDynamicClaims ? newClaims : void 0
|
|
2086
|
+
});
|
|
2087
|
+
if (Array.isArray(newClaims)) {
|
|
2088
|
+
steps.push(...newClaims);
|
|
2089
|
+
} else {
|
|
2090
|
+
steps.push(newClaims);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
const onLastStep = async (result) => {
|
|
2095
|
+
let nextWorkflow = isUrl(extraParams.nw) ? extraParams.nw : "";
|
|
2096
|
+
if (nextWorkflow && result?.nextWorkflowData) {
|
|
2097
|
+
if (isPlainObject(result.nextWorkflowData) === false) {
|
|
2098
|
+
const err = new Error(`expect nextWorkflowData should be a plain object, got: ${result.nextWorkflowData}`);
|
|
2099
|
+
return onProcessError({ req, res, stage: "validate-next-workflow-data", err });
|
|
2100
|
+
}
|
|
2101
|
+
const tmp = new URL(nextWorkflow);
|
|
2102
|
+
const merged = Object.assign(extraParams.previousWorkflowData || {}, result.nextWorkflowData);
|
|
2103
|
+
const previousWorkflowData = (0, import_util3.toBase64)(JSON.stringify(merged));
|
|
2104
|
+
if (previousWorkflowData.length > 8192) {
|
|
2105
|
+
const err = new Error("base64 encoded nextWorkflowData should be less than 8192 characters");
|
|
2106
|
+
return onProcessError({ req, res, stage: "append-next-workflow", err });
|
|
2107
|
+
}
|
|
2108
|
+
if (isDeepLink(nextWorkflow)) {
|
|
2109
|
+
const actualUrl = decodeURIComponent(tmp.searchParams.get("url"));
|
|
2110
|
+
const obj = new URL(actualUrl);
|
|
2111
|
+
obj.searchParams.set("previousWorkflowData", previousWorkflowData);
|
|
2112
|
+
tmp.searchParams.set("url", obj.href);
|
|
2113
|
+
} else {
|
|
2114
|
+
tmp.searchParams.set("previousWorkflowData", previousWorkflowData);
|
|
2115
|
+
}
|
|
2116
|
+
nextWorkflow = tmp.href;
|
|
2117
|
+
}
|
|
2118
|
+
const updates = {};
|
|
2119
|
+
let actualNw = nextWorkflow || result?.nextWorkflow || "";
|
|
2120
|
+
if (actualNw) {
|
|
2121
|
+
if (isDeepLink(actualNw)) {
|
|
2122
|
+
actualNw = new URL(actualNw).searchParams.get("url");
|
|
2123
|
+
}
|
|
2124
|
+
if (actualNw) {
|
|
2125
|
+
updates.nextWorkflow = decodeURIComponent(actualNw);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
if (result?.nextToken && result.nextWorkflow) {
|
|
2129
|
+
try {
|
|
2130
|
+
await tokenStorage.update(result.nextToken, { prevToken: token });
|
|
2131
|
+
} catch (err) {
|
|
2132
|
+
console.error("DIDAuth: failed to to update nextToken", err);
|
|
2133
|
+
updates.status = SESSION_STATUS.SUCCEED;
|
|
2134
|
+
}
|
|
2135
|
+
} else {
|
|
2136
|
+
if (store.prevToken) {
|
|
2137
|
+
try {
|
|
2138
|
+
await tokenStorage.update(store.prevToken, { status: SESSION_STATUS.SUCCEED });
|
|
2139
|
+
} catch (err) {
|
|
2140
|
+
console.error("DIDAuth: failed to to update prevToken", err);
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
updates.status = SESSION_STATUS.SUCCEED;
|
|
2144
|
+
}
|
|
2145
|
+
await tokenStorage.update(token, updates);
|
|
2146
|
+
return res.jsonp({ ...Object.assign({ nextWorkflow }, result || {}) });
|
|
2147
|
+
};
|
|
2148
|
+
if (steps.length === 1) {
|
|
2149
|
+
const result = await onAuth(cbParams);
|
|
2150
|
+
return onLastStep(result);
|
|
2151
|
+
}
|
|
2152
|
+
if (isConnected && store.currentStep < steps.length) {
|
|
2153
|
+
const result = await onAuth(cbParams);
|
|
2154
|
+
const isLastStep = store.currentStep === steps.length - 1;
|
|
2155
|
+
if (isLastStep) {
|
|
2156
|
+
return onLastStep(result);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
const nextStep = store.currentStep + 1;
|
|
2160
|
+
const nextChallenge = getStepChallenge();
|
|
2161
|
+
await tokenStorage.update(token, { currentStep: nextStep, challenge: nextChallenge, mfaCode: 0 });
|
|
2162
|
+
const signParams = await getSignParams(req);
|
|
2163
|
+
try {
|
|
2164
|
+
const nextSignedClaim = await authenticator.sign(
|
|
2165
|
+
Object.assign(signParams, {
|
|
2166
|
+
context: {
|
|
2167
|
+
token,
|
|
2168
|
+
userDid,
|
|
2169
|
+
userPk,
|
|
2170
|
+
didwallet,
|
|
2171
|
+
...pick(store, ["currentStep", "sharedKey", "encryptionKey"]),
|
|
2172
|
+
mfaCode: store.mfaSupported ? createMfaCodeGenerator(token) : void 0
|
|
2173
|
+
},
|
|
2174
|
+
claims: steps[nextStep],
|
|
2175
|
+
pathname: _preparePathname(getPathName(pathname, req), req),
|
|
2176
|
+
baseUrl: prepareBaseUrl(req, extraParams),
|
|
2177
|
+
extraParams,
|
|
2178
|
+
challenge: nextChallenge,
|
|
2179
|
+
appInfo: store.appInfo,
|
|
2180
|
+
memberAppInfo: store.memberAppInfo,
|
|
2181
|
+
request: req
|
|
2182
|
+
})
|
|
2183
|
+
);
|
|
2184
|
+
return res.jsonp(encrypt(nextSignedClaim, store));
|
|
2185
|
+
} catch (err) {
|
|
2186
|
+
return onProcessError({ req, res, stage: "next-auth-claim", err });
|
|
2187
|
+
}
|
|
2188
|
+
} catch (err) {
|
|
2189
|
+
onProcessError({ req, res, stage: "verify-auth-claim", err });
|
|
2190
|
+
}
|
|
2191
|
+
};
|
|
2192
|
+
const ensureContext = async (req, _res, next) => {
|
|
2193
|
+
const didwallet = parseWalletUA(req.query["user-agent"] || req.headers["user-agent"]);
|
|
2194
|
+
const params = { ...req.body, ...req.query, ...req.params };
|
|
2195
|
+
const token = params[tokenKey];
|
|
2196
|
+
const locale = getLocale(req);
|
|
2197
|
+
let store = null;
|
|
2198
|
+
if (token) {
|
|
2199
|
+
store = await tokenStorage.read(token);
|
|
2200
|
+
if (params.previousWorkflowData) {
|
|
2201
|
+
try {
|
|
2202
|
+
store.extraParams.previousWorkflowData = JSON.parse((0, import_util3.fromBase64)(params.previousWorkflowData).toString());
|
|
2203
|
+
await tokenStorage.update(token, { extraParams: store.extraParams });
|
|
2204
|
+
} catch (e) {
|
|
2205
|
+
console.warn("Could not parse previousWorkflowData", params.previousWorkflowData, e);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (store?.destToken && typeof params.notrace === "undefined") {
|
|
2209
|
+
const result = await tokenStorage.read(store.destToken);
|
|
2210
|
+
if (result) {
|
|
2211
|
+
store = result;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
}
|
|
2215
|
+
req.context = { locale, token, didwallet, params, store };
|
|
2216
|
+
return next();
|
|
2217
|
+
};
|
|
2218
|
+
const ensureSignedJson = (req, res, next) => {
|
|
2219
|
+
if (req.ensureSignedJson === void 0) {
|
|
2220
|
+
req.ensureSignedJson = true;
|
|
2221
|
+
const originJsonp = res.jsonp;
|
|
2222
|
+
res.jsonp = async (payload) => {
|
|
2223
|
+
if (payload.appPk && payload.authInfo) {
|
|
2224
|
+
return originJsonp.call(res, payload);
|
|
2225
|
+
}
|
|
2226
|
+
const data = payload.response ? { response: payload.response } : { response: payload };
|
|
2227
|
+
const fields = ["error", "errorMessage", "successMessage", "nextWorkflow", "nextUrl", "cookies", "storages"];
|
|
2228
|
+
fields.forEach((x) => {
|
|
2229
|
+
if (payload[x]) {
|
|
2230
|
+
data[x] = payload[x];
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
data.errorMessage = data.error || data.errorMessage || "";
|
|
2234
|
+
if (typeof data.response === "object") {
|
|
2235
|
+
data.response = omit(data.response, fields);
|
|
2236
|
+
}
|
|
2237
|
+
const params = { ...req.body, ...req.query, ...req.params };
|
|
2238
|
+
const token2 = params[tokenKey];
|
|
2239
|
+
const store2 = token2 ? await tokenStorage.read(token2) : null;
|
|
2240
|
+
const extraParams = store2?.extraParams ?? {};
|
|
2241
|
+
const signedData = await authenticator.signResponse(data, prepareBaseUrl(req, extraParams), req, extraParams);
|
|
2242
|
+
originJsonp.call(res, encrypt(signedData, store2));
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
const { token, store, locale } = req.context;
|
|
2246
|
+
if (!token || !store) {
|
|
2247
|
+
return res.jsonp({ error: errors.token404[locale] });
|
|
2248
|
+
}
|
|
2249
|
+
next();
|
|
2250
|
+
};
|
|
2251
|
+
return {
|
|
2252
|
+
generateSession,
|
|
2253
|
+
expireSession,
|
|
2254
|
+
checkSession,
|
|
2255
|
+
onAuthRequest,
|
|
2256
|
+
onAuthResponse,
|
|
2257
|
+
ensureContext,
|
|
2258
|
+
ensureSignedJson,
|
|
2259
|
+
createExtraParams
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// src/handlers/wallet.ts
|
|
2264
|
+
var debug3 = (0, import_debug4.default)("@arcblock/did-connect-js:handlers:wallet");
|
|
2265
|
+
var noop2 = () => {
|
|
2266
|
+
};
|
|
2267
|
+
var WalletHandlers = class extends base_default2 {
|
|
2268
|
+
options;
|
|
2269
|
+
/**
|
|
2270
|
+
* Creates an instance of DID Auth Handlers.
|
|
2271
|
+
*
|
|
2272
|
+
* @class
|
|
2273
|
+
* @param {object} config
|
|
2274
|
+
* @param {object} config.tokenStorage - function to generate action token
|
|
2275
|
+
* @param {object} config.authenticator - Authenticator instance that can to jwt sign/verify
|
|
2276
|
+
* @param {function} [config.pathTransformer=null] - how should we update pathname
|
|
2277
|
+
* @param {function} [config.onConnect=noop] - function called before each auth request send back to app, used to check for permission, throw error to halt the auth process
|
|
2278
|
+
* @param {object} [config.options={}] - custom options to define all handlers attached
|
|
2279
|
+
* @param {string} [config.options.prefix='/api/did'] - url prefix for this group endpoints
|
|
2280
|
+
* @param {number} [config.options.cleanupDelay=60000] - how long to wait before cleanup finished session
|
|
2281
|
+
* @param {string} [config.options.tokenKey='_t_'] - query param key for `token`
|
|
2282
|
+
* @param {string} [config.options.encKey='_ek_'] - query param key for encryption key
|
|
2283
|
+
* @param {string} [config.options.versionKey='_v_'] - query param key for protocol `version`
|
|
2284
|
+
*/
|
|
2285
|
+
constructor({
|
|
2286
|
+
pathTransformer,
|
|
2287
|
+
tokenStorage,
|
|
2288
|
+
authenticator,
|
|
2289
|
+
onConnect = noop2,
|
|
2290
|
+
options = {}
|
|
2291
|
+
}) {
|
|
2292
|
+
super({ pathTransformer, tokenStorage, authenticator, onConnect });
|
|
2293
|
+
this.options = {
|
|
2294
|
+
prefix: "/api/did",
|
|
2295
|
+
cleanupDelay: 6e4,
|
|
2296
|
+
tokenKey: "_t_",
|
|
2297
|
+
encKey: "_ek_",
|
|
2298
|
+
versionKey: "_v_",
|
|
2299
|
+
...options
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
/**
|
|
2303
|
+
* Attach routes and handlers for authenticator
|
|
2304
|
+
* Now express app have route handlers attached to the following url
|
|
2305
|
+
* - `GET /api/did/{action}/token` create new token
|
|
2306
|
+
* - `GET /api/did/{action}/status` check for token status
|
|
2307
|
+
* - `GET /api/did/{action}/timeout` expire a token
|
|
2308
|
+
* - `GET /api/did/{action}/auth` create auth response
|
|
2309
|
+
* - `POST /api/did/{action}/auth` process payment request
|
|
2310
|
+
*
|
|
2311
|
+
* @method
|
|
2312
|
+
* @param {object} config
|
|
2313
|
+
* @param {object} config.app - express instance to attach routes to
|
|
2314
|
+
* @param {object} [config.claims] - claims for this request
|
|
2315
|
+
* @param {string} config.action - action of this group of routes
|
|
2316
|
+
* @param {function} [config.onStart=noop] - callback when a new action start
|
|
2317
|
+
* @param {function} [config.onConnect=noop] - callback when a new action start
|
|
2318
|
+
* @param {function} config.onAuth - callback when user completed auth in DID Wallet, and data posted back
|
|
2319
|
+
* @param {function} [config.onDecline=noop] - callback when user has declined in wallet
|
|
2320
|
+
* @param {function} [config.onComplete=noop] - callback when the whole auth process is done, action token is removed
|
|
2321
|
+
* @param {function} [config.onExpire=noop] - callback when the action token expired
|
|
2322
|
+
* @param {function} [config.onError=console.error] - callback when there are some errors
|
|
2323
|
+
* @param {boolean|string|did} [config.authPrincipal=true] - whether should we do auth principal claim first
|
|
2324
|
+
* @param {boolean} [config.persistentDynamicClaims=false] - whether should we persist dynamic claims
|
|
2325
|
+
* @return void
|
|
2326
|
+
*/
|
|
2327
|
+
attach({
|
|
2328
|
+
app,
|
|
2329
|
+
action,
|
|
2330
|
+
claims: claims2 = void 0,
|
|
2331
|
+
onStart = noop2,
|
|
2332
|
+
onConnect,
|
|
2333
|
+
onAuth,
|
|
2334
|
+
onDecline = noop2,
|
|
2335
|
+
onComplete = noop2,
|
|
2336
|
+
onExpire = noop2,
|
|
2337
|
+
onError = console.error,
|
|
2338
|
+
authPrincipal = true,
|
|
2339
|
+
persistentDynamicClaims = false
|
|
2340
|
+
}) {
|
|
2341
|
+
if (typeof onAuth !== "function") {
|
|
2342
|
+
throw new Error("onAuth callback is required to attach did auth handlers");
|
|
2343
|
+
}
|
|
2344
|
+
if (typeof onDecline !== "function") {
|
|
2345
|
+
throw new Error("onDecline callback is required to attach did auth handlers");
|
|
2346
|
+
}
|
|
2347
|
+
if (typeof onComplete !== "function") {
|
|
2348
|
+
throw new Error("onComplete callback is required to attach did auth handlers");
|
|
2349
|
+
}
|
|
2350
|
+
const { prefix } = this.options;
|
|
2351
|
+
const pathname = `${prefix}/${action}/auth`;
|
|
2352
|
+
debug3("attach routes", { action, prefix, pathname });
|
|
2353
|
+
const onConnectWrapped = async (...args) => {
|
|
2354
|
+
if (typeof this.onConnect === "function") {
|
|
2355
|
+
await this.onConnect(...args);
|
|
2356
|
+
}
|
|
2357
|
+
if (typeof onConnect === "function") {
|
|
2358
|
+
return onConnect(...args);
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
const {
|
|
2362
|
+
generateSession,
|
|
2363
|
+
expireSession,
|
|
2364
|
+
checkSession,
|
|
2365
|
+
onAuthRequest,
|
|
2366
|
+
onAuthResponse,
|
|
2367
|
+
ensureContext,
|
|
2368
|
+
ensureSignedJson
|
|
2369
|
+
} = createHandlers({
|
|
2370
|
+
action,
|
|
2371
|
+
pathname,
|
|
2372
|
+
claims: claims2,
|
|
2373
|
+
onStart,
|
|
2374
|
+
onConnect: onConnectWrapped,
|
|
2375
|
+
// must be deterministic when returning dynamic claims
|
|
2376
|
+
onAuth,
|
|
2377
|
+
onDecline,
|
|
2378
|
+
onComplete,
|
|
2379
|
+
onExpire,
|
|
2380
|
+
onError,
|
|
2381
|
+
authPrincipal,
|
|
2382
|
+
persistentDynamicClaims,
|
|
2383
|
+
options: this.options,
|
|
2384
|
+
pathTransformer: this.pathTransformer,
|
|
2385
|
+
tokenStorage: this.tokenStorage,
|
|
2386
|
+
authenticator: this.authenticator
|
|
2387
|
+
});
|
|
2388
|
+
const handlerFns = {
|
|
2389
|
+
generateSession,
|
|
2390
|
+
checkSession,
|
|
2391
|
+
expireSession,
|
|
2392
|
+
onAuthRequest,
|
|
2393
|
+
onAuthResponse,
|
|
2394
|
+
ensureContext,
|
|
2395
|
+
ensureSignedJson
|
|
2396
|
+
};
|
|
2397
|
+
if (isHonoApp(app)) {
|
|
2398
|
+
return attachHono({ app, prefix, action, handlers: handlerFns });
|
|
2399
|
+
}
|
|
2400
|
+
return attachExpress({ app, prefix, action, handlers: handlerFns });
|
|
2401
|
+
}
|
|
2402
|
+
};
|
|
2403
|
+
var wallet_default2 = WalletHandlers;
|
|
2404
|
+
|
|
2405
|
+
// src/storage/kv.ts
|
|
2406
|
+
var import_events = require("events");
|
|
2407
|
+
var CloudflareKVStorage = class extends import_events.EventEmitter {
|
|
2408
|
+
kv;
|
|
2409
|
+
ttl;
|
|
2410
|
+
prefix;
|
|
2411
|
+
constructor(kv, options = {}) {
|
|
2412
|
+
super();
|
|
2413
|
+
this.kv = kv;
|
|
2414
|
+
this.ttl = options.ttl ?? 300;
|
|
2415
|
+
this.prefix = options.prefix ?? "";
|
|
2416
|
+
}
|
|
2417
|
+
key(token) {
|
|
2418
|
+
return `${this.prefix}${token}`;
|
|
2419
|
+
}
|
|
2420
|
+
async create(token, status = "created") {
|
|
2421
|
+
const record = { token, status };
|
|
2422
|
+
await this.kv.put(this.key(token), JSON.stringify(record), { expirationTtl: this.ttl });
|
|
2423
|
+
this.emit("create", record);
|
|
2424
|
+
return record;
|
|
2425
|
+
}
|
|
2426
|
+
async read(token) {
|
|
2427
|
+
const raw = await this.kv.get(this.key(token));
|
|
2428
|
+
if (!raw) return null;
|
|
2429
|
+
return JSON.parse(raw);
|
|
2430
|
+
}
|
|
2431
|
+
async update(token, updates) {
|
|
2432
|
+
const existing = await this.read(token);
|
|
2433
|
+
if (!existing) return null;
|
|
2434
|
+
delete updates.token;
|
|
2435
|
+
const merged = { ...existing, ...updates };
|
|
2436
|
+
await this.kv.put(this.key(token), JSON.stringify(merged), { expirationTtl: this.ttl });
|
|
2437
|
+
this.emit("update", merged);
|
|
2438
|
+
return merged;
|
|
2439
|
+
}
|
|
2440
|
+
async delete(token) {
|
|
2441
|
+
const existing = await this.read(token);
|
|
2442
|
+
if (existing) {
|
|
2443
|
+
this.emit("destroy", existing);
|
|
2444
|
+
}
|
|
2445
|
+
await this.kv.delete(this.key(token));
|
|
2446
|
+
}
|
|
2447
|
+
async exist(token, did) {
|
|
2448
|
+
const record = await this.read(token);
|
|
2449
|
+
if (!record) return false;
|
|
2450
|
+
if (did) return record.did === did;
|
|
2451
|
+
return true;
|
|
2452
|
+
}
|
|
2453
|
+
};
|
|
2454
|
+
var kv_default = CloudflareKVStorage;
|
|
2455
|
+
|
|
2456
|
+
// src/index.ts
|
|
2457
|
+
var import_did_connect_core2 = require("@arcblock/did-connect-core");
|
|
2458
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2459
|
+
0 && (module.exports = {
|
|
2460
|
+
CloudflareKVStorage,
|
|
2461
|
+
VERSION,
|
|
2462
|
+
WalletAuthenticator,
|
|
2463
|
+
WalletHandlers,
|
|
2464
|
+
attachExpress,
|
|
2465
|
+
attachHono,
|
|
2466
|
+
createHonoRequest,
|
|
2467
|
+
createHonoResponse,
|
|
2468
|
+
isExpressApp,
|
|
2469
|
+
isHonoApp,
|
|
2470
|
+
parseWalletUA,
|
|
2471
|
+
prepareBaseUrl
|
|
2472
|
+
});
|