/
auth.ts
185 lines (155 loc) · 4.93 KB
/
auth.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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import { base58btc } from 'multiformats/bases/base58'
import * as varint from 'varint'
import nacl from 'tweetnacl'
/**
* Request tag indicating what blockchain will be used to mint. Currently, the value
* will always be set to `"solana"` and cannot be overridden by the user.
*/
export const TagChain = 'chain'
/**
* Request tag indicating which [Solana cluster](https://docs.solana.com/clusters) will be
* used to mint.
*
* Currently this library will accept any string value, however it is strongly
* recommended that you use one of these "canonical" values: `"devnet"`, `"mainnet-beta"`, `"testnet"`.
* This may be enforced by the backend at a later date.
*/
export const TagSolanaCluster = 'solanaCluster'
/**
* Request tag indicating which "user agent" or tool is being used to prepare the upload. This should be
* set to a string that includes the name of the tool or platform.
*
* Projects using this library are free to choose their own value for this tag, however you should avoid
* changing the name over time, unless the project itself changes names (for example, due to a community fork or re-branding).
*
* For personal projects or individuals creating tools that are not affiliated with a public platform, please set the
* value to a URL for your code repository. If your code is not yet public, please create a repository containing a
* description of the project and links to its public-facing interface.
*
* Examples of suitable values:
*
* - `"metaplex/candy-machine-cli"`
* - `"metaplex/js-sdk"`
* - `"magiceden/mint-authority"`
* - `"https://github.com/samuelvanderwaal/metaboss"`
*
*/
export const TagMintingAgent = 'mintingAgent'
/**
* Optional request tag indicating which version of the "minting agent" was used to prepare the request.
* This may contain arbitrary text, as each project may have their own versioning scheme.
*/
export const TagMintingAgentVersion = 'agentVersion'
const DEFAULT_CLUSTER = 'devnet'
const MulticodecEd25519Pubkey = varint.encode(0xed)
export type SolanaCluster = string
export interface AuthContext {
chain: 'solana'
solanaCluster: SolanaCluster
mintingAgent: string
agentVersion?: string
signMessage: Signer
publicKey: Uint8Array
}
export type Signer = (message: Uint8Array) => Promise<Uint8Array>
export interface RequestContext {
message: RequestMessage
messageBytes: Uint8Array
mintDID: string
signature: Uint8Array
}
export function MetaplexAuthWithSigner(
signMessage: Signer,
publicKey: Uint8Array,
opts: {
mintingAgent: string
agentVersion?: string
solanaCluster?: SolanaCluster
}
): AuthContext {
const chain = 'solana'
const solanaCluster = opts.solanaCluster || DEFAULT_CLUSTER
const { mintingAgent, agentVersion } = opts
if (!mintingAgent) {
throw new Error('required option "mintingAgent" not provided')
}
return {
chain,
solanaCluster,
mintingAgent,
agentVersion,
signMessage,
publicKey,
}
}
export function MetaplexAuthWithSecretKey(
privkey: Uint8Array,
opts: {
mintingAgent: string
agentVersion?: string
solanaCluster?: SolanaCluster
}
): AuthContext {
const { publicKey, secretKey } = nacl.sign.keyPair.fromSecretKey(privkey)
const signMessage = async (message: Uint8Array) => {
return nacl.sign.detached(message, secretKey)
}
return MetaplexAuthWithSigner(signMessage, publicKey, opts)
}
export async function makeMetaplexUploadToken(
auth: AuthContext,
rootCID: string
): Promise<string> {
const tags = {
[TagChain]: auth.chain,
[TagSolanaCluster]: auth.solanaCluster,
[TagMintingAgent]: auth.mintingAgent,
[TagMintingAgentVersion]: auth.agentVersion,
}
const req = {
put: {
rootCID,
tags,
},
}
const iss = keyDID(auth.publicKey)
const payload = {
iss,
req,
}
const headerB64 = objectToB64URL({ alg: 'EdDSA', typ: 'JWT' })
const payloadB64 = objectToB64URL(payload)
const encoded = headerB64 + '.' + payloadB64
const encodedBytes = new TextEncoder().encode(encoded)
const sig = await auth.signMessage(encodedBytes)
const sigB64 = b64urlEncode(sig)
const token = encoded + '.' + sigB64
return token
}
export function keyDID(pubkey: Uint8Array): string {
const keyWithCodec = new Uint8Array([...MulticodecEd25519Pubkey, ...pubkey])
const mb = base58btc.encode(keyWithCodec)
return `did:key:${mb}`
}
function objectToB64URL(o: object): string {
const s = new TextEncoder().encode(JSON.stringify(o))
return b64urlEncode(s)
}
function b64urlEncode(bytes: Uint8Array): string {
const s = b64Encode(bytes)
return s.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_')
}
function b64Encode(bytes: Uint8Array): string {
if (Buffer !== undefined) {
return Buffer.from(bytes).toString('base64')
}
return btoa(String.fromCharCode.apply(null, [...bytes]))
}
// internal types
interface PutCarRequest {
rootCID: string
tags: Record<string, string>
}
interface RequestMessage {
put?: PutCarRequest
}