@armory-sh/extensions 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -0
- package/dist/bazaar.js +103 -0
- package/dist/index.js +557 -0
- package/dist/payment-identifier.js +134 -0
- package/dist/sign-in-with-x.js +236 -0
- package/package.json +59 -0
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# @armory-sh/extensions
|
|
2
|
+
|
|
3
|
+
Protocol extensions for the x402 payment standard.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @armory-sh/extensions
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Available Extensions
|
|
12
|
+
|
|
13
|
+
| Extension | Purpose |
|
|
14
|
+
|-----------|---------|
|
|
15
|
+
| Sign-In-With-X | Wallet authentication for repeat access |
|
|
16
|
+
| Payment Identifier | Idempotency for payment requests |
|
|
17
|
+
| Bazaar | Resource discovery |
|
|
18
|
+
|
|
19
|
+
## Hook Creators
|
|
20
|
+
|
|
21
|
+
### createSIWxHook
|
|
22
|
+
|
|
23
|
+
Creates a hook that handles Sign-In-With-X authentication:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createSIWxHook } from '@armory-sh/extensions';
|
|
27
|
+
import { createX402Client } from '@armory-sh/client-viem';
|
|
28
|
+
|
|
29
|
+
const client = createX402Client({
|
|
30
|
+
wallet: { type: 'account', account },
|
|
31
|
+
hooks: {
|
|
32
|
+
siwx: createSIWxHook({
|
|
33
|
+
domain: 'example.com',
|
|
34
|
+
statement: 'Sign in to access premium content'
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### createPaymentIdHook
|
|
41
|
+
|
|
42
|
+
Creates a hook that adds payment idempotency:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { createPaymentIdHook } from '@armory-sh/extensions';
|
|
46
|
+
|
|
47
|
+
const hook = createPaymentIdHook({ paymentId: 'my-custom-id' });
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### createCustomHook
|
|
51
|
+
|
|
52
|
+
Create custom extension hooks:
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { createCustomHook } from '@armory-sh/extensions';
|
|
56
|
+
|
|
57
|
+
const customHook = createCustomHook({
|
|
58
|
+
key: 'my-extension',
|
|
59
|
+
handler: async (context) => {
|
|
60
|
+
if (context.payload) {
|
|
61
|
+
context.payload.extensions = {
|
|
62
|
+
...(context.payload.extensions ?? {}),
|
|
63
|
+
'my-extension': { data: 'value' }
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
priority: 75
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Server Extension Declaration
|
|
72
|
+
|
|
73
|
+
For server-side middleware:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { declareSIWxExtension, declarePaymentIdentifierExtension } from '@armory-sh/extensions';
|
|
77
|
+
|
|
78
|
+
const siwxExt = declareSIWxExtension({
|
|
79
|
+
domain: 'api.example.com',
|
|
80
|
+
statement: 'Sign in to access this API'
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const paymentIdExt = declarePaymentIdentifierExtension({ required: true });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Exports
|
|
87
|
+
|
|
88
|
+
| Export | Description |
|
|
89
|
+
|--------|-------------|
|
|
90
|
+
| `createSIWxHook` | Hook for Sign-In-With-X |
|
|
91
|
+
| `createPaymentIdHook` | Hook for payment idempotency |
|
|
92
|
+
| `createCustomHook` | Create custom hooks |
|
|
93
|
+
| `declareSIWxExtension` | Declare SIWX extension for server |
|
|
94
|
+
| `declarePaymentIdentifierExtension` | Declare payment ID extension |
|
|
95
|
+
| `validateSIWxMessage` | Validate SIWX payload |
|
|
96
|
+
| `verifySIWxSignature` | Verify SIWX signature |
|
|
97
|
+
| `generatePaymentId` | Generate random payment ID |
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
package/dist/bazaar.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/validators.ts
|
|
2
|
+
function extractExtension(extensions, key) {
|
|
3
|
+
if (!extensions || typeof extensions !== "object") {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const extension = extensions[key];
|
|
7
|
+
if (!extension || typeof extension !== "object") {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return extension;
|
|
11
|
+
}
|
|
12
|
+
function createExtension(info, schema) {
|
|
13
|
+
return { info, schema };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// src/types.ts
|
|
17
|
+
var BAZAAR = "bazaar";
|
|
18
|
+
|
|
19
|
+
// src/bazaar.ts
|
|
20
|
+
var BAZAAR_SCHEMA = {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {
|
|
23
|
+
input: {
|
|
24
|
+
description: "Expected input schema for the resource"
|
|
25
|
+
},
|
|
26
|
+
inputSchema: {
|
|
27
|
+
description: "JSON Schema for input validation",
|
|
28
|
+
type: "object"
|
|
29
|
+
},
|
|
30
|
+
output: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
example: {
|
|
34
|
+
description: "Example output for the resource"
|
|
35
|
+
},
|
|
36
|
+
schema: {
|
|
37
|
+
description: "JSON Schema for output validation",
|
|
38
|
+
type: "object"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function declareDiscoveryExtension(config = {}) {
|
|
45
|
+
const info = {};
|
|
46
|
+
if (config.input !== void 0) {
|
|
47
|
+
info.input = config.input;
|
|
48
|
+
}
|
|
49
|
+
if (config.inputSchema !== void 0) {
|
|
50
|
+
info.inputSchema = config.inputSchema;
|
|
51
|
+
}
|
|
52
|
+
if (config.output !== void 0) {
|
|
53
|
+
info.output = config.output;
|
|
54
|
+
}
|
|
55
|
+
return createExtension(info, BAZAAR_SCHEMA);
|
|
56
|
+
}
|
|
57
|
+
function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = true) {
|
|
58
|
+
const extensions = paymentPayload.extensions;
|
|
59
|
+
const bazaarExtension = extractExtension(extensions, BAZAAR);
|
|
60
|
+
if (!bazaarExtension) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
input: bazaarExtension.info.input,
|
|
65
|
+
inputSchema: bazaarExtension.info.inputSchema,
|
|
66
|
+
output: bazaarExtension.info.output,
|
|
67
|
+
network: paymentRequirements.network,
|
|
68
|
+
token: paymentRequirements.asset,
|
|
69
|
+
amount: paymentRequirements.amount,
|
|
70
|
+
payTo: paymentRequirements.payTo
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function validateDiscoveryExtension(extension) {
|
|
74
|
+
if (!extension || typeof extension !== "object") {
|
|
75
|
+
return { valid: false, errors: ["Extension must be an object"] };
|
|
76
|
+
}
|
|
77
|
+
const ext = extension;
|
|
78
|
+
if (!("info" in ext) || typeof ext.info !== "object") {
|
|
79
|
+
return { valid: false, errors: ["Extension must have an 'info' field"] };
|
|
80
|
+
}
|
|
81
|
+
if (!("schema" in ext) || typeof ext.schema !== "object") {
|
|
82
|
+
return { valid: false, errors: ["Extension must have a 'schema' field"] };
|
|
83
|
+
}
|
|
84
|
+
return { valid: true };
|
|
85
|
+
}
|
|
86
|
+
function createDiscoveryConfig(config) {
|
|
87
|
+
return config;
|
|
88
|
+
}
|
|
89
|
+
function isDiscoveryExtension(extension) {
|
|
90
|
+
if (!extension || typeof extension !== "object") {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const ext = extension;
|
|
94
|
+
return "info" in ext && "schema" in ext;
|
|
95
|
+
}
|
|
96
|
+
export {
|
|
97
|
+
BAZAAR,
|
|
98
|
+
createDiscoveryConfig,
|
|
99
|
+
declareDiscoveryExtension,
|
|
100
|
+
extractDiscoveryInfo,
|
|
101
|
+
isDiscoveryExtension,
|
|
102
|
+
validateDiscoveryExtension
|
|
103
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var BAZAAR = "bazaar";
|
|
3
|
+
var SIGN_IN_WITH_X = "sign-in-with-x";
|
|
4
|
+
var PAYMENT_IDENTIFIER = "payment-identifier";
|
|
5
|
+
|
|
6
|
+
// src/validators.ts
|
|
7
|
+
var ajvInstance = null;
|
|
8
|
+
async function getAjv() {
|
|
9
|
+
if (!ajvInstance) {
|
|
10
|
+
const ajvModule = await import("ajv");
|
|
11
|
+
ajvInstance = new ajvModule.default({
|
|
12
|
+
allErrors: true,
|
|
13
|
+
strict: false
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return ajvInstance;
|
|
17
|
+
}
|
|
18
|
+
async function validateExtension(extension, data) {
|
|
19
|
+
const ajv = await getAjv();
|
|
20
|
+
const validate = ajv.compile(extension.schema);
|
|
21
|
+
if (validate(data)) {
|
|
22
|
+
return { valid: true };
|
|
23
|
+
}
|
|
24
|
+
const errors = validate.errors?.map((err) => {
|
|
25
|
+
let path = "";
|
|
26
|
+
if (err.instancePath) {
|
|
27
|
+
path = err.instancePath;
|
|
28
|
+
} else if (err.schemaPath && Array.isArray(err.schemaPath)) {
|
|
29
|
+
path = err.schemaPath.join(".");
|
|
30
|
+
}
|
|
31
|
+
return `${path}: ${err.message || "validation error"}`;
|
|
32
|
+
});
|
|
33
|
+
return { valid: false, errors };
|
|
34
|
+
}
|
|
35
|
+
function extractExtension(extensions, key) {
|
|
36
|
+
if (!extensions || typeof extensions !== "object") {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const extension = extensions[key];
|
|
40
|
+
if (!extension || typeof extension !== "object") {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return extension;
|
|
44
|
+
}
|
|
45
|
+
function createExtension(info, schema) {
|
|
46
|
+
return { info, schema };
|
|
47
|
+
}
|
|
48
|
+
async function validateWithSchema(schema, data) {
|
|
49
|
+
const ajv = await getAjv();
|
|
50
|
+
const validate = ajv.compile(schema);
|
|
51
|
+
if (validate(data)) {
|
|
52
|
+
return { valid: true };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
valid: false,
|
|
56
|
+
errors: validate.errors || []
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/bazaar.ts
|
|
61
|
+
var BAZAAR_SCHEMA = {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
input: {
|
|
65
|
+
description: "Expected input schema for the resource"
|
|
66
|
+
},
|
|
67
|
+
inputSchema: {
|
|
68
|
+
description: "JSON Schema for input validation",
|
|
69
|
+
type: "object"
|
|
70
|
+
},
|
|
71
|
+
output: {
|
|
72
|
+
type: "object",
|
|
73
|
+
properties: {
|
|
74
|
+
example: {
|
|
75
|
+
description: "Example output for the resource"
|
|
76
|
+
},
|
|
77
|
+
schema: {
|
|
78
|
+
description: "JSON Schema for output validation",
|
|
79
|
+
type: "object"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
function declareDiscoveryExtension(config = {}) {
|
|
86
|
+
const info = {};
|
|
87
|
+
if (config.input !== void 0) {
|
|
88
|
+
info.input = config.input;
|
|
89
|
+
}
|
|
90
|
+
if (config.inputSchema !== void 0) {
|
|
91
|
+
info.inputSchema = config.inputSchema;
|
|
92
|
+
}
|
|
93
|
+
if (config.output !== void 0) {
|
|
94
|
+
info.output = config.output;
|
|
95
|
+
}
|
|
96
|
+
return createExtension(info, BAZAAR_SCHEMA);
|
|
97
|
+
}
|
|
98
|
+
function extractDiscoveryInfo(paymentPayload, paymentRequirements, validate = true) {
|
|
99
|
+
const extensions = paymentPayload.extensions;
|
|
100
|
+
const bazaarExtension = extractExtension(extensions, BAZAAR);
|
|
101
|
+
if (!bazaarExtension) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
input: bazaarExtension.info.input,
|
|
106
|
+
inputSchema: bazaarExtension.info.inputSchema,
|
|
107
|
+
output: bazaarExtension.info.output,
|
|
108
|
+
network: paymentRequirements.network,
|
|
109
|
+
token: paymentRequirements.asset,
|
|
110
|
+
amount: paymentRequirements.amount,
|
|
111
|
+
payTo: paymentRequirements.payTo
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function validateDiscoveryExtension(extension) {
|
|
115
|
+
if (!extension || typeof extension !== "object") {
|
|
116
|
+
return { valid: false, errors: ["Extension must be an object"] };
|
|
117
|
+
}
|
|
118
|
+
const ext = extension;
|
|
119
|
+
if (!("info" in ext) || typeof ext.info !== "object") {
|
|
120
|
+
return { valid: false, errors: ["Extension must have an 'info' field"] };
|
|
121
|
+
}
|
|
122
|
+
if (!("schema" in ext) || typeof ext.schema !== "object") {
|
|
123
|
+
return { valid: false, errors: ["Extension must have a 'schema' field"] };
|
|
124
|
+
}
|
|
125
|
+
return { valid: true };
|
|
126
|
+
}
|
|
127
|
+
function createDiscoveryConfig(config) {
|
|
128
|
+
return config;
|
|
129
|
+
}
|
|
130
|
+
function isDiscoveryExtension(extension) {
|
|
131
|
+
if (!extension || typeof extension !== "object") {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
const ext = extension;
|
|
135
|
+
return "info" in ext && "schema" in ext;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/sign-in-with-x.ts
|
|
139
|
+
import { isAddress } from "@armory-sh/base";
|
|
140
|
+
var SIWX_SCHEMA = {
|
|
141
|
+
type: "object",
|
|
142
|
+
properties: {
|
|
143
|
+
domain: {
|
|
144
|
+
type: "string",
|
|
145
|
+
description: "Domain requesting the sign-in"
|
|
146
|
+
},
|
|
147
|
+
resourceUri: {
|
|
148
|
+
type: "string",
|
|
149
|
+
description: "URI of the resource being accessed"
|
|
150
|
+
},
|
|
151
|
+
network: {
|
|
152
|
+
oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
|
153
|
+
description: "Blockchain network(s) for authentication"
|
|
154
|
+
},
|
|
155
|
+
statement: {
|
|
156
|
+
type: "string",
|
|
157
|
+
description: "Human-readable statement about the sign-in"
|
|
158
|
+
},
|
|
159
|
+
version: {
|
|
160
|
+
type: "string",
|
|
161
|
+
description: "SIWX message version"
|
|
162
|
+
},
|
|
163
|
+
expirationSeconds: {
|
|
164
|
+
type: "number",
|
|
165
|
+
description: "Seconds until the message expires"
|
|
166
|
+
},
|
|
167
|
+
supportedChains: {
|
|
168
|
+
type: "array",
|
|
169
|
+
items: {
|
|
170
|
+
type: "object",
|
|
171
|
+
properties: {
|
|
172
|
+
chainId: { type: "string" },
|
|
173
|
+
type: { type: "string" }
|
|
174
|
+
},
|
|
175
|
+
required: ["chainId", "type"]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
var SIWX_HEADER_PATTERN = /^siwx-v1-(.+)$/;
|
|
181
|
+
function base64UrlEncode(str) {
|
|
182
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
183
|
+
}
|
|
184
|
+
function base64UrlDecode(str) {
|
|
185
|
+
let padded = str;
|
|
186
|
+
while (padded.length % 4) {
|
|
187
|
+
padded += "=";
|
|
188
|
+
}
|
|
189
|
+
return atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
|
|
190
|
+
}
|
|
191
|
+
function declareSIWxExtension(config = {}) {
|
|
192
|
+
const info = {};
|
|
193
|
+
if (config.domain !== void 0) {
|
|
194
|
+
info.domain = config.domain;
|
|
195
|
+
}
|
|
196
|
+
if (config.resourceUri !== void 0) {
|
|
197
|
+
info.resourceUri = config.resourceUri;
|
|
198
|
+
}
|
|
199
|
+
if (config.network !== void 0) {
|
|
200
|
+
info.network = config.network;
|
|
201
|
+
}
|
|
202
|
+
if (config.statement !== void 0) {
|
|
203
|
+
info.statement = config.statement;
|
|
204
|
+
}
|
|
205
|
+
if (config.version !== void 0) {
|
|
206
|
+
info.version = config.version;
|
|
207
|
+
}
|
|
208
|
+
if (config.expirationSeconds !== void 0) {
|
|
209
|
+
info.expirationSeconds = config.expirationSeconds;
|
|
210
|
+
}
|
|
211
|
+
return createExtension(info, SIWX_SCHEMA);
|
|
212
|
+
}
|
|
213
|
+
function parseSIWxHeader(header) {
|
|
214
|
+
const match = header.match(SIWX_HEADER_PATTERN);
|
|
215
|
+
if (!match) {
|
|
216
|
+
throw new Error("Invalid SIWX header format");
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
const decoded = base64UrlDecode(match[1]);
|
|
220
|
+
return JSON.parse(decoded);
|
|
221
|
+
} catch {
|
|
222
|
+
throw new Error("Failed to decode SIWX header");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function validateSIWxMessage(payload, resourceUri, options = {}) {
|
|
226
|
+
if (!payload.domain) {
|
|
227
|
+
return { valid: false, error: "Missing domain" };
|
|
228
|
+
}
|
|
229
|
+
if (!payload.address || !isAddress(payload.address)) {
|
|
230
|
+
return { valid: false, error: "Invalid or missing address" };
|
|
231
|
+
}
|
|
232
|
+
if (payload.resourceUri && payload.resourceUri !== resourceUri) {
|
|
233
|
+
return { valid: false, error: "Resource URI mismatch" };
|
|
234
|
+
}
|
|
235
|
+
if (payload.expirationTime) {
|
|
236
|
+
const expiration = new Date(payload.expirationTime).getTime();
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
if (expiration < now) {
|
|
239
|
+
return { valid: false, error: "Message has expired" };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (options.maxAge && payload.issuedAt) {
|
|
243
|
+
const issued = new Date(payload.issuedAt).getTime();
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
const age = now - issued;
|
|
246
|
+
if (age > options.maxAge * 1e3) {
|
|
247
|
+
return { valid: false, error: "Message is too old" };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (options.checkNonce && payload.nonce) {
|
|
251
|
+
if (!options.checkNonce(payload.nonce)) {
|
|
252
|
+
return { valid: false, error: "Invalid nonce" };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return { valid: true };
|
|
256
|
+
}
|
|
257
|
+
async function verifySIWxSignature(payload, options = {}) {
|
|
258
|
+
if (!payload.signature) {
|
|
259
|
+
return { valid: false, error: "Missing signature" };
|
|
260
|
+
}
|
|
261
|
+
if (!payload.address || !isAddress(payload.address)) {
|
|
262
|
+
return { valid: false, error: "Invalid address" };
|
|
263
|
+
}
|
|
264
|
+
if (options.evmVerifier) {
|
|
265
|
+
const message = createSIWxMessage(payload);
|
|
266
|
+
const valid = await options.evmVerifier(message, payload.signature, payload.address);
|
|
267
|
+
if (!valid) {
|
|
268
|
+
return { valid: false, error: "Signature verification failed" };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { valid: true, address: payload.address };
|
|
272
|
+
}
|
|
273
|
+
function createSIWxMessage(payload) {
|
|
274
|
+
const lines = [];
|
|
275
|
+
lines.push(`${payload.domain} wants you to sign in`);
|
|
276
|
+
if (payload.statement) {
|
|
277
|
+
lines.push("");
|
|
278
|
+
lines.push(payload.statement);
|
|
279
|
+
}
|
|
280
|
+
lines.push("");
|
|
281
|
+
if (payload.resourceUri) {
|
|
282
|
+
lines.push(`URI: ${payload.resourceUri}`);
|
|
283
|
+
}
|
|
284
|
+
if (payload.version) {
|
|
285
|
+
lines.push(`Version: ${payload.version}`);
|
|
286
|
+
}
|
|
287
|
+
if (payload.nonce) {
|
|
288
|
+
lines.push(`Nonce: ${payload.nonce}`);
|
|
289
|
+
}
|
|
290
|
+
if (payload.issuedAt) {
|
|
291
|
+
lines.push(`Issued At: ${payload.issuedAt}`);
|
|
292
|
+
}
|
|
293
|
+
if (payload.expirationTime) {
|
|
294
|
+
lines.push(`Expiration Time: ${payload.expirationTime}`);
|
|
295
|
+
}
|
|
296
|
+
if (payload.chainId) {
|
|
297
|
+
const chains = Array.isArray(payload.chainId) ? payload.chainId.join(", ") : payload.chainId;
|
|
298
|
+
lines.push(`Chain ID(s): ${chains}`);
|
|
299
|
+
}
|
|
300
|
+
lines.push("");
|
|
301
|
+
lines.push(`Address: ${payload.address}`);
|
|
302
|
+
return lines.join("\n");
|
|
303
|
+
}
|
|
304
|
+
function encodeSIWxHeader(payload) {
|
|
305
|
+
const payloadStr = JSON.stringify(payload);
|
|
306
|
+
const encoded = base64UrlEncode(payloadStr);
|
|
307
|
+
return `siwx-v1-${encoded}`;
|
|
308
|
+
}
|
|
309
|
+
function createSIWxPayload(serverInfo, address, options = {}) {
|
|
310
|
+
const getDefaultDomain = () => {
|
|
311
|
+
if (typeof window !== "undefined" && window.location) {
|
|
312
|
+
return window.location.hostname;
|
|
313
|
+
}
|
|
314
|
+
return "localhost";
|
|
315
|
+
};
|
|
316
|
+
const payload = {
|
|
317
|
+
domain: serverInfo.domain || getDefaultDomain(),
|
|
318
|
+
address
|
|
319
|
+
};
|
|
320
|
+
if (serverInfo.resourceUri) {
|
|
321
|
+
payload.resourceUri = serverInfo.resourceUri;
|
|
322
|
+
}
|
|
323
|
+
if (serverInfo.statement) {
|
|
324
|
+
payload.statement = serverInfo.statement;
|
|
325
|
+
}
|
|
326
|
+
if (serverInfo.version) {
|
|
327
|
+
payload.version = serverInfo.version;
|
|
328
|
+
}
|
|
329
|
+
if (serverInfo.network) {
|
|
330
|
+
payload.chainId = serverInfo.network;
|
|
331
|
+
}
|
|
332
|
+
if (options.nonce) {
|
|
333
|
+
payload.nonce = options.nonce;
|
|
334
|
+
}
|
|
335
|
+
if (options.issuedAt) {
|
|
336
|
+
payload.issuedAt = options.issuedAt;
|
|
337
|
+
}
|
|
338
|
+
if (options.expirationTime) {
|
|
339
|
+
payload.expirationTime = options.expirationTime;
|
|
340
|
+
} else if (serverInfo.expirationSeconds) {
|
|
341
|
+
const expiration = new Date(Date.now() + serverInfo.expirationSeconds * 1e3);
|
|
342
|
+
payload.expirationTime = expiration.toISOString();
|
|
343
|
+
}
|
|
344
|
+
return payload;
|
|
345
|
+
}
|
|
346
|
+
function isSIWxExtension(extension) {
|
|
347
|
+
if (!extension || typeof extension !== "object") {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
const ext = extension;
|
|
351
|
+
return "info" in ext && "schema" in ext;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// src/payment-identifier.ts
|
|
355
|
+
var PAYMENT_ID_ALLOWED_CHARS = /^[a-z0-9_-]+$/;
|
|
356
|
+
var PAYMENT_ID_MIN_LENGTH = 3;
|
|
357
|
+
var PAYMENT_ID_MAX_LENGTH = 128;
|
|
358
|
+
function generatePaymentId() {
|
|
359
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
360
|
+
let result = "";
|
|
361
|
+
for (let i = 0; i < 32; i++) {
|
|
362
|
+
const randomIndex = Math.floor(Math.random() * chars.length);
|
|
363
|
+
result += chars[randomIndex];
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
366
|
+
}
|
|
367
|
+
function appendPaymentIdentifierToExtensions(extensions, paymentId) {
|
|
368
|
+
const existing = extensions || {};
|
|
369
|
+
const extension = {
|
|
370
|
+
paymentId
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
...existing,
|
|
374
|
+
"payment-identifier": extension
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
function extractPaymentIdentifierInfo(paymentPayload) {
|
|
378
|
+
const extensions = paymentPayload.extensions;
|
|
379
|
+
if (!extensions || typeof extensions !== "object") {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const identifierExt = extensions["payment-identifier"];
|
|
383
|
+
if (!identifierExt || typeof identifierExt !== "object") {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const ext = identifierExt;
|
|
387
|
+
return ext.info?.paymentId || null;
|
|
388
|
+
}
|
|
389
|
+
function validatePaymentIdentifierExtension(extension) {
|
|
390
|
+
if (!extension || typeof extension !== "object") {
|
|
391
|
+
return { valid: false, errors: ["Extension must be an object"] };
|
|
392
|
+
}
|
|
393
|
+
const ext = extension;
|
|
394
|
+
if (!("info" in ext) || typeof ext.info !== "object") {
|
|
395
|
+
return { valid: false, errors: ["Extension must have an 'info' field"] };
|
|
396
|
+
}
|
|
397
|
+
const info = ext.info;
|
|
398
|
+
if (info.paymentId !== void 0) {
|
|
399
|
+
if (typeof info.paymentId !== "string") {
|
|
400
|
+
return { valid: false, errors: ["paymentId must be a string when defined"] };
|
|
401
|
+
}
|
|
402
|
+
if (info.paymentId.length > PAYMENT_ID_MAX_LENGTH) {
|
|
403
|
+
return { valid: false, errors: ["must be 128 characters or fewer"] };
|
|
404
|
+
}
|
|
405
|
+
if (info.paymentId.length < PAYMENT_ID_MIN_LENGTH) {
|
|
406
|
+
return {
|
|
407
|
+
valid: false,
|
|
408
|
+
errors: [`must be at least ${PAYMENT_ID_MIN_LENGTH} characters`]
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (!PAYMENT_ID_ALLOWED_CHARS.test(info.paymentId)) {
|
|
412
|
+
return {
|
|
413
|
+
valid: false,
|
|
414
|
+
errors: ["alphanumeric, hyphens, and underscores only"]
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (info.required !== void 0 && typeof info.required !== "boolean") {
|
|
419
|
+
return { valid: false, errors: ["required must be a boolean"] };
|
|
420
|
+
}
|
|
421
|
+
return { valid: true };
|
|
422
|
+
}
|
|
423
|
+
function isPaymentIdentifierExtension(extension) {
|
|
424
|
+
if (!extension || typeof extension !== "object") {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
const ext = extension;
|
|
428
|
+
return "info" in ext && "schema" in ext;
|
|
429
|
+
}
|
|
430
|
+
function declarePaymentIdentifierExtension(config = {}) {
|
|
431
|
+
const info = {};
|
|
432
|
+
if (config.paymentId !== void 0) {
|
|
433
|
+
info.paymentId = config.paymentId;
|
|
434
|
+
}
|
|
435
|
+
if (config.required !== void 0) {
|
|
436
|
+
info.required = config.required;
|
|
437
|
+
}
|
|
438
|
+
const paymentIdSchema = {
|
|
439
|
+
type: "string",
|
|
440
|
+
minLength: PAYMENT_ID_MIN_LENGTH,
|
|
441
|
+
maxLength: PAYMENT_ID_MAX_LENGTH,
|
|
442
|
+
pattern: PAYMENT_ID_ALLOWED_CHARS.source,
|
|
443
|
+
description: "Unique payment identifier for idempotency"
|
|
444
|
+
};
|
|
445
|
+
if (info.required !== void 0) {
|
|
446
|
+
paymentIdSchema.required = [info.required];
|
|
447
|
+
}
|
|
448
|
+
return createExtension(info, {
|
|
449
|
+
type: "object",
|
|
450
|
+
properties: {
|
|
451
|
+
paymentId: paymentIdSchema,
|
|
452
|
+
required: {
|
|
453
|
+
type: "boolean",
|
|
454
|
+
description: "Whether payment identifier is required for this payment"
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
required: info.required ? ["paymentId"] : []
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/hooks.ts
|
|
462
|
+
function createSIWxHook(config) {
|
|
463
|
+
return {
|
|
464
|
+
hook: async (context) => {
|
|
465
|
+
if (!context.payload) return;
|
|
466
|
+
const serverExtensions = context.paymentContext?.serverExtensions;
|
|
467
|
+
const siwxInfo = serverExtensions?.["sign-in-with-x"];
|
|
468
|
+
if (!siwxInfo?.info) return;
|
|
469
|
+
const info = siwxInfo.info;
|
|
470
|
+
const address = context.paymentContext?.fromAddress;
|
|
471
|
+
const nonce = context.paymentContext?.nonce;
|
|
472
|
+
if (!address || !nonce) return;
|
|
473
|
+
const payload = createSIWxPayload(info, address, {
|
|
474
|
+
nonce: nonce.slice(2),
|
|
475
|
+
issuedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
476
|
+
});
|
|
477
|
+
const message = createSIWxMessage(payload);
|
|
478
|
+
const wallet = context.wallet;
|
|
479
|
+
let signature;
|
|
480
|
+
if (wallet.type === "account") {
|
|
481
|
+
signature = await wallet.account.signMessage({ message });
|
|
482
|
+
} else if (wallet.type === "walletClient") {
|
|
483
|
+
signature = await wallet.walletClient.account.signMessage({ message });
|
|
484
|
+
} else {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
payload.signature = signature;
|
|
488
|
+
const header = encodeSIWxHeader(payload);
|
|
489
|
+
context.payload.extensions = {
|
|
490
|
+
...context.payload.extensions ?? {},
|
|
491
|
+
"sign-in-with-x": header
|
|
492
|
+
};
|
|
493
|
+
},
|
|
494
|
+
priority: 100,
|
|
495
|
+
name: "sign-in-with-x"
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
function createPaymentIdHook(config) {
|
|
499
|
+
const state = { paymentId: config?.paymentId };
|
|
500
|
+
return {
|
|
501
|
+
hook: async (context) => {
|
|
502
|
+
if (context.serverExtensions) {
|
|
503
|
+
const paymentIdExt = context.serverExtensions["payment-identifier"];
|
|
504
|
+
if (paymentIdExt && typeof paymentIdExt === "object") {
|
|
505
|
+
const info = paymentIdExt.info;
|
|
506
|
+
if (info?.required && !state.paymentId) {
|
|
507
|
+
state.paymentId = generatePaymentId();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (context.payload && state.paymentId) {
|
|
512
|
+
context.payload.extensions = appendPaymentIdentifierToExtensions(
|
|
513
|
+
context.payload.extensions,
|
|
514
|
+
state.paymentId
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
priority: 50,
|
|
519
|
+
name: "payment-identifier"
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function createCustomHook(config) {
|
|
523
|
+
return {
|
|
524
|
+
hook: config.handler,
|
|
525
|
+
priority: config.priority ?? 0,
|
|
526
|
+
name: config.key
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
export {
|
|
530
|
+
BAZAAR,
|
|
531
|
+
PAYMENT_IDENTIFIER,
|
|
532
|
+
SIGN_IN_WITH_X,
|
|
533
|
+
createCustomHook,
|
|
534
|
+
createDiscoveryConfig,
|
|
535
|
+
createExtension,
|
|
536
|
+
createPaymentIdHook,
|
|
537
|
+
createSIWxHook,
|
|
538
|
+
createSIWxMessage,
|
|
539
|
+
createSIWxPayload,
|
|
540
|
+
declareDiscoveryExtension,
|
|
541
|
+
declarePaymentIdentifierExtension,
|
|
542
|
+
declareSIWxExtension,
|
|
543
|
+
encodeSIWxHeader,
|
|
544
|
+
extractDiscoveryInfo,
|
|
545
|
+
extractExtension,
|
|
546
|
+
extractPaymentIdentifierInfo,
|
|
547
|
+
isDiscoveryExtension,
|
|
548
|
+
isPaymentIdentifierExtension,
|
|
549
|
+
isSIWxExtension,
|
|
550
|
+
parseSIWxHeader,
|
|
551
|
+
validateDiscoveryExtension,
|
|
552
|
+
validateExtension,
|
|
553
|
+
validatePaymentIdentifierExtension,
|
|
554
|
+
validateSIWxMessage,
|
|
555
|
+
validateWithSchema,
|
|
556
|
+
verifySIWxSignature
|
|
557
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// src/validators.ts
|
|
2
|
+
function createExtension(info, schema) {
|
|
3
|
+
return { info, schema };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/payment-identifier.ts
|
|
7
|
+
var PAYMENT_ID_ALLOWED_CHARS = /^[a-z0-9_-]+$/;
|
|
8
|
+
var PAYMENT_ID_MIN_LENGTH = 3;
|
|
9
|
+
var PAYMENT_ID_MAX_LENGTH = 128;
|
|
10
|
+
function generatePaymentId() {
|
|
11
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
12
|
+
let result = "";
|
|
13
|
+
for (let i = 0; i < 32; i++) {
|
|
14
|
+
const randomIndex = Math.floor(Math.random() * chars.length);
|
|
15
|
+
result += chars[randomIndex];
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
function appendPaymentIdentifierToExtensions(extensions, paymentId) {
|
|
20
|
+
const existing = extensions || {};
|
|
21
|
+
const extension = {
|
|
22
|
+
paymentId
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
...existing,
|
|
26
|
+
"payment-identifier": extension
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function extractPaymentIdentifierInfo(paymentPayload) {
|
|
30
|
+
const extensions = paymentPayload.extensions;
|
|
31
|
+
if (!extensions || typeof extensions !== "object") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const identifierExt = extensions["payment-identifier"];
|
|
35
|
+
if (!identifierExt || typeof identifierExt !== "object") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const ext = identifierExt;
|
|
39
|
+
return ext.info?.paymentId || null;
|
|
40
|
+
}
|
|
41
|
+
function isValidPaymentId(paymentId) {
|
|
42
|
+
if (typeof paymentId !== "string") {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (paymentId.length < PAYMENT_ID_MIN_LENGTH) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
if (paymentId.length > PAYMENT_ID_MAX_LENGTH) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
return PAYMENT_ID_ALLOWED_CHARS.test(paymentId);
|
|
52
|
+
}
|
|
53
|
+
function validatePaymentIdentifierExtension(extension) {
|
|
54
|
+
if (!extension || typeof extension !== "object") {
|
|
55
|
+
return { valid: false, errors: ["Extension must be an object"] };
|
|
56
|
+
}
|
|
57
|
+
const ext = extension;
|
|
58
|
+
if (!("info" in ext) || typeof ext.info !== "object") {
|
|
59
|
+
return { valid: false, errors: ["Extension must have an 'info' field"] };
|
|
60
|
+
}
|
|
61
|
+
const info = ext.info;
|
|
62
|
+
if (info.paymentId !== void 0) {
|
|
63
|
+
if (typeof info.paymentId !== "string") {
|
|
64
|
+
return { valid: false, errors: ["paymentId must be a string when defined"] };
|
|
65
|
+
}
|
|
66
|
+
if (info.paymentId.length > PAYMENT_ID_MAX_LENGTH) {
|
|
67
|
+
return { valid: false, errors: ["must be 128 characters or fewer"] };
|
|
68
|
+
}
|
|
69
|
+
if (info.paymentId.length < PAYMENT_ID_MIN_LENGTH) {
|
|
70
|
+
return {
|
|
71
|
+
valid: false,
|
|
72
|
+
errors: [`must be at least ${PAYMENT_ID_MIN_LENGTH} characters`]
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (!PAYMENT_ID_ALLOWED_CHARS.test(info.paymentId)) {
|
|
76
|
+
return {
|
|
77
|
+
valid: false,
|
|
78
|
+
errors: ["alphanumeric, hyphens, and underscores only"]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (info.required !== void 0 && typeof info.required !== "boolean") {
|
|
83
|
+
return { valid: false, errors: ["required must be a boolean"] };
|
|
84
|
+
}
|
|
85
|
+
return { valid: true };
|
|
86
|
+
}
|
|
87
|
+
function isPaymentIdentifierExtension(extension) {
|
|
88
|
+
if (!extension || typeof extension !== "object") {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const ext = extension;
|
|
92
|
+
return "info" in ext && "schema" in ext;
|
|
93
|
+
}
|
|
94
|
+
function declarePaymentIdentifierExtension(config = {}) {
|
|
95
|
+
const info = {};
|
|
96
|
+
if (config.paymentId !== void 0) {
|
|
97
|
+
info.paymentId = config.paymentId;
|
|
98
|
+
}
|
|
99
|
+
if (config.required !== void 0) {
|
|
100
|
+
info.required = config.required;
|
|
101
|
+
}
|
|
102
|
+
const paymentIdSchema = {
|
|
103
|
+
type: "string",
|
|
104
|
+
minLength: PAYMENT_ID_MIN_LENGTH,
|
|
105
|
+
maxLength: PAYMENT_ID_MAX_LENGTH,
|
|
106
|
+
pattern: PAYMENT_ID_ALLOWED_CHARS.source,
|
|
107
|
+
description: "Unique payment identifier for idempotency"
|
|
108
|
+
};
|
|
109
|
+
if (info.required !== void 0) {
|
|
110
|
+
paymentIdSchema.required = [info.required];
|
|
111
|
+
}
|
|
112
|
+
return createExtension(info, {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
paymentId: paymentIdSchema,
|
|
116
|
+
required: {
|
|
117
|
+
type: "boolean",
|
|
118
|
+
description: "Whether payment identifier is required for this payment"
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
required: info.required ? ["paymentId"] : []
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
var PAYMENT_IDENTIFIER = "payment-identifier";
|
|
125
|
+
export {
|
|
126
|
+
PAYMENT_IDENTIFIER,
|
|
127
|
+
appendPaymentIdentifierToExtensions,
|
|
128
|
+
declarePaymentIdentifierExtension,
|
|
129
|
+
extractPaymentIdentifierInfo,
|
|
130
|
+
generatePaymentId,
|
|
131
|
+
isPaymentIdentifierExtension,
|
|
132
|
+
isValidPaymentId,
|
|
133
|
+
validatePaymentIdentifierExtension
|
|
134
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
// src/validators.ts
|
|
2
|
+
function createExtension(info, schema) {
|
|
3
|
+
return { info, schema };
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/sign-in-with-x.ts
|
|
7
|
+
import { isAddress } from "@armory-sh/base";
|
|
8
|
+
|
|
9
|
+
// src/types.ts
|
|
10
|
+
var SIGN_IN_WITH_X = "sign-in-with-x";
|
|
11
|
+
|
|
12
|
+
// src/sign-in-with-x.ts
|
|
13
|
+
var SIWX_SCHEMA = {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
domain: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Domain requesting the sign-in"
|
|
19
|
+
},
|
|
20
|
+
resourceUri: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "URI of the resource being accessed"
|
|
23
|
+
},
|
|
24
|
+
network: {
|
|
25
|
+
oneOf: [{ type: "string" }, { type: "array", items: { type: "string" } }],
|
|
26
|
+
description: "Blockchain network(s) for authentication"
|
|
27
|
+
},
|
|
28
|
+
statement: {
|
|
29
|
+
type: "string",
|
|
30
|
+
description: "Human-readable statement about the sign-in"
|
|
31
|
+
},
|
|
32
|
+
version: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "SIWX message version"
|
|
35
|
+
},
|
|
36
|
+
expirationSeconds: {
|
|
37
|
+
type: "number",
|
|
38
|
+
description: "Seconds until the message expires"
|
|
39
|
+
},
|
|
40
|
+
supportedChains: {
|
|
41
|
+
type: "array",
|
|
42
|
+
items: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
chainId: { type: "string" },
|
|
46
|
+
type: { type: "string" }
|
|
47
|
+
},
|
|
48
|
+
required: ["chainId", "type"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var SIWX_HEADER_PATTERN = /^siwx-v1-(.+)$/;
|
|
54
|
+
function base64UrlEncode(str) {
|
|
55
|
+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
56
|
+
}
|
|
57
|
+
function base64UrlDecode(str) {
|
|
58
|
+
let padded = str;
|
|
59
|
+
while (padded.length % 4) {
|
|
60
|
+
padded += "=";
|
|
61
|
+
}
|
|
62
|
+
return atob(padded.replace(/-/g, "+").replace(/_/g, "/"));
|
|
63
|
+
}
|
|
64
|
+
function declareSIWxExtension(config = {}) {
|
|
65
|
+
const info = {};
|
|
66
|
+
if (config.domain !== void 0) {
|
|
67
|
+
info.domain = config.domain;
|
|
68
|
+
}
|
|
69
|
+
if (config.resourceUri !== void 0) {
|
|
70
|
+
info.resourceUri = config.resourceUri;
|
|
71
|
+
}
|
|
72
|
+
if (config.network !== void 0) {
|
|
73
|
+
info.network = config.network;
|
|
74
|
+
}
|
|
75
|
+
if (config.statement !== void 0) {
|
|
76
|
+
info.statement = config.statement;
|
|
77
|
+
}
|
|
78
|
+
if (config.version !== void 0) {
|
|
79
|
+
info.version = config.version;
|
|
80
|
+
}
|
|
81
|
+
if (config.expirationSeconds !== void 0) {
|
|
82
|
+
info.expirationSeconds = config.expirationSeconds;
|
|
83
|
+
}
|
|
84
|
+
return createExtension(info, SIWX_SCHEMA);
|
|
85
|
+
}
|
|
86
|
+
function parseSIWxHeader(header) {
|
|
87
|
+
const match = header.match(SIWX_HEADER_PATTERN);
|
|
88
|
+
if (!match) {
|
|
89
|
+
throw new Error("Invalid SIWX header format");
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const decoded = base64UrlDecode(match[1]);
|
|
93
|
+
return JSON.parse(decoded);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error("Failed to decode SIWX header");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function validateSIWxMessage(payload, resourceUri, options = {}) {
|
|
99
|
+
if (!payload.domain) {
|
|
100
|
+
return { valid: false, error: "Missing domain" };
|
|
101
|
+
}
|
|
102
|
+
if (!payload.address || !isAddress(payload.address)) {
|
|
103
|
+
return { valid: false, error: "Invalid or missing address" };
|
|
104
|
+
}
|
|
105
|
+
if (payload.resourceUri && payload.resourceUri !== resourceUri) {
|
|
106
|
+
return { valid: false, error: "Resource URI mismatch" };
|
|
107
|
+
}
|
|
108
|
+
if (payload.expirationTime) {
|
|
109
|
+
const expiration = new Date(payload.expirationTime).getTime();
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
if (expiration < now) {
|
|
112
|
+
return { valid: false, error: "Message has expired" };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (options.maxAge && payload.issuedAt) {
|
|
116
|
+
const issued = new Date(payload.issuedAt).getTime();
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
const age = now - issued;
|
|
119
|
+
if (age > options.maxAge * 1e3) {
|
|
120
|
+
return { valid: false, error: "Message is too old" };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (options.checkNonce && payload.nonce) {
|
|
124
|
+
if (!options.checkNonce(payload.nonce)) {
|
|
125
|
+
return { valid: false, error: "Invalid nonce" };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return { valid: true };
|
|
129
|
+
}
|
|
130
|
+
async function verifySIWxSignature(payload, options = {}) {
|
|
131
|
+
if (!payload.signature) {
|
|
132
|
+
return { valid: false, error: "Missing signature" };
|
|
133
|
+
}
|
|
134
|
+
if (!payload.address || !isAddress(payload.address)) {
|
|
135
|
+
return { valid: false, error: "Invalid address" };
|
|
136
|
+
}
|
|
137
|
+
if (options.evmVerifier) {
|
|
138
|
+
const message = createSIWxMessage(payload);
|
|
139
|
+
const valid = await options.evmVerifier(message, payload.signature, payload.address);
|
|
140
|
+
if (!valid) {
|
|
141
|
+
return { valid: false, error: "Signature verification failed" };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { valid: true, address: payload.address };
|
|
145
|
+
}
|
|
146
|
+
function createSIWxMessage(payload) {
|
|
147
|
+
const lines = [];
|
|
148
|
+
lines.push(`${payload.domain} wants you to sign in`);
|
|
149
|
+
if (payload.statement) {
|
|
150
|
+
lines.push("");
|
|
151
|
+
lines.push(payload.statement);
|
|
152
|
+
}
|
|
153
|
+
lines.push("");
|
|
154
|
+
if (payload.resourceUri) {
|
|
155
|
+
lines.push(`URI: ${payload.resourceUri}`);
|
|
156
|
+
}
|
|
157
|
+
if (payload.version) {
|
|
158
|
+
lines.push(`Version: ${payload.version}`);
|
|
159
|
+
}
|
|
160
|
+
if (payload.nonce) {
|
|
161
|
+
lines.push(`Nonce: ${payload.nonce}`);
|
|
162
|
+
}
|
|
163
|
+
if (payload.issuedAt) {
|
|
164
|
+
lines.push(`Issued At: ${payload.issuedAt}`);
|
|
165
|
+
}
|
|
166
|
+
if (payload.expirationTime) {
|
|
167
|
+
lines.push(`Expiration Time: ${payload.expirationTime}`);
|
|
168
|
+
}
|
|
169
|
+
if (payload.chainId) {
|
|
170
|
+
const chains = Array.isArray(payload.chainId) ? payload.chainId.join(", ") : payload.chainId;
|
|
171
|
+
lines.push(`Chain ID(s): ${chains}`);
|
|
172
|
+
}
|
|
173
|
+
lines.push("");
|
|
174
|
+
lines.push(`Address: ${payload.address}`);
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
function encodeSIWxHeader(payload) {
|
|
178
|
+
const payloadStr = JSON.stringify(payload);
|
|
179
|
+
const encoded = base64UrlEncode(payloadStr);
|
|
180
|
+
return `siwx-v1-${encoded}`;
|
|
181
|
+
}
|
|
182
|
+
function createSIWxPayload(serverInfo, address, options = {}) {
|
|
183
|
+
const getDefaultDomain = () => {
|
|
184
|
+
if (typeof window !== "undefined" && window.location) {
|
|
185
|
+
return window.location.hostname;
|
|
186
|
+
}
|
|
187
|
+
return "localhost";
|
|
188
|
+
};
|
|
189
|
+
const payload = {
|
|
190
|
+
domain: serverInfo.domain || getDefaultDomain(),
|
|
191
|
+
address
|
|
192
|
+
};
|
|
193
|
+
if (serverInfo.resourceUri) {
|
|
194
|
+
payload.resourceUri = serverInfo.resourceUri;
|
|
195
|
+
}
|
|
196
|
+
if (serverInfo.statement) {
|
|
197
|
+
payload.statement = serverInfo.statement;
|
|
198
|
+
}
|
|
199
|
+
if (serverInfo.version) {
|
|
200
|
+
payload.version = serverInfo.version;
|
|
201
|
+
}
|
|
202
|
+
if (serverInfo.network) {
|
|
203
|
+
payload.chainId = serverInfo.network;
|
|
204
|
+
}
|
|
205
|
+
if (options.nonce) {
|
|
206
|
+
payload.nonce = options.nonce;
|
|
207
|
+
}
|
|
208
|
+
if (options.issuedAt) {
|
|
209
|
+
payload.issuedAt = options.issuedAt;
|
|
210
|
+
}
|
|
211
|
+
if (options.expirationTime) {
|
|
212
|
+
payload.expirationTime = options.expirationTime;
|
|
213
|
+
} else if (serverInfo.expirationSeconds) {
|
|
214
|
+
const expiration = new Date(Date.now() + serverInfo.expirationSeconds * 1e3);
|
|
215
|
+
payload.expirationTime = expiration.toISOString();
|
|
216
|
+
}
|
|
217
|
+
return payload;
|
|
218
|
+
}
|
|
219
|
+
function isSIWxExtension(extension) {
|
|
220
|
+
if (!extension || typeof extension !== "object") {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
const ext = extension;
|
|
224
|
+
return "info" in ext && "schema" in ext;
|
|
225
|
+
}
|
|
226
|
+
export {
|
|
227
|
+
SIGN_IN_WITH_X,
|
|
228
|
+
createSIWxMessage,
|
|
229
|
+
createSIWxPayload,
|
|
230
|
+
declareSIWxExtension,
|
|
231
|
+
encodeSIWxHeader,
|
|
232
|
+
isSIWxExtension,
|
|
233
|
+
parseSIWxHeader,
|
|
234
|
+
validateSIWxMessage,
|
|
235
|
+
verifySIWxSignature
|
|
236
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@armory-sh/extensions",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"author": "Sawyer Cutler <sawyer@dirtroad.dev>",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"bun": "./src/index.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./bazaar": {
|
|
16
|
+
"types": "./dist/bazaar.d.ts",
|
|
17
|
+
"bun": "./src/bazaar.ts",
|
|
18
|
+
"default": "./dist/bazaar.js"
|
|
19
|
+
},
|
|
20
|
+
"./sign-in-with-x": {
|
|
21
|
+
"types": "./dist/sign-in-with-x.d.ts",
|
|
22
|
+
"bun": "./src/sign-in-with-x.ts",
|
|
23
|
+
"default": "./dist/sign-in-with-x.js"
|
|
24
|
+
},
|
|
25
|
+
"./payment-identifier": {
|
|
26
|
+
"types": "./dist/payment-identifier.d.ts",
|
|
27
|
+
"bun": "./src/payment-identifier.ts",
|
|
28
|
+
"default": "./dist/payment-identifier.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/thegreataxios/armory.git",
|
|
41
|
+
"directory": "packages/extensions"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@armory-sh/base": "0.2.20",
|
|
45
|
+
"ajv": "^8.12.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^25.2.1",
|
|
49
|
+
"bun-types": "latest",
|
|
50
|
+
"tsup": "^8.0.0",
|
|
51
|
+
"typescript": "5.9.3"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "rm -rf dist && tsup",
|
|
55
|
+
"build:dts": "rm -rf dist && tsc --declaration --skipLibCheck",
|
|
56
|
+
"test": "bun test",
|
|
57
|
+
"typecheck": "tsc --noEmit"
|
|
58
|
+
}
|
|
59
|
+
}
|