/
payment-pointer.ts
114 lines (96 loc) · 3.68 KB
/
payment-pointer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
export const createHttpUrl = (rawUrl: string, base?: string): URL | undefined => {
try {
const url = new URL(rawUrl, base)
if (url.protocol === 'https:' || url.protocol === 'http:') {
return url
}
} catch (_) {
return
}
}
/** URL of a unique account payable over Interledger, queryable via SPSP or Open Payments */
export class AccountUrl {
private static DEFAULT_PATH = '/.well-known/pay'
/** Protocol of the URL */
private protocol: string
/** Domain name of the URL */
private hostname: string
/** Path with stripped trailing slash, or `undefined` for default, well-known account path */
private path?: string
/** Query string and/or fragment. Empty string for PP, optional for the full URL format */
private suffix: string
/** Parse a [payment pointer](https://paymentpoiners.org) prefixed with "$" */
static fromPaymentPointer(paymentPointer: string): AccountUrl | undefined {
if (!paymentPointer.startsWith('$')) {
return
}
/**
* From paymentpointers.org/syntax-resolution/:
*
* "...the Payment Pointer syntax only supports a host which excludes the userinfo and port.
* The Payment Pointer syntax also excludes the query and fragment parts that are allowed in the URL syntax.
*
* Payment Pointers that do not meet the limited syntax of this profile MUST be
* considered invalid and should not be used to resolve a URL."
*/
const url = createHttpUrl('https://' + paymentPointer.substring(1))
if (
!url || // URL was invalid
url.username !== '' ||
url.password !== '' ||
url.port !== '' ||
url.search !== '' || // No query params
url.hash !== '' // No fragment
) {
return
}
return new AccountUrl(url)
}
/** Parse SPSP/Open Payments account URL. Must be HTTPS/HTTP, contain no credentials, and no port. */
static fromUrl(rawUrl: string): AccountUrl | undefined {
const url = createHttpUrl(rawUrl)
if (!url || url.username !== '' || url.password !== '' || url.port !== '') {
return
}
// Don't error if query string or fragment is included -- allowed from URL format
return new AccountUrl(url)
}
private constructor(url: URL) {
this.protocol = url.protocol
this.hostname = url.hostname
// Strip trailing slash. If empty, `URL` still adds back the initial slash
const pathname = url.pathname.replace(/\/$/, '')
// Don't set the path if it corresponds to the default
if (!(pathname === '' || pathname === AccountUrl.DEFAULT_PATH)) {
this.path = pathname
}
// Empty for payment pointers (fails), optional for full URL variant
this.suffix = url.search + url.hash
}
/** Endpoint URL for SPSP queries to the account. Includes query string and/or fragment */
toEndpointUrl(): string {
return (
this.protocol + '//' + this.hostname + (this.path ?? AccountUrl.DEFAULT_PATH) + this.suffix
)
}
/** Endpoint URL for SPSP queries to the account. Excludes query string and/or fragment */
toBaseUrl(): string {
return this.protocol + '//' + this.hostname + (this.path ?? AccountUrl.DEFAULT_PATH)
}
/**
* SPSP/Open Payments account URL, identifying a unique account. Use this for comparing sameness between
* accounts.
*/
toString(): string {
return this.toEndpointUrl()
}
/**
* Unique payment pointer for this SPSP or Open Payments account. Stripped trailing slash.
* Returns undefined when the protocol is not "https".
* Returns undefined when there is a query string or fragment.
*/
toPaymentPointer(): string | undefined {
if (this.protocol !== 'https:' || this.suffix !== '') return
return '$' + this.hostname + (this.path ?? '')
}
}