@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 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
+ }