@atproto/xrpc-server 0.11.4 → 0.11.6
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/CHANGELOG.md +27 -0
- package/package.json +26 -21
- package/jest.config.cjs +0 -21
- package/src/auth.ts +0 -235
- package/src/errors.ts +0 -312
- package/src/index.ts +0 -14
- package/src/logger.ts +0 -8
- package/src/rate-limiter-http.ts +0 -82
- package/src/rate-limiter.ts +0 -279
- package/src/server.ts +0 -858
- package/src/stream/frames.ts +0 -125
- package/src/stream/index.ts +0 -5
- package/src/stream/logger.ts +0 -6
- package/src/stream/server.ts +0 -66
- package/src/stream/stream.ts +0 -39
- package/src/stream/subscription.ts +0 -96
- package/src/stream/types.ts +0 -27
- package/src/types.ts +0 -330
- package/src/util.ts +0 -708
- package/tests/_util.ts +0 -124
- package/tests/auth.test.ts +0 -333
- package/tests/bodies.test.ts +0 -608
- package/tests/errors.test.ts +0 -299
- package/tests/frames.test.ts +0 -135
- package/tests/ipld.test.ts +0 -97
- package/tests/parameters.test.ts +0 -331
- package/tests/parsing.test.ts +0 -89
- package/tests/procedures.test.ts +0 -176
- package/tests/queries.test.ts +0 -140
- package/tests/rate-limiter.test.ts +0 -312
- package/tests/responses.test.ts +0 -72
- package/tests/stream.test.ts +0 -169
- package/tests/subscriptions.test.ts +0 -398
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -7
package/tests/_util.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { once } from 'node:events'
|
|
2
|
-
import * as http from 'node:http'
|
|
3
|
-
import express from 'express'
|
|
4
|
-
import {
|
|
5
|
-
LexiconDocument,
|
|
6
|
-
LexiconIterableIndexer,
|
|
7
|
-
LexiconSchemaBuilder,
|
|
8
|
-
} from '@atproto/lex-document'
|
|
9
|
-
import { LexiconDoc } from '@atproto/lexicon'
|
|
10
|
-
import {
|
|
11
|
-
AuthRequiredError,
|
|
12
|
-
MethodConfigOrHandler,
|
|
13
|
-
Options,
|
|
14
|
-
Server,
|
|
15
|
-
StreamConfigOrHandler,
|
|
16
|
-
} from '../src/index.js'
|
|
17
|
-
|
|
18
|
-
// @ts-expect-error
|
|
19
|
-
Symbol.asyncDispose ??= Symbol.for('nodejs.asyncDispose')
|
|
20
|
-
|
|
21
|
-
export async function createServer({ router }: Server): Promise<http.Server> {
|
|
22
|
-
const app = express()
|
|
23
|
-
app.use(router)
|
|
24
|
-
const httpServer = app.listen(0)
|
|
25
|
-
await once(httpServer, 'listening')
|
|
26
|
-
// @NOTE Types define `http.Server` as `AsyncDisposable`, but not all
|
|
27
|
-
// environments seem to support it.
|
|
28
|
-
if (!(Symbol.asyncDispose in httpServer)) {
|
|
29
|
-
Object.defineProperty(httpServer, Symbol.asyncDispose, {
|
|
30
|
-
value: async function (this: http.Server) {
|
|
31
|
-
return closeServer(this)
|
|
32
|
-
},
|
|
33
|
-
})
|
|
34
|
-
}
|
|
35
|
-
return httpServer
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function closeServer(httpServer: http.Server) {
|
|
39
|
-
await new Promise((r) => {
|
|
40
|
-
httpServer.close(() => r(undefined))
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function createBasicAuth(allowed: {
|
|
45
|
-
username: string
|
|
46
|
-
password: string
|
|
47
|
-
}) {
|
|
48
|
-
return function (ctx: { req: http.IncomingMessage }) {
|
|
49
|
-
const header = ctx.req.headers.authorization ?? ''
|
|
50
|
-
if (!header.startsWith('Basic ')) {
|
|
51
|
-
throw new AuthRequiredError()
|
|
52
|
-
}
|
|
53
|
-
const original = header.replace('Basic ', '')
|
|
54
|
-
const [username, password] = Buffer.from(original, 'base64')
|
|
55
|
-
.toString()
|
|
56
|
-
.split(':')
|
|
57
|
-
if (username !== allowed.username || password !== allowed.password) {
|
|
58
|
-
throw new AuthRequiredError()
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
credentials: { username },
|
|
62
|
-
artifacts: { original },
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function basicAuthHeaders(creds: {
|
|
68
|
-
username: string
|
|
69
|
-
password: string
|
|
70
|
-
}) {
|
|
71
|
-
return {
|
|
72
|
-
authorization:
|
|
73
|
-
'Basic ' +
|
|
74
|
-
Buffer.from(`${creds.username}:${creds.password}`).toString('base64'),
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Builds a lexicon server based on an `@atproto/lexicon`
|
|
80
|
-
* {@link import('@atproto/lexicon').Lexicons} instance. Validation will be
|
|
81
|
-
* performed by {@link import('@atproto/lexicon').Lexicons}'s various assertion
|
|
82
|
-
* methods. This allows for testing the server's integration with
|
|
83
|
-
* `@atproto/lexicon`.
|
|
84
|
-
*/
|
|
85
|
-
export async function buildMethodLexicons(
|
|
86
|
-
lexicons: LexiconDoc[],
|
|
87
|
-
handlers: Record<string, MethodConfigOrHandler | StreamConfigOrHandler>,
|
|
88
|
-
options?: Options,
|
|
89
|
-
) {
|
|
90
|
-
const server = new Server(structuredClone(lexicons), options)
|
|
91
|
-
for (const [id, handler] of Object.entries(handlers)) {
|
|
92
|
-
const def = server.lex.getDef(id)
|
|
93
|
-
if (def?.type === 'subscription') {
|
|
94
|
-
server.addStreamMethod(id, handler as StreamConfigOrHandler)
|
|
95
|
-
} else {
|
|
96
|
-
server.method(id, handler as MethodConfigOrHandler)
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return server
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Builds a lexicon server based on `@atproto/lex`'s
|
|
104
|
-
* {@link import('@atproto/lex').Query},
|
|
105
|
-
* {@link import('@atproto/lex').Procedure}, and
|
|
106
|
-
* {@link import('@atproto/lex').Subscription} method definitions. Validation
|
|
107
|
-
* will be performed through built schema verifiers created by
|
|
108
|
-
* {@link LexiconSchemaBuilder}. This helper allows for testing the server's
|
|
109
|
-
* integration with `@atproto/lex`.
|
|
110
|
-
*/
|
|
111
|
-
export async function buildAddLexicons(
|
|
112
|
-
lexicons: LexiconDocument[],
|
|
113
|
-
handlers: Record<string, MethodConfigOrHandler | StreamConfigOrHandler>,
|
|
114
|
-
options?: Options,
|
|
115
|
-
) {
|
|
116
|
-
const server = new Server(undefined, options)
|
|
117
|
-
await using indexer = new LexiconIterableIndexer(structuredClone(lexicons))
|
|
118
|
-
await using builder = new LexiconSchemaBuilder(indexer)
|
|
119
|
-
for (const [id, handler] of Object.entries(handlers)) {
|
|
120
|
-
const schema = await builder.buildFullRef(`${id}#main`)
|
|
121
|
-
server.add(schema as any, handler as any)
|
|
122
|
-
}
|
|
123
|
-
return server
|
|
124
|
-
}
|
package/tests/auth.test.ts
DELETED
|
@@ -1,333 +0,0 @@
|
|
|
1
|
-
import { KeyObject, createPrivateKey } from 'node:crypto'
|
|
2
|
-
import * as http from 'node:http'
|
|
3
|
-
import { AddressInfo } from 'node:net'
|
|
4
|
-
import * as jose from 'jose'
|
|
5
|
-
import KeyEncoderModule from 'key-encoder'
|
|
6
|
-
import { MINUTE } from '@atproto/common'
|
|
7
|
-
import { Secp256k1Keypair } from '@atproto/crypto'
|
|
8
|
-
import { LexiconDoc } from '@atproto/lexicon'
|
|
9
|
-
import { XRPCError, XrpcClient } from '@atproto/xrpc'
|
|
10
|
-
import * as xrpcServer from '../src/index.js'
|
|
11
|
-
import {
|
|
12
|
-
basicAuthHeaders,
|
|
13
|
-
closeServer,
|
|
14
|
-
createBasicAuth,
|
|
15
|
-
createServer,
|
|
16
|
-
} from './_util.js'
|
|
17
|
-
|
|
18
|
-
// key-encoder is CJS with exports.default; Node ESM interop wraps it as { default: Class }
|
|
19
|
-
const KeyEncoder = ((m) => m.default ?? m)(KeyEncoderModule)
|
|
20
|
-
|
|
21
|
-
const LEXICONS: LexiconDoc[] = [
|
|
22
|
-
{
|
|
23
|
-
lexicon: 1,
|
|
24
|
-
id: 'io.example.authTest',
|
|
25
|
-
defs: {
|
|
26
|
-
main: {
|
|
27
|
-
type: 'procedure',
|
|
28
|
-
input: {
|
|
29
|
-
encoding: 'application/json',
|
|
30
|
-
schema: {
|
|
31
|
-
type: 'object',
|
|
32
|
-
properties: {
|
|
33
|
-
present: { type: 'boolean', const: true },
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
output: {
|
|
38
|
-
encoding: 'application/json',
|
|
39
|
-
schema: {
|
|
40
|
-
type: 'object',
|
|
41
|
-
properties: {
|
|
42
|
-
username: { type: 'string' },
|
|
43
|
-
original: { type: 'string' },
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
]
|
|
51
|
-
|
|
52
|
-
describe('Auth', () => {
|
|
53
|
-
let s: http.Server
|
|
54
|
-
const server = xrpcServer.createServer(LEXICONS)
|
|
55
|
-
server.method('io.example.authTest', {
|
|
56
|
-
auth: createBasicAuth({ username: 'admin', password: 'password' }),
|
|
57
|
-
handler: ({ auth }) => {
|
|
58
|
-
return {
|
|
59
|
-
encoding: 'application/json',
|
|
60
|
-
body: {
|
|
61
|
-
username: auth.credentials.username,
|
|
62
|
-
original: auth.artifacts.original,
|
|
63
|
-
},
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
let client: XrpcClient
|
|
69
|
-
beforeAll(async () => {
|
|
70
|
-
s = await createServer(server)
|
|
71
|
-
const { port } = s.address() as AddressInfo
|
|
72
|
-
client = new XrpcClient(`http://localhost:${port}`, LEXICONS)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
afterAll(async () => {
|
|
76
|
-
await closeServer(s)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('creates and validates service auth headers', async () => {
|
|
80
|
-
const keypair = await Secp256k1Keypair.create()
|
|
81
|
-
const iss = 'did:example:alice'
|
|
82
|
-
const aud = 'did:example:bob'
|
|
83
|
-
const token = await xrpcServer.createServiceJwt({
|
|
84
|
-
iss,
|
|
85
|
-
aud,
|
|
86
|
-
keypair,
|
|
87
|
-
lxm: null,
|
|
88
|
-
})
|
|
89
|
-
const validated = await xrpcServer.verifyJwt(token, null, null, async () =>
|
|
90
|
-
keypair.did(),
|
|
91
|
-
)
|
|
92
|
-
expect(validated.iss).toEqual(iss)
|
|
93
|
-
expect(validated.aud).toEqual(aud)
|
|
94
|
-
// should expire within the minute when no exp is provided
|
|
95
|
-
expect(validated.exp).toBeGreaterThan(Date.now() / 1000)
|
|
96
|
-
expect(validated.exp).toBeLessThan(Date.now() / 1000 + 60)
|
|
97
|
-
expect(typeof validated.jti).toBe('string')
|
|
98
|
-
expect(validated.lxm).toBeUndefined()
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
it('creates and validates service auth headers bound to a particular method', async () => {
|
|
102
|
-
const keypair = await Secp256k1Keypair.create()
|
|
103
|
-
const iss = 'did:example:alice'
|
|
104
|
-
const aud = 'did:example:bob'
|
|
105
|
-
const lxm = 'com.atproto.repo.createRecord'
|
|
106
|
-
const token = await xrpcServer.createServiceJwt({
|
|
107
|
-
iss,
|
|
108
|
-
aud,
|
|
109
|
-
keypair,
|
|
110
|
-
lxm,
|
|
111
|
-
})
|
|
112
|
-
const validated = await xrpcServer.verifyJwt(token, null, lxm, async () =>
|
|
113
|
-
keypair.did(),
|
|
114
|
-
)
|
|
115
|
-
expect(validated.iss).toEqual(iss)
|
|
116
|
-
expect(validated.aud).toEqual(aud)
|
|
117
|
-
expect(validated.lxm).toEqual(lxm)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('fails on bad auth before invalid request payload.', async () => {
|
|
121
|
-
try {
|
|
122
|
-
await client.call(
|
|
123
|
-
'io.example.authTest',
|
|
124
|
-
{},
|
|
125
|
-
{ present: false },
|
|
126
|
-
{
|
|
127
|
-
headers: basicAuthHeaders({
|
|
128
|
-
username: 'admin',
|
|
129
|
-
password: 'wrong',
|
|
130
|
-
}),
|
|
131
|
-
},
|
|
132
|
-
)
|
|
133
|
-
throw new Error('Didnt throw')
|
|
134
|
-
} catch (e: any) {
|
|
135
|
-
expect(e).toBeInstanceOf(XRPCError)
|
|
136
|
-
expect(e.success).toBeFalsy()
|
|
137
|
-
expect(e.error).toBe('AuthenticationRequired')
|
|
138
|
-
expect(e.message).toBe('Authentication Required')
|
|
139
|
-
expect(e.status).toBe(401)
|
|
140
|
-
}
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
it('fails on invalid request payload after good auth.', async () => {
|
|
144
|
-
try {
|
|
145
|
-
await client.call(
|
|
146
|
-
'io.example.authTest',
|
|
147
|
-
{},
|
|
148
|
-
{ present: false },
|
|
149
|
-
{
|
|
150
|
-
headers: basicAuthHeaders({
|
|
151
|
-
username: 'admin',
|
|
152
|
-
password: 'password',
|
|
153
|
-
}),
|
|
154
|
-
},
|
|
155
|
-
)
|
|
156
|
-
throw new Error('Didnt throw')
|
|
157
|
-
} catch (e: any) {
|
|
158
|
-
expect(e).toBeInstanceOf(XRPCError)
|
|
159
|
-
expect(e.success).toBeFalsy()
|
|
160
|
-
expect(e.error).toBe('InvalidRequest')
|
|
161
|
-
expect(e.message).toBe('Input/present must be true')
|
|
162
|
-
expect(e.status).toBe(400)
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
it('succeeds on good auth and payload.', async () => {
|
|
167
|
-
const res = await client.call(
|
|
168
|
-
'io.example.authTest',
|
|
169
|
-
{},
|
|
170
|
-
{ present: true },
|
|
171
|
-
{
|
|
172
|
-
headers: basicAuthHeaders({
|
|
173
|
-
username: 'admin',
|
|
174
|
-
password: 'password',
|
|
175
|
-
}),
|
|
176
|
-
},
|
|
177
|
-
)
|
|
178
|
-
expect(res.success).toBe(true)
|
|
179
|
-
expect(res.data).toEqual({
|
|
180
|
-
username: 'admin',
|
|
181
|
-
original: 'YWRtaW46cGFzc3dvcmQ=',
|
|
182
|
-
})
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
describe('verifyJwt()', () => {
|
|
186
|
-
it('fails on expired jwt.', async () => {
|
|
187
|
-
const keypair = await Secp256k1Keypair.create()
|
|
188
|
-
const jwt = await xrpcServer.createServiceJwt({
|
|
189
|
-
aud: 'did:example:aud',
|
|
190
|
-
iss: 'did:example:iss',
|
|
191
|
-
keypair,
|
|
192
|
-
exp: Math.floor((Date.now() - MINUTE) / 1000),
|
|
193
|
-
lxm: null,
|
|
194
|
-
})
|
|
195
|
-
const tryVerify = xrpcServer.verifyJwt(
|
|
196
|
-
jwt,
|
|
197
|
-
'did:example:aud',
|
|
198
|
-
null,
|
|
199
|
-
async () => {
|
|
200
|
-
return keypair.did()
|
|
201
|
-
},
|
|
202
|
-
)
|
|
203
|
-
await expect(tryVerify).rejects.toThrow('jwt expired')
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('fails on bad audience.', async () => {
|
|
207
|
-
const keypair = await Secp256k1Keypair.create()
|
|
208
|
-
const jwt = await xrpcServer.createServiceJwt({
|
|
209
|
-
aud: 'did:example:aud1',
|
|
210
|
-
iss: 'did:example:iss',
|
|
211
|
-
keypair,
|
|
212
|
-
lxm: null,
|
|
213
|
-
})
|
|
214
|
-
const tryVerify = xrpcServer.verifyJwt(
|
|
215
|
-
jwt,
|
|
216
|
-
'did:example:aud2',
|
|
217
|
-
null,
|
|
218
|
-
async () => {
|
|
219
|
-
return keypair.did()
|
|
220
|
-
},
|
|
221
|
-
)
|
|
222
|
-
await expect(tryVerify).rejects.toThrow(
|
|
223
|
-
'jwt audience does not match service did',
|
|
224
|
-
)
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('fails on bad lxm', async () => {
|
|
228
|
-
const keypair = await Secp256k1Keypair.create()
|
|
229
|
-
const jwt = await xrpcServer.createServiceJwt({
|
|
230
|
-
aud: 'did:example:aud1',
|
|
231
|
-
iss: 'did:example:iss',
|
|
232
|
-
keypair,
|
|
233
|
-
lxm: 'com.atproto.repo.createRecord',
|
|
234
|
-
})
|
|
235
|
-
const tryVerify = xrpcServer.verifyJwt(
|
|
236
|
-
jwt,
|
|
237
|
-
'did:example:aud1',
|
|
238
|
-
'com.atproto.repo.putRecord',
|
|
239
|
-
async () => {
|
|
240
|
-
return keypair.did()
|
|
241
|
-
},
|
|
242
|
-
)
|
|
243
|
-
await expect(tryVerify).rejects.toThrow(/bad jwt lexicon method/)
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
it('fails on null lxm when lxm is required', async () => {
|
|
247
|
-
const keypair = await Secp256k1Keypair.create()
|
|
248
|
-
const jwt = await xrpcServer.createServiceJwt({
|
|
249
|
-
aud: 'did:example:aud1',
|
|
250
|
-
iss: 'did:example:iss',
|
|
251
|
-
keypair,
|
|
252
|
-
lxm: null,
|
|
253
|
-
})
|
|
254
|
-
const tryVerify = xrpcServer.verifyJwt(
|
|
255
|
-
jwt,
|
|
256
|
-
'did:example:aud1',
|
|
257
|
-
'com.atproto.repo.putRecord',
|
|
258
|
-
async () => {
|
|
259
|
-
return keypair.did()
|
|
260
|
-
},
|
|
261
|
-
)
|
|
262
|
-
await expect(tryVerify).rejects.toThrow(/missing jwt lexicon method/)
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
it('refreshes key on verification failure.', async () => {
|
|
266
|
-
const keypair1 = await Secp256k1Keypair.create()
|
|
267
|
-
const keypair2 = await Secp256k1Keypair.create()
|
|
268
|
-
const jwt = await xrpcServer.createServiceJwt({
|
|
269
|
-
aud: 'did:example:aud',
|
|
270
|
-
iss: 'did:example:iss',
|
|
271
|
-
keypair: keypair2,
|
|
272
|
-
lxm: null,
|
|
273
|
-
})
|
|
274
|
-
let usedKeypair1 = false
|
|
275
|
-
let usedKeypair2 = false
|
|
276
|
-
const tryVerify = xrpcServer.verifyJwt(
|
|
277
|
-
jwt,
|
|
278
|
-
'did:example:aud',
|
|
279
|
-
null,
|
|
280
|
-
async (_did, forceRefresh) => {
|
|
281
|
-
if (forceRefresh) {
|
|
282
|
-
usedKeypair2 = true
|
|
283
|
-
return keypair2.did()
|
|
284
|
-
} else {
|
|
285
|
-
usedKeypair1 = true
|
|
286
|
-
return keypair1.did()
|
|
287
|
-
}
|
|
288
|
-
},
|
|
289
|
-
)
|
|
290
|
-
await expect(tryVerify).resolves.toMatchObject({
|
|
291
|
-
aud: 'did:example:aud',
|
|
292
|
-
iss: 'did:example:iss',
|
|
293
|
-
})
|
|
294
|
-
expect(usedKeypair1).toBe(true)
|
|
295
|
-
expect(usedKeypair2).toBe(true)
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
it('interoperates with jwts signed by other libraries.', async () => {
|
|
299
|
-
const keypair = await Secp256k1Keypair.create({ exportable: true })
|
|
300
|
-
const signingKey = await createPrivateKeyObject(keypair)
|
|
301
|
-
const payload = {
|
|
302
|
-
aud: 'did:example:aud',
|
|
303
|
-
iss: 'did:example:iss',
|
|
304
|
-
exp: Math.floor((Date.now() + MINUTE) / 1000),
|
|
305
|
-
}
|
|
306
|
-
const jwt = await new jose.SignJWT(payload)
|
|
307
|
-
.setProtectedHeader({ typ: 'JWT', alg: keypair.jwtAlg })
|
|
308
|
-
.sign(signingKey)
|
|
309
|
-
const tryVerify = xrpcServer.verifyJwt(
|
|
310
|
-
jwt,
|
|
311
|
-
'did:example:aud',
|
|
312
|
-
null,
|
|
313
|
-
async () => {
|
|
314
|
-
return keypair.did()
|
|
315
|
-
},
|
|
316
|
-
)
|
|
317
|
-
await expect(tryVerify).resolves.toEqual(payload)
|
|
318
|
-
})
|
|
319
|
-
})
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
const createPrivateKeyObject = async (
|
|
323
|
-
privateKey: Secp256k1Keypair,
|
|
324
|
-
): Promise<KeyObject> => {
|
|
325
|
-
const raw = await privateKey.export()
|
|
326
|
-
const encoder = new KeyEncoder('secp256k1')
|
|
327
|
-
const key = encoder.encodePrivate(
|
|
328
|
-
Buffer.from(raw).toString('hex'),
|
|
329
|
-
'raw',
|
|
330
|
-
'pem',
|
|
331
|
-
)
|
|
332
|
-
return createPrivateKey({ format: 'pem', key })
|
|
333
|
-
}
|