@wordkey/sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +6 -0
- package/README.md +103 -0
- package/dist/chunk-JWCQBSIQ.mjs +13 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +862 -0
- package/dist/index.mjs +821 -0
- package/dist/server.d.mts +48 -0
- package/dist/server.d.ts +48 -0
- package/dist/server.js +106 -0
- package/dist/server.mjs +80 -0
- package/dist/types-Be6QFOjD.d.mts +49 -0
- package/dist/types-Be6QFOjD.d.ts +49 -0
- package/package.json +60 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { W as WordKeyUser, b as AuthEvent } from './types-Be6QFOjD.mjs';
|
|
2
|
+
export { A as Address, a as WordKeyError, c as WordKeyProfile } from './types-Be6QFOjD.mjs';
|
|
3
|
+
|
|
4
|
+
interface WordKeyConfig {
|
|
5
|
+
/** Your SECRET key (wk_sec_...). Server-only — never ship it to a browser. */
|
|
6
|
+
secretKey: string;
|
|
7
|
+
/** Override the protocol host (defaults to app.wordkey.app). */
|
|
8
|
+
apiUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Server-side WordKey client. Businesses use this in their backend to verify
|
|
12
|
+
* tokens received from the WordKeyButton and to check webhook signatures.
|
|
13
|
+
* Authenticated with the SECRET key — keep it out of client bundles.
|
|
14
|
+
*/
|
|
15
|
+
declare class WordKey {
|
|
16
|
+
private secretKey;
|
|
17
|
+
private apiUrl;
|
|
18
|
+
constructor(config: WordKeyConfig);
|
|
19
|
+
private post;
|
|
20
|
+
/** Verify a single-use token from the client. Throws if invalid/expired/used. */
|
|
21
|
+
verify(token: string): Promise<WordKeyUser>;
|
|
22
|
+
/** Check whether a WordKey user id exists. */
|
|
23
|
+
userExists(userId: string): Promise<boolean>;
|
|
24
|
+
/** Recent authentication events for a user at this business. */
|
|
25
|
+
getUserEvents(userId: string): Promise<AuthEvent[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Verify a webhook signature. `signature` is the X-WordKey-Signature header;
|
|
28
|
+
* `payload` is the raw request body. Signed with your SECRET key (HMAC-SHA256).
|
|
29
|
+
*/
|
|
30
|
+
verifyWebhook(payload: string, signature: string): Promise<boolean>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type WordKeyWebhookEvent = {
|
|
34
|
+
type: "auth.success";
|
|
35
|
+
user: WordKeyUser;
|
|
36
|
+
isNewUser: boolean;
|
|
37
|
+
} | {
|
|
38
|
+
type: "auth.failed";
|
|
39
|
+
reason: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: "user.word_changed";
|
|
42
|
+
userId: string;
|
|
43
|
+
} | {
|
|
44
|
+
type: "user.disconnected";
|
|
45
|
+
userId: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { AuthEvent, WordKey, type WordKeyConfig, WordKeyUser, type WordKeyWebhookEvent };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { W as WordKeyUser, b as AuthEvent } from './types-Be6QFOjD.js';
|
|
2
|
+
export { A as Address, a as WordKeyError, c as WordKeyProfile } from './types-Be6QFOjD.js';
|
|
3
|
+
|
|
4
|
+
interface WordKeyConfig {
|
|
5
|
+
/** Your SECRET key (wk_sec_...). Server-only — never ship it to a browser. */
|
|
6
|
+
secretKey: string;
|
|
7
|
+
/** Override the protocol host (defaults to app.wordkey.app). */
|
|
8
|
+
apiUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Server-side WordKey client. Businesses use this in their backend to verify
|
|
12
|
+
* tokens received from the WordKeyButton and to check webhook signatures.
|
|
13
|
+
* Authenticated with the SECRET key — keep it out of client bundles.
|
|
14
|
+
*/
|
|
15
|
+
declare class WordKey {
|
|
16
|
+
private secretKey;
|
|
17
|
+
private apiUrl;
|
|
18
|
+
constructor(config: WordKeyConfig);
|
|
19
|
+
private post;
|
|
20
|
+
/** Verify a single-use token from the client. Throws if invalid/expired/used. */
|
|
21
|
+
verify(token: string): Promise<WordKeyUser>;
|
|
22
|
+
/** Check whether a WordKey user id exists. */
|
|
23
|
+
userExists(userId: string): Promise<boolean>;
|
|
24
|
+
/** Recent authentication events for a user at this business. */
|
|
25
|
+
getUserEvents(userId: string): Promise<AuthEvent[]>;
|
|
26
|
+
/**
|
|
27
|
+
* Verify a webhook signature. `signature` is the X-WordKey-Signature header;
|
|
28
|
+
* `payload` is the raw request body. Signed with your SECRET key (HMAC-SHA256).
|
|
29
|
+
*/
|
|
30
|
+
verifyWebhook(payload: string, signature: string): Promise<boolean>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type WordKeyWebhookEvent = {
|
|
34
|
+
type: "auth.success";
|
|
35
|
+
user: WordKeyUser;
|
|
36
|
+
isNewUser: boolean;
|
|
37
|
+
} | {
|
|
38
|
+
type: "auth.failed";
|
|
39
|
+
reason: string;
|
|
40
|
+
} | {
|
|
41
|
+
type: "user.word_changed";
|
|
42
|
+
userId: string;
|
|
43
|
+
} | {
|
|
44
|
+
type: "user.disconnected";
|
|
45
|
+
userId: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export { AuthEvent, WordKey, type WordKeyConfig, WordKeyUser, type WordKeyWebhookEvent };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
WordKey: () => WordKey
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(server_exports);
|
|
26
|
+
|
|
27
|
+
// src/types.ts
|
|
28
|
+
var DEFAULT_API_URL = "https://app.wordkey.app";
|
|
29
|
+
|
|
30
|
+
// src/server.ts
|
|
31
|
+
var WordKey = class {
|
|
32
|
+
secretKey;
|
|
33
|
+
apiUrl;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
if (!config.secretKey) throw new Error("WordKey: secretKey is required");
|
|
36
|
+
this.secretKey = config.secretKey;
|
|
37
|
+
this.apiUrl = config.apiUrl ?? DEFAULT_API_URL;
|
|
38
|
+
}
|
|
39
|
+
async post(path, body) {
|
|
40
|
+
const res = await fetch(`${this.apiUrl}${path}`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: {
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
Authorization: `Bearer ${this.secretKey}`
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify(body)
|
|
47
|
+
});
|
|
48
|
+
const json = await res.json();
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new Error(json?.error?.message ?? `WordKey request failed (${res.status})`);
|
|
51
|
+
}
|
|
52
|
+
return json;
|
|
53
|
+
}
|
|
54
|
+
/** Verify a single-use token from the client. Throws if invalid/expired/used. */
|
|
55
|
+
async verify(token) {
|
|
56
|
+
const res = await this.post("/api/auth/token-verify", { token });
|
|
57
|
+
return {
|
|
58
|
+
id: res.userId,
|
|
59
|
+
isNewUser: res.isNewUser,
|
|
60
|
+
authenticatedAt: res.authenticatedAt
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Check whether a WordKey user id exists. */
|
|
64
|
+
async userExists(userId) {
|
|
65
|
+
const res = await this.post("/api/sdk/user-exists", {
|
|
66
|
+
userId
|
|
67
|
+
});
|
|
68
|
+
return res.exists;
|
|
69
|
+
}
|
|
70
|
+
/** Recent authentication events for a user at this business. */
|
|
71
|
+
async getUserEvents(userId) {
|
|
72
|
+
const res = await this.post("/api/sdk/user-events", {
|
|
73
|
+
userId
|
|
74
|
+
});
|
|
75
|
+
return res.events;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Verify a webhook signature. `signature` is the X-WordKey-Signature header;
|
|
79
|
+
* `payload` is the raw request body. Signed with your SECRET key (HMAC-SHA256).
|
|
80
|
+
*/
|
|
81
|
+
async verifyWebhook(payload, signature) {
|
|
82
|
+
const key = await crypto.subtle.importKey(
|
|
83
|
+
"raw",
|
|
84
|
+
new TextEncoder().encode(this.secretKey),
|
|
85
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
86
|
+
false,
|
|
87
|
+
["sign"]
|
|
88
|
+
);
|
|
89
|
+
const sig = await crypto.subtle.sign(
|
|
90
|
+
"HMAC",
|
|
91
|
+
key,
|
|
92
|
+
new TextEncoder().encode(payload)
|
|
93
|
+
);
|
|
94
|
+
const expected = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
95
|
+
if (expected.length !== signature.length) return false;
|
|
96
|
+
let diff = 0;
|
|
97
|
+
for (let i = 0; i < expected.length; i++) {
|
|
98
|
+
diff |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
|
|
99
|
+
}
|
|
100
|
+
return diff === 0;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
104
|
+
0 && (module.exports = {
|
|
105
|
+
WordKey
|
|
106
|
+
});
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_API_URL
|
|
3
|
+
} from "./chunk-JWCQBSIQ.mjs";
|
|
4
|
+
|
|
5
|
+
// src/server.ts
|
|
6
|
+
var WordKey = class {
|
|
7
|
+
secretKey;
|
|
8
|
+
apiUrl;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
if (!config.secretKey) throw new Error("WordKey: secretKey is required");
|
|
11
|
+
this.secretKey = config.secretKey;
|
|
12
|
+
this.apiUrl = config.apiUrl ?? DEFAULT_API_URL;
|
|
13
|
+
}
|
|
14
|
+
async post(path, body) {
|
|
15
|
+
const res = await fetch(`${this.apiUrl}${path}`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${this.secretKey}`
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify(body)
|
|
22
|
+
});
|
|
23
|
+
const json = await res.json();
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
throw new Error(json?.error?.message ?? `WordKey request failed (${res.status})`);
|
|
26
|
+
}
|
|
27
|
+
return json;
|
|
28
|
+
}
|
|
29
|
+
/** Verify a single-use token from the client. Throws if invalid/expired/used. */
|
|
30
|
+
async verify(token) {
|
|
31
|
+
const res = await this.post("/api/auth/token-verify", { token });
|
|
32
|
+
return {
|
|
33
|
+
id: res.userId,
|
|
34
|
+
isNewUser: res.isNewUser,
|
|
35
|
+
authenticatedAt: res.authenticatedAt
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Check whether a WordKey user id exists. */
|
|
39
|
+
async userExists(userId) {
|
|
40
|
+
const res = await this.post("/api/sdk/user-exists", {
|
|
41
|
+
userId
|
|
42
|
+
});
|
|
43
|
+
return res.exists;
|
|
44
|
+
}
|
|
45
|
+
/** Recent authentication events for a user at this business. */
|
|
46
|
+
async getUserEvents(userId) {
|
|
47
|
+
const res = await this.post("/api/sdk/user-events", {
|
|
48
|
+
userId
|
|
49
|
+
});
|
|
50
|
+
return res.events;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Verify a webhook signature. `signature` is the X-WordKey-Signature header;
|
|
54
|
+
* `payload` is the raw request body. Signed with your SECRET key (HMAC-SHA256).
|
|
55
|
+
*/
|
|
56
|
+
async verifyWebhook(payload, signature) {
|
|
57
|
+
const key = await crypto.subtle.importKey(
|
|
58
|
+
"raw",
|
|
59
|
+
new TextEncoder().encode(this.secretKey),
|
|
60
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
61
|
+
false,
|
|
62
|
+
["sign"]
|
|
63
|
+
);
|
|
64
|
+
const sig = await crypto.subtle.sign(
|
|
65
|
+
"HMAC",
|
|
66
|
+
key,
|
|
67
|
+
new TextEncoder().encode(payload)
|
|
68
|
+
);
|
|
69
|
+
const expected = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
70
|
+
if (expected.length !== signature.length) return false;
|
|
71
|
+
let diff = 0;
|
|
72
|
+
for (let i = 0; i < expected.length; i++) {
|
|
73
|
+
diff |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
|
|
74
|
+
}
|
|
75
|
+
return diff === 0;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
export {
|
|
79
|
+
WordKey
|
|
80
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
interface Address {
|
|
2
|
+
line1?: string;
|
|
3
|
+
line2?: string;
|
|
4
|
+
city?: string;
|
|
5
|
+
state?: string;
|
|
6
|
+
postalCode?: string;
|
|
7
|
+
country?: string;
|
|
8
|
+
}
|
|
9
|
+
interface WordKeyProfile {
|
|
10
|
+
name?: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
phone?: string;
|
|
13
|
+
shippingAddress?: Address;
|
|
14
|
+
/** Site-defined custom fields, keyed by the field key the site requested. */
|
|
15
|
+
custom?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
/** Standard profile fields a business may request via `requestedFields`. */
|
|
18
|
+
type ProfileField = "name" | "email" | "phone" | "shipping_address";
|
|
19
|
+
/** A site-defined custom field. Stored on device keyed by `key`, reusable. */
|
|
20
|
+
interface CustomField {
|
|
21
|
+
key: string;
|
|
22
|
+
label: string;
|
|
23
|
+
type: "text" | "date" | "number" | "select";
|
|
24
|
+
required?: boolean;
|
|
25
|
+
/** Options for `type: "select"`. */
|
|
26
|
+
options?: string[];
|
|
27
|
+
}
|
|
28
|
+
/** Each requested field is either a standard field name or a custom descriptor. */
|
|
29
|
+
type RequestedField = ProfileField | CustomField;
|
|
30
|
+
interface WordKeyUser {
|
|
31
|
+
/** Permanent unique WordKey user id. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** True on the user's first-ever authentication (i.e. account creation). */
|
|
34
|
+
isNewUser: boolean;
|
|
35
|
+
authenticatedAt: string;
|
|
36
|
+
/** Present only when requestedFields were granted; unfilled fields omitted. */
|
|
37
|
+
profile?: WordKeyProfile;
|
|
38
|
+
}
|
|
39
|
+
interface WordKeyError {
|
|
40
|
+
code: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
interface AuthEvent {
|
|
44
|
+
type: string;
|
|
45
|
+
deviceType: string | null;
|
|
46
|
+
at: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type { Address as A, CustomField as C, ProfileField as P, RequestedField as R, WordKeyUser as W, WordKeyError as a, AuthEvent as b, WordKeyProfile as c };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
interface Address {
|
|
2
|
+
line1?: string;
|
|
3
|
+
line2?: string;
|
|
4
|
+
city?: string;
|
|
5
|
+
state?: string;
|
|
6
|
+
postalCode?: string;
|
|
7
|
+
country?: string;
|
|
8
|
+
}
|
|
9
|
+
interface WordKeyProfile {
|
|
10
|
+
name?: string;
|
|
11
|
+
email?: string;
|
|
12
|
+
phone?: string;
|
|
13
|
+
shippingAddress?: Address;
|
|
14
|
+
/** Site-defined custom fields, keyed by the field key the site requested. */
|
|
15
|
+
custom?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
/** Standard profile fields a business may request via `requestedFields`. */
|
|
18
|
+
type ProfileField = "name" | "email" | "phone" | "shipping_address";
|
|
19
|
+
/** A site-defined custom field. Stored on device keyed by `key`, reusable. */
|
|
20
|
+
interface CustomField {
|
|
21
|
+
key: string;
|
|
22
|
+
label: string;
|
|
23
|
+
type: "text" | "date" | "number" | "select";
|
|
24
|
+
required?: boolean;
|
|
25
|
+
/** Options for `type: "select"`. */
|
|
26
|
+
options?: string[];
|
|
27
|
+
}
|
|
28
|
+
/** Each requested field is either a standard field name or a custom descriptor. */
|
|
29
|
+
type RequestedField = ProfileField | CustomField;
|
|
30
|
+
interface WordKeyUser {
|
|
31
|
+
/** Permanent unique WordKey user id. */
|
|
32
|
+
id: string;
|
|
33
|
+
/** True on the user's first-ever authentication (i.e. account creation). */
|
|
34
|
+
isNewUser: boolean;
|
|
35
|
+
authenticatedAt: string;
|
|
36
|
+
/** Present only when requestedFields were granted; unfilled fields omitted. */
|
|
37
|
+
profile?: WordKeyProfile;
|
|
38
|
+
}
|
|
39
|
+
interface WordKeyError {
|
|
40
|
+
code: string;
|
|
41
|
+
message: string;
|
|
42
|
+
}
|
|
43
|
+
interface AuthEvent {
|
|
44
|
+
type: string;
|
|
45
|
+
deviceType: string | null;
|
|
46
|
+
at: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type { Address as A, CustomField as C, ProfileField as P, RequestedField as R, WordKeyUser as W, WordKeyError as a, AuthEvent as b, WordKeyProfile as c };
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wordkey/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Add the Word? button to your site in one afternoon. One word. Every door.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./server": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/server.d.mts",
|
|
23
|
+
"default": "./dist/server.mjs"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/server.d.ts",
|
|
27
|
+
"default": "./dist/server.js"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"homepage": "https://wordkey.app",
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"prepublishOnly": "npm run build",
|
|
41
|
+
"dev": "tsup --watch",
|
|
42
|
+
"typecheck": "tsc --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": ">=18",
|
|
46
|
+
"react-dom": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/qrcode": "^1.5.5",
|
|
50
|
+
"@types/react": "^19.0.0",
|
|
51
|
+
"@types/react-dom": "^19.0.0",
|
|
52
|
+
"react": "^19.0.0",
|
|
53
|
+
"react-dom": "^19.0.0",
|
|
54
|
+
"tsup": "^8.3.0",
|
|
55
|
+
"typescript": "^5.7.0"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"qrcode": "^1.5.4"
|
|
59
|
+
}
|
|
60
|
+
}
|