@atproto/dev-env 0.0.0-spaces-alpha-20260818163953
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 +3424 -0
- package/LICENSE.txt +7 -0
- package/README.md +39 -0
- package/bin.js +7 -0
- package/dist/bin-multi-pds.d.ts +2 -0
- package/dist/bin-multi-pds.d.ts.map +1 -0
- package/dist/bin-multi-pds.js +130 -0
- package/dist/bin-multi-pds.js.map +1 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +56 -0
- package/dist/bin.js.map +1 -0
- package/dist/bsky.d.ts +27 -0
- package/dist/bsky.d.ts.map +1 -0
- package/dist/bsky.js +165 -0
- package/dist/bsky.js.map +1 -0
- package/dist/bsync.d.ts +22 -0
- package/dist/bsync.d.ts.map +1 -0
- package/dist/bsync.js +54 -0
- package/dist/bsync.js.map +1 -0
- package/dist/const.d.ts +4 -0
- package/dist/const.d.ts.map +1 -0
- package/dist/const.js +4 -0
- package/dist/const.js.map +1 -0
- package/dist/env.d.ts +2 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +10 -0
- package/dist/env.js.map +1 -0
- package/dist/feed-gen.d.ts +15 -0
- package/dist/feed-gen.d.ts.map +1 -0
- package/dist/feed-gen.js +73 -0
- package/dist/feed-gen.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/introspect.d.ts +22 -0
- package/dist/introspect.d.ts.map +1 -0
- package/dist/introspect.js +65 -0
- package/dist/introspect.js.map +1 -0
- package/dist/mock/data.d.ts +3 -0
- package/dist/mock/data.d.ts.map +1 -0
- package/dist/mock/data.js +46 -0
- package/dist/mock/data.js.map +1 -0
- package/dist/mock/img/blur-hash-avatar-b64.d.ts +3 -0
- package/dist/mock/img/blur-hash-avatar-b64.d.ts.map +1 -0
- package/dist/mock/img/blur-hash-avatar-b64.js +2 -0
- package/dist/mock/img/blur-hash-avatar-b64.js.map +1 -0
- package/dist/mock/img/labeled-img-b64.d.ts +3 -0
- package/dist/mock/img/labeled-img-b64.d.ts.map +1 -0
- package/dist/mock/img/labeled-img-b64.js +2 -0
- package/dist/mock/img/labeled-img-b64.js.map +1 -0
- package/dist/mock/index.d.ts +3 -0
- package/dist/mock/index.d.ts.map +1 -0
- package/dist/mock/index.js +574 -0
- package/dist/mock/index.js.map +1 -0
- package/dist/moderator-client.d.ts +58 -0
- package/dist/moderator-client.d.ts.map +1 -0
- package/dist/moderator-client.js +110 -0
- package/dist/moderator-client.js.map +1 -0
- package/dist/network-no-appview.d.ts +21 -0
- package/dist/network-no-appview.d.ts.map +1 -0
- package/dist/network-no-appview.js +74 -0
- package/dist/network-no-appview.js.map +1 -0
- package/dist/network.d.ts +32 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +190 -0
- package/dist/network.js.map +1 -0
- package/dist/ozone.d.ts +31 -0
- package/dist/ozone.d.ts.map +1 -0
- package/dist/ozone.js +254 -0
- package/dist/ozone.js.map +1 -0
- package/dist/pds.d.ts +23 -0
- package/dist/pds.d.ts.map +1 -0
- package/dist/pds.js +97 -0
- package/dist/pds.js.map +1 -0
- package/dist/plc.d.ts +14 -0
- package/dist/plc.d.ts.map +1 -0
- package/dist/plc.js +28 -0
- package/dist/plc.js.map +1 -0
- package/dist/seed/author-feed.d.ts +4 -0
- package/dist/seed/author-feed.d.ts.map +1 -0
- package/dist/seed/author-feed.js +50 -0
- package/dist/seed/author-feed.js.map +1 -0
- package/dist/seed/basic.d.ts +16 -0
- package/dist/seed/basic.d.ts.map +1 -0
- package/dist/seed/basic.js +113 -0
- package/dist/seed/basic.js.map +1 -0
- package/dist/seed/client.d.ts +151 -0
- package/dist/seed/client.d.ts.map +1 -0
- package/dist/seed/client.js +365 -0
- package/dist/seed/client.js.map +1 -0
- package/dist/seed/follows.d.ts +4 -0
- package/dist/seed/follows.d.ts.map +1 -0
- package/dist/seed/follows.js +55 -0
- package/dist/seed/follows.js.map +1 -0
- package/dist/seed/index.d.ts +11 -0
- package/dist/seed/index.d.ts.map +1 -0
- package/dist/seed/index.js +11 -0
- package/dist/seed/index.js.map +1 -0
- package/dist/seed/likes.d.ts +4 -0
- package/dist/seed/likes.d.ts.map +1 -0
- package/dist/seed/likes.js +37 -0
- package/dist/seed/likes.js.map +1 -0
- package/dist/seed/quotes.d.ts +4 -0
- package/dist/seed/quotes.d.ts.map +1 -0
- package/dist/seed/quotes.js +21 -0
- package/dist/seed/quotes.js.map +1 -0
- package/dist/seed/reposts.d.ts +4 -0
- package/dist/seed/reposts.d.ts.map +1 -0
- package/dist/seed/reposts.js +17 -0
- package/dist/seed/reposts.js.map +1 -0
- package/dist/seed/thread-v2.d.ts +227 -0
- package/dist/seed/thread-v2.d.ts.map +1 -0
- package/dist/seed/thread-v2.js +755 -0
- package/dist/seed/thread-v2.js.map +1 -0
- package/dist/seed/users-bulk.d.ts +3 -0
- package/dist/seed/users-bulk.d.ts.map +1 -0
- package/dist/seed/users-bulk.js +224 -0
- package/dist/seed/users-bulk.js.map +1 -0
- package/dist/seed/users.d.ts +3 -0
- package/dist/seed/users.d.ts.map +1 -0
- package/dist/seed/users.js +45 -0
- package/dist/seed/users.js.map +1 -0
- package/dist/seed/verifications.d.ts +5 -0
- package/dist/seed/verifications.d.ts.map +1 -0
- package/dist/seed/verifications.js +164 -0
- package/dist/seed/verifications.js.map +1 -0
- package/dist/service-profile-lexicon.d.ts +11 -0
- package/dist/service-profile-lexicon.d.ts.map +1 -0
- package/dist/service-profile-lexicon.js +115 -0
- package/dist/service-profile-lexicon.js.map +1 -0
- package/dist/service-profile-ozone.d.ts +18 -0
- package/dist/service-profile-ozone.d.ts.map +1 -0
- package/dist/service-profile-ozone.js +333 -0
- package/dist/service-profile-ozone.js.map +1 -0
- package/dist/service-profile.d.ts +24 -0
- package/dist/service-profile.d.ts.map +1 -0
- package/dist/service-profile.js +61 -0
- package/dist/service-profile.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +14 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +86 -0
- package/dist/util.js.map +1 -0
- package/package.json +69 -0
- package/src/bin-multi-pds.ts +166 -0
- package/src/bin.ts +64 -0
- package/src/bsky.ts +198 -0
- package/src/bsync.ts +66 -0
- package/src/const.ts +3 -0
- package/src/env.ts +9 -0
- package/src/feed-gen.ts +96 -0
- package/src/index.ts +17 -0
- package/src/introspect.ts +91 -0
- package/src/mock/data.ts +46 -0
- package/src/mock/img/blur-hash-avatar-b64.ts +1 -0
- package/src/mock/img/labeled-img-b64.ts +1 -0
- package/src/mock/index.ts +702 -0
- package/src/moderator-client.ts +267 -0
- package/src/network-no-appview.ts +89 -0
- package/src/network.ts +243 -0
- package/src/ozone.ts +287 -0
- package/src/pds.ts +118 -0
- package/src/plc.ts +33 -0
- package/src/seed/author-feed.ts +98 -0
- package/src/seed/basic.ts +189 -0
- package/src/seed/client.ts +676 -0
- package/src/seed/follows.ts +57 -0
- package/src/seed/index.ts +11 -0
- package/src/seed/likes.ts +43 -0
- package/src/seed/quotes.ts +53 -0
- package/src/seed/reposts.ts +18 -0
- package/src/seed/thread-v2.ts +925 -0
- package/src/seed/users-bulk.ts +229 -0
- package/src/seed/users.ts +64 -0
- package/src/seed/verifications.ts +254 -0
- package/src/service-profile-lexicon.ts +127 -0
- package/src/service-profile-ozone.ts +375 -0
- package/src/service-profile.ts +95 -0
- package/src/types.ts +68 -0
- package/src/util.ts +118 -0
- package/tsconfig.build.json +25 -0
- package/tsconfig.json +4 -0
package/dist/util.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as plc from '@did-plc/lib';
|
|
2
|
+
import { request } from 'undici';
|
|
3
|
+
import { Secp256k1Keypair } from '@atproto/crypto';
|
|
4
|
+
export const mockNetworkUtilities = (pds, bsky) => {
|
|
5
|
+
const pdses = Array.isArray(pds) ? pds : [pds];
|
|
6
|
+
for (const p of pdses) {
|
|
7
|
+
mockResolvers(p.ctx.idResolver, pdses);
|
|
8
|
+
}
|
|
9
|
+
if (bsky) {
|
|
10
|
+
mockResolvers(bsky.ctx.idResolver, pdses);
|
|
11
|
+
mockResolvers(bsky.dataplane.idResolver, pdses);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export const mockResolvers = (idResolver, pds,
|
|
15
|
+
// handles resolve against the matching pds by default, but may point
|
|
16
|
+
// elsewhere e.g. an entryway
|
|
17
|
+
handleResolveUrl) => {
|
|
18
|
+
const pdses = Array.isArray(pds) ? pds : [pds];
|
|
19
|
+
// Map pds public url to its local url when resolving from plc
|
|
20
|
+
const origResolveDid = idResolver.did.resolveNoCache;
|
|
21
|
+
idResolver.did.resolveNoCache = async (did) => {
|
|
22
|
+
const result = await origResolveDid.call(idResolver.did, did);
|
|
23
|
+
const service = result?.service?.find((svc) => svc.id === '#atproto_pds');
|
|
24
|
+
if (typeof service?.serviceEndpoint === 'string') {
|
|
25
|
+
for (const p of pdses) {
|
|
26
|
+
service.serviceEndpoint = service.serviceEndpoint.replace(p.ctx.cfg.service.publicUrl, `http://localhost:${p.port}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
const origResolveHandleDns = idResolver.handle.resolveDns;
|
|
32
|
+
idResolver.handle.resolve = async (handle) => {
|
|
33
|
+
// Handle domains across PDSes are disjoint suffixes (.test, .test2, ...);
|
|
34
|
+
// first match wins.
|
|
35
|
+
const match = pdses.find((p) => p.ctx.cfg.identity.serviceHandleDomains.some((d) => handle.endsWith(d)));
|
|
36
|
+
if (!match) {
|
|
37
|
+
return origResolveHandleDns.call(idResolver.handle, handle);
|
|
38
|
+
}
|
|
39
|
+
const url = new URL(`/.well-known/atproto-did`, handleResolveUrl ?? match.url);
|
|
40
|
+
try {
|
|
41
|
+
const res = await request(url, { headers: { host: handle } });
|
|
42
|
+
if (res.statusCode !== 200) {
|
|
43
|
+
await res.body.dump();
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return res.body.text();
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export const mockMailer = (pds) => {
|
|
54
|
+
const mailer = pds.ctx.mailer;
|
|
55
|
+
const _origSendMail = mailer.transporter.sendMail;
|
|
56
|
+
mailer.transporter.sendMail = async (opts) => {
|
|
57
|
+
const result = await _origSendMail.call(mailer.transporter, opts);
|
|
58
|
+
console.log(`✉️ Email: ${JSON.stringify(result, null, 2)}`);
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const usedLockIds = new Set();
|
|
63
|
+
export const uniqueLockId = () => {
|
|
64
|
+
let lockId;
|
|
65
|
+
do {
|
|
66
|
+
lockId = 1000 + Math.ceil(1000 * Math.random());
|
|
67
|
+
} while (usedLockIds.has(lockId));
|
|
68
|
+
usedLockIds.add(lockId);
|
|
69
|
+
return lockId;
|
|
70
|
+
};
|
|
71
|
+
export const createDidAndKey = async (opts) => {
|
|
72
|
+
const { plcUrl, handle, pds } = opts;
|
|
73
|
+
const key = await Secp256k1Keypair.create({ exportable: true });
|
|
74
|
+
const did = await new plc.Client(plcUrl).createDid({
|
|
75
|
+
signingKey: key.did(),
|
|
76
|
+
rotationKeys: [key.did()],
|
|
77
|
+
handle,
|
|
78
|
+
pds,
|
|
79
|
+
signer: key,
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
key,
|
|
83
|
+
did,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=util.js.map
|
package/dist/util.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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","sourcesContent":["import * as plc from '@did-plc/lib'\nimport { request } from 'undici'\nimport { Secp256k1Keypair } from '@atproto/crypto'\nimport type { IdResolver } from '@atproto/identity'\nimport type { TestBsky } from './bsky.js'\nimport type { TestPds } from './pds.js'\nimport type { DidAndKey } from './types.js'\n\nexport const mockNetworkUtilities = (\n pds: TestPds | TestPds[],\n bsky?: TestBsky,\n) => {\n const pdses = Array.isArray(pds) ? pds : [pds]\n for (const p of pdses) {\n mockResolvers(p.ctx.idResolver, pdses)\n }\n if (bsky) {\n mockResolvers(bsky.ctx.idResolver, pdses)\n mockResolvers(bsky.dataplane.idResolver, pdses)\n }\n}\n\nexport const mockResolvers = (\n idResolver: IdResolver,\n pds: TestPds | TestPds[],\n // handles resolve against the matching pds by default, but may point\n // elsewhere e.g. an entryway\n handleResolveUrl?: string,\n) => {\n const pdses = Array.isArray(pds) ? pds : [pds]\n\n // Map pds public url to its local url when resolving from plc\n const origResolveDid = idResolver.did.resolveNoCache\n idResolver.did.resolveNoCache = async (did: string) => {\n const result = await (origResolveDid.call(\n idResolver.did,\n did,\n ) as ReturnType<typeof origResolveDid>)\n const service = result?.service?.find((svc) => svc.id === '#atproto_pds')\n if (typeof service?.serviceEndpoint === 'string') {\n for (const p of pdses) {\n service.serviceEndpoint = service.serviceEndpoint.replace(\n p.ctx.cfg.service.publicUrl,\n `http://localhost:${p.port}`,\n )\n }\n }\n return result\n }\n\n const origResolveHandleDns = idResolver.handle.resolveDns\n idResolver.handle.resolve = async (handle: string) => {\n // Handle domains across PDSes are disjoint suffixes (.test, .test2, ...);\n // first match wins.\n const match = pdses.find((p) =>\n p.ctx.cfg.identity.serviceHandleDomains.some((d) => handle.endsWith(d)),\n )\n if (!match) {\n return origResolveHandleDns.call(idResolver.handle, handle)\n }\n\n const url = new URL(\n `/.well-known/atproto-did`,\n handleResolveUrl ?? match.url,\n )\n try {\n const res = await request(url, { headers: { host: handle } })\n if (res.statusCode !== 200) {\n await res.body.dump()\n return undefined\n }\n\n return res.body.text()\n } catch (err) {\n return undefined\n }\n }\n}\n\nexport const mockMailer = (pds: TestPds) => {\n const mailer = pds.ctx.mailer\n const _origSendMail = mailer.transporter.sendMail\n mailer.transporter.sendMail = async (opts) => {\n const result = await _origSendMail.call(mailer.transporter, opts)\n console.log(`✉️ Email: ${JSON.stringify(result, null, 2)}`)\n return result\n }\n}\n\nconst usedLockIds = new Set()\nexport const uniqueLockId = () => {\n let lockId: number\n do {\n lockId = 1000 + Math.ceil(1000 * Math.random())\n } while (usedLockIds.has(lockId))\n usedLockIds.add(lockId)\n return lockId\n}\n\nexport const createDidAndKey = async (opts: {\n plcUrl: string\n handle: string\n pds: string\n}): Promise<DidAndKey> => {\n const { plcUrl, handle, pds } = opts\n const key = await Secp256k1Keypair.create({ exportable: true })\n const did = await new plc.Client(plcUrl).createDid({\n signingKey: key.did(),\n rotationKeys: [key.did()],\n handle,\n pds,\n signer: key,\n })\n return {\n key,\n did,\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@atproto/dev-env",
|
|
3
|
+
"version": "0.0.0-spaces-alpha-20260818163953",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Local development environment helper for atproto development",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"atproto"
|
|
8
|
+
],
|
|
9
|
+
"homepage": "https://atproto.com",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "https://github.com/bluesky-social/atproto",
|
|
13
|
+
"directory": "packages/dev-env"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"./src",
|
|
17
|
+
"./tsconfig.json",
|
|
18
|
+
"./tsconfig.build.json",
|
|
19
|
+
"./dist",
|
|
20
|
+
"./README.md",
|
|
21
|
+
"./CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@did-plc/lib": "^0.0.1",
|
|
35
|
+
"@did-plc/server": "^0.0.1",
|
|
36
|
+
"dotenv": "^16.0.3",
|
|
37
|
+
"express": "^4.18.2",
|
|
38
|
+
"get-port": "^5.1.1",
|
|
39
|
+
"http-terminator": "^3.2.0",
|
|
40
|
+
"multiformats": "^13.0.0",
|
|
41
|
+
"uint8arrays": "^5.0.0",
|
|
42
|
+
"undici": "^8.5.0",
|
|
43
|
+
"@atproto/api": "^0.0.0-spaces-alpha-20260818163953",
|
|
44
|
+
"@atproto/bsky": "^0.0.0-spaces-alpha-20260818163953",
|
|
45
|
+
"@atproto/common-web": "^0.0.0-spaces-alpha-20260818163953",
|
|
46
|
+
"@atproto/identity": "^0.0.0-spaces-alpha-20260818163953",
|
|
47
|
+
"@atproto/bsync": "^0.0.0-spaces-alpha-20260818163953",
|
|
48
|
+
"@atproto/lex": "^0.0.0-spaces-alpha-20260818163953",
|
|
49
|
+
"@atproto/lexicon": "^0.0.0-spaces-alpha-20260818163953",
|
|
50
|
+
"@atproto/crypto": "^0.0.0-spaces-alpha-20260818163953",
|
|
51
|
+
"@atproto/ozone": "^0.0.0-spaces-alpha-20260818163953",
|
|
52
|
+
"@atproto/pds": "^0.0.0-spaces-alpha-20260818163953",
|
|
53
|
+
"@atproto/sync": "^0.0.0-spaces-alpha-20260818163953",
|
|
54
|
+
"@atproto/xrpc-server": "^0.0.0-spaces-alpha-20260818163953",
|
|
55
|
+
"@atproto/syntax": "^0.0.0-spaces-alpha-20260818163953"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/express": "^4.17.13"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "tsc --build tsconfig.build.json",
|
|
62
|
+
"start": "../dev-infra/with-test-redis-and-db.sh node --enable-source-maps dist/bin.js",
|
|
63
|
+
"dev": "../dev-infra/with-test-redis-and-db.sh node --enable-source-maps --watch dist/bin.js",
|
|
64
|
+
"start:multi-pds": "node --enable-source-maps dist/bin-multi-pds.js"
|
|
65
|
+
},
|
|
66
|
+
"bin": {
|
|
67
|
+
"dev-env": "bin.js"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import './env.js'
|
|
2
|
+
import getPort from 'get-port'
|
|
3
|
+
import { IntrospectServer } from './introspect.js'
|
|
4
|
+
import { TestPds } from './pds.js'
|
|
5
|
+
import { TestPlc } from './plc.js'
|
|
6
|
+
import { LexiconAuthorityProfile } from './service-profile-lexicon.js'
|
|
7
|
+
import { mockMailer, mockNetworkUtilities } from './util.js'
|
|
8
|
+
|
|
9
|
+
const INTROSPECT_PORT = 2581
|
|
10
|
+
const PRIMARY_PDS_PORT = 2583
|
|
11
|
+
const PLC_PORT = 2582
|
|
12
|
+
|
|
13
|
+
// The lex-authority account hosts permission-set / space lexicon docs that
|
|
14
|
+
// dev-env PDSes resolve via `lexiconDidAuthority`. Demo apps can log into this
|
|
15
|
+
// account to publish new lexicons (e.g. `space`-typed documents) at runtime.
|
|
16
|
+
const LEX_AUTHORITY_USER = {
|
|
17
|
+
email: 'lex-authority@test.com',
|
|
18
|
+
handle: 'lex-authority.test',
|
|
19
|
+
password: 'hunter2',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const parsePdsCount = (): number => {
|
|
23
|
+
const arg = process.argv[2]
|
|
24
|
+
if (arg === undefined) return 3
|
|
25
|
+
const n = Number.parseInt(arg, 10)
|
|
26
|
+
if (!Number.isFinite(n) || n < 1) {
|
|
27
|
+
throw new Error(`Invalid PDS count "${arg}": must be an integer >= 1`)
|
|
28
|
+
}
|
|
29
|
+
return n
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const run = async () => {
|
|
33
|
+
const totalPdsCount = parsePdsCount()
|
|
34
|
+
const extraPdsCount = totalPdsCount - 1
|
|
35
|
+
|
|
36
|
+
console.log(`
|
|
37
|
+
██████╗
|
|
38
|
+
██╔═══██╗
|
|
39
|
+
██║██╗██║
|
|
40
|
+
██║██║██║
|
|
41
|
+
╚█║████╔╝
|
|
42
|
+
╚╝╚═══╝ protocol
|
|
43
|
+
|
|
44
|
+
[ created by Bluesky ]`)
|
|
45
|
+
|
|
46
|
+
const plc = await TestPlc.create({ port: PLC_PORT })
|
|
47
|
+
|
|
48
|
+
// Step 1 — scratch PDS. We need an existing PDS to host the lex-authority
|
|
49
|
+
// account creation, but we don't want the scratch to live past startup. The
|
|
50
|
+
// account migrates to the primary PDS once we know its DID.
|
|
51
|
+
const scratchPds = await TestPds.create({
|
|
52
|
+
didPlcUrl: plc.url,
|
|
53
|
+
inviteRequired: false,
|
|
54
|
+
port: await getPort(),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Step 2 — create the lex-authority account on the scratch PDS so we can
|
|
58
|
+
// capture its DID before any "real" PDS spins up with `lexiconDidAuthority`
|
|
59
|
+
// pointing at it.
|
|
60
|
+
const lexAuthority = await LexiconAuthorityProfile.create(
|
|
61
|
+
scratchPds,
|
|
62
|
+
LEX_AUTHORITY_USER,
|
|
63
|
+
)
|
|
64
|
+
const lexAuthorityDid = lexAuthority.did
|
|
65
|
+
|
|
66
|
+
// Step 3 — primary PDS, configured to resolve lexicons from the authority.
|
|
67
|
+
const primary = await TestPds.create({
|
|
68
|
+
port: PRIMARY_PDS_PORT,
|
|
69
|
+
hostname: 'localhost',
|
|
70
|
+
enableDidDocWithSession: true,
|
|
71
|
+
didPlcUrl: plc.url,
|
|
72
|
+
lexiconDidAuthority: lexAuthorityDid,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Step 4 — migrate the authority account to the primary PDS, then publish
|
|
76
|
+
// the seed lexicon docs (permission sets, etc.) at the new home.
|
|
77
|
+
await lexAuthority.migrateTo(primary)
|
|
78
|
+
await lexAuthority.createRecords()
|
|
79
|
+
|
|
80
|
+
// Step 5 — extra PDSes, each pointing at the same lex authority.
|
|
81
|
+
const extraPdses: TestPds[] = []
|
|
82
|
+
for (let i = 0; i < extraPdsCount; i++) {
|
|
83
|
+
const domain = `.test${i + 2}`
|
|
84
|
+
const extra = await TestPds.create({
|
|
85
|
+
didPlcUrl: plc.url,
|
|
86
|
+
port: await getPort(),
|
|
87
|
+
serviceHandleDomains: [domain],
|
|
88
|
+
lexiconDidAuthority: lexAuthorityDid,
|
|
89
|
+
})
|
|
90
|
+
extraPdses.push(extra)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Step 6 — close the scratch PDS now that the authority lives on primary.
|
|
94
|
+
await scratchPds.close()
|
|
95
|
+
|
|
96
|
+
// Mock network utilities (handle/DID resolution) across the surviving PDSes.
|
|
97
|
+
const allPdsesArray = [primary, ...extraPdses]
|
|
98
|
+
mockNetworkUtilities(allPdsesArray)
|
|
99
|
+
for (const pds of allPdsesArray) {
|
|
100
|
+
mockMailer(pds)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const introspect = await IntrospectServer.start(
|
|
104
|
+
INTROSPECT_PORT,
|
|
105
|
+
plc,
|
|
106
|
+
allPdsesArray,
|
|
107
|
+
undefined,
|
|
108
|
+
undefined,
|
|
109
|
+
undefined,
|
|
110
|
+
{
|
|
111
|
+
did: lexAuthorityDid,
|
|
112
|
+
handle: LEX_AUTHORITY_USER.handle,
|
|
113
|
+
password: LEX_AUTHORITY_USER.password,
|
|
114
|
+
pds: primary.url,
|
|
115
|
+
},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
console.log(`🔍 Introspection server http://localhost:${introspect.port}`)
|
|
119
|
+
console.log(`👤 DID Placeholder server ${plc.url}`)
|
|
120
|
+
|
|
121
|
+
const allPdses: { label: string; pds: TestPds; domain: string }[] = [
|
|
122
|
+
{ label: 'pds1 (primary)', pds: primary, domain: 'test' },
|
|
123
|
+
...extraPdses.map((pds, i) => ({
|
|
124
|
+
label: `pds${i + 2}`,
|
|
125
|
+
pds,
|
|
126
|
+
domain: `test${i + 2}`,
|
|
127
|
+
})),
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
for (const { label, pds } of allPdses) {
|
|
131
|
+
console.log(`🌞 ${label} http://localhost:${pds.port}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(
|
|
135
|
+
`\n📚 Lex authority ${LEX_AUTHORITY_USER.handle} (${lexAuthorityDid})`,
|
|
136
|
+
)
|
|
137
|
+
console.log(
|
|
138
|
+
` hosted on pds1 — log in with password "${LEX_AUTHORITY_USER.password}"`,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
// Seed alice/bob/carol on each PDS so each has data that's reachable cross-PDS.
|
|
142
|
+
const seedNames = ['alice', 'bob', 'carol']
|
|
143
|
+
const seeded: { handle: string; did: string; pds: string }[] = []
|
|
144
|
+
for (const { label, pds, domain } of allPdses) {
|
|
145
|
+
const agent = pds.getAgent()
|
|
146
|
+
for (const name of seedNames) {
|
|
147
|
+
const handle = `${name}.${domain}`
|
|
148
|
+
const res = await agent.com.atproto.server.createAccount({
|
|
149
|
+
handle,
|
|
150
|
+
email: `${name}-${domain}@test.com`,
|
|
151
|
+
password: `${name}-pass`,
|
|
152
|
+
})
|
|
153
|
+
seeded.push({ handle, did: res.data.did, pds: label })
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log('\n👥 Seeded accounts:')
|
|
158
|
+
for (const acct of seeded) {
|
|
159
|
+
console.log(
|
|
160
|
+
` ${acct.pds.padEnd(16)} handle=${acct.handle.padEnd(20)} did=${acct.did}`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
console.log(' (passwords: alice-pass / bob-pass / carol-pass)')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
run()
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import './env.js'
|
|
2
|
+
import { generateMockSetup } from './mock/index.js'
|
|
3
|
+
import { TestNetwork } from './network.js'
|
|
4
|
+
import { mockMailer } from './util.js'
|
|
5
|
+
|
|
6
|
+
const run = async () => {
|
|
7
|
+
console.log(`
|
|
8
|
+
██████╗
|
|
9
|
+
██╔═══██╗
|
|
10
|
+
██║██╗██║
|
|
11
|
+
██║██║██║
|
|
12
|
+
╚█║████╔╝
|
|
13
|
+
╚╝╚═══╝ protocol
|
|
14
|
+
|
|
15
|
+
[ created by Bluesky ]`)
|
|
16
|
+
|
|
17
|
+
const network = await TestNetwork.create({
|
|
18
|
+
pds: {
|
|
19
|
+
port: 2583,
|
|
20
|
+
hostname: 'localhost',
|
|
21
|
+
enableDidDocWithSession: true,
|
|
22
|
+
},
|
|
23
|
+
bsky: {
|
|
24
|
+
dbPostgresSchema: 'bsky',
|
|
25
|
+
port: 2584,
|
|
26
|
+
publicUrl: 'http://localhost:2584',
|
|
27
|
+
},
|
|
28
|
+
plc: { port: 2582 },
|
|
29
|
+
ozone: {
|
|
30
|
+
port: 2587,
|
|
31
|
+
chatUrl: 'http://localhost:2590', // must run separate chat service
|
|
32
|
+
chatDid: 'did:example:chat',
|
|
33
|
+
dbMaterializedViewRefreshIntervalMs: 30_000,
|
|
34
|
+
},
|
|
35
|
+
introspect: { port: 2581 },
|
|
36
|
+
})
|
|
37
|
+
mockMailer(network.pds)
|
|
38
|
+
|
|
39
|
+
if (network.introspect) {
|
|
40
|
+
console.log(
|
|
41
|
+
`🔍 Dev-env introspection server http://localhost:${network.introspect.port}`,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
console.log(`👤 DID Placeholder server http://localhost:${network.plc.port}`)
|
|
45
|
+
console.log(`🌞 Main PDS http://localhost:${network.pds.port}`)
|
|
46
|
+
console.log(`🌞 Main PDS account DB`, network.pds.ctx.cfg.db.accountDbLoc)
|
|
47
|
+
console.log(
|
|
48
|
+
`🔨 Lexicon authority DID ${network.pds.ctx.cfg.lexicon.didAuthority}`,
|
|
49
|
+
)
|
|
50
|
+
console.log(`🗼 Ozone server http://localhost:${network.ozone.port}`)
|
|
51
|
+
console.log(`🗼 Ozone service DID ${network.ozone.ctx.cfg.service.did}`)
|
|
52
|
+
console.log(`🔄 Bsync server http://localhost:${network.bsync.port}`)
|
|
53
|
+
console.log(`🌅 Bsky Appview http://localhost:${network.bsky.port}`)
|
|
54
|
+
console.log(`🌅 Bsky Appview DID ${network.bsky.serverDid}`)
|
|
55
|
+
for (const fg of network.feedGens) {
|
|
56
|
+
console.log(`🤖 Feed Generator (${fg.did}) http://localhost:${fg.port}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.time('✅ Dev environment is ready')
|
|
60
|
+
await generateMockSetup(network)
|
|
61
|
+
console.timeEnd('✅ Dev environment is ready')
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
run()
|
package/src/bsky.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Client as PlcClient } from '@did-plc/lib'
|
|
2
|
+
import getPort from 'get-port'
|
|
3
|
+
import * as ui8 from 'uint8arrays'
|
|
4
|
+
import { AtpAgent } from '@atproto/api'
|
|
5
|
+
import * as bsky from '@atproto/bsky'
|
|
6
|
+
import { Secp256k1Keypair } from '@atproto/crypto'
|
|
7
|
+
import { Client } from '@atproto/lex'
|
|
8
|
+
import type { DidString } from '@atproto/syntax'
|
|
9
|
+
import { ADMIN_PASSWORD, EXAMPLE_LABELER } from './const.js'
|
|
10
|
+
import type { BskyConfig } from './types.js'
|
|
11
|
+
export * from '@atproto/bsky'
|
|
12
|
+
|
|
13
|
+
export class TestBsky {
|
|
14
|
+
constructor(
|
|
15
|
+
public url: string,
|
|
16
|
+
public port: number,
|
|
17
|
+
public db: bsky.Database,
|
|
18
|
+
public server: bsky.BskyAppView,
|
|
19
|
+
public dataplane: bsky.DataPlaneServer,
|
|
20
|
+
public bsyncSub: bsky.BsyncSubscription,
|
|
21
|
+
public sub: bsky.RepoSubscription,
|
|
22
|
+
public serverDid: string,
|
|
23
|
+
) {}
|
|
24
|
+
|
|
25
|
+
static async create(cfg: BskyConfig): Promise<TestBsky> {
|
|
26
|
+
const serviceKeypair = cfg.privateKey
|
|
27
|
+
? await Secp256k1Keypair.import(cfg.privateKey)
|
|
28
|
+
: await Secp256k1Keypair.create()
|
|
29
|
+
const plcClient = new PlcClient(cfg.plcUrl)
|
|
30
|
+
|
|
31
|
+
const port = cfg.port || (await getPort())
|
|
32
|
+
const url = `http://localhost:${port}`
|
|
33
|
+
const serverDid = (await plcClient.createDid({
|
|
34
|
+
signingKey: serviceKeypair.did(),
|
|
35
|
+
rotationKeys: [serviceKeypair.did()],
|
|
36
|
+
handle: 'bsky.test',
|
|
37
|
+
pds: `http://localhost:${port}`,
|
|
38
|
+
signer: serviceKeypair,
|
|
39
|
+
})) as DidString
|
|
40
|
+
|
|
41
|
+
const endpoint = `http://localhost:${port}`
|
|
42
|
+
|
|
43
|
+
await plcClient.updateData(serverDid, serviceKeypair, (x) => {
|
|
44
|
+
x.services['bsky_notif'] = {
|
|
45
|
+
type: 'BskyNotificationService',
|
|
46
|
+
endpoint,
|
|
47
|
+
}
|
|
48
|
+
x.services['bsky_appview'] = {
|
|
49
|
+
type: 'BskyAppView',
|
|
50
|
+
endpoint,
|
|
51
|
+
}
|
|
52
|
+
return x
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// shared across server, ingester, and indexer in order to share pool, avoid too many pg connections.
|
|
56
|
+
const db = new bsky.Database({
|
|
57
|
+
url: cfg.dbPostgresUrl,
|
|
58
|
+
schema: cfg.dbPostgresSchema,
|
|
59
|
+
poolSize: 10,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const dataplanePort = await getPort()
|
|
63
|
+
const dataplane = await bsky.DataPlaneServer.create(
|
|
64
|
+
db,
|
|
65
|
+
dataplanePort,
|
|
66
|
+
cfg.plcUrl,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const config = new bsky.ServerConfig({
|
|
70
|
+
version: 'unknown',
|
|
71
|
+
port,
|
|
72
|
+
didPlcUrl: cfg.plcUrl,
|
|
73
|
+
publicUrl: 'https://bsky.public.url',
|
|
74
|
+
serverDid,
|
|
75
|
+
alternateAudienceDids: [],
|
|
76
|
+
dataplaneUrls: [`http://localhost:${dataplanePort}`],
|
|
77
|
+
dataplaneHttpVersion: '1.1',
|
|
78
|
+
bsyncHttpVersion: '1.1',
|
|
79
|
+
bsyncApiKey: 'bsync-api-key',
|
|
80
|
+
modServiceDid: cfg.modServiceDid ?? 'did:example:invalidMod',
|
|
81
|
+
labelsFromIssuerDids: [EXAMPLE_LABELER],
|
|
82
|
+
bigThreadUris: new Set(),
|
|
83
|
+
maxThreadParents: cfg.maxThreadParents ?? 50,
|
|
84
|
+
disableSsrfProtection: true,
|
|
85
|
+
searchTagsHide: new Set(),
|
|
86
|
+
searchTagsHideAll: new Set(),
|
|
87
|
+
threadTagsBumpDown: new Set(),
|
|
88
|
+
threadTagsHide: new Set(),
|
|
89
|
+
visibilityTagHide: '',
|
|
90
|
+
visibilityTagRankPrefix: '',
|
|
91
|
+
debugFieldAllowedDids: new Set(),
|
|
92
|
+
draftsLimit: 500,
|
|
93
|
+
...cfg,
|
|
94
|
+
adminPasswords: [ADMIN_PASSWORD],
|
|
95
|
+
etcdHosts: [],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Separate migration db in case migration changes some connection state that we need in the tests, e.g. "alter database ... set ..."
|
|
99
|
+
const migrationDb = new bsky.Database({
|
|
100
|
+
url: cfg.dbPostgresUrl,
|
|
101
|
+
schema: cfg.dbPostgresSchema,
|
|
102
|
+
})
|
|
103
|
+
if (cfg.migration) {
|
|
104
|
+
await migrationDb.migrateToOrThrow(cfg.migration)
|
|
105
|
+
} else {
|
|
106
|
+
await migrationDb.migrateToLatestOrThrow()
|
|
107
|
+
}
|
|
108
|
+
await migrationDb.close()
|
|
109
|
+
|
|
110
|
+
// api server
|
|
111
|
+
const server = bsky.BskyAppView.create({
|
|
112
|
+
config,
|
|
113
|
+
signingKey: serviceKeypair,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const bsyncSub = new bsky.BsyncSubscription({
|
|
117
|
+
config,
|
|
118
|
+
db,
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const sub = new bsky.RepoSubscription({
|
|
122
|
+
service: cfg.repoProvider,
|
|
123
|
+
db,
|
|
124
|
+
idResolver: dataplane.idResolver,
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
await server.start()
|
|
128
|
+
|
|
129
|
+
bsyncSub.start()
|
|
130
|
+
void sub.start()
|
|
131
|
+
|
|
132
|
+
return new TestBsky(
|
|
133
|
+
url,
|
|
134
|
+
port,
|
|
135
|
+
db,
|
|
136
|
+
server,
|
|
137
|
+
dataplane,
|
|
138
|
+
bsyncSub,
|
|
139
|
+
sub,
|
|
140
|
+
serverDid,
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
get ctx(): bsky.AppContext {
|
|
145
|
+
return this.server.ctx
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getAgent(): AtpAgent {
|
|
149
|
+
const agent = new AtpAgent({ service: this.url })
|
|
150
|
+
agent.configureLabelers([EXAMPLE_LABELER])
|
|
151
|
+
return agent
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
getClient(): Client {
|
|
155
|
+
const client = new Client({ service: this.url })
|
|
156
|
+
client.setLabelers([EXAMPLE_LABELER])
|
|
157
|
+
return client
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
adminAuth(): string {
|
|
161
|
+
const [password] = this.ctx.cfg.adminPasswords
|
|
162
|
+
return (
|
|
163
|
+
'Basic ' +
|
|
164
|
+
ui8.toString(ui8.fromString(`admin:${password}`, 'utf8'), 'base64pad')
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
adminAuthHeaders() {
|
|
169
|
+
return {
|
|
170
|
+
authorization: this.adminAuth(),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async close() {
|
|
175
|
+
// @TODO Use disposable stack when it becomes available (Node24+)
|
|
176
|
+
try {
|
|
177
|
+
await this.server.destroy()
|
|
178
|
+
} finally {
|
|
179
|
+
try {
|
|
180
|
+
await this.bsyncSub.destroy()
|
|
181
|
+
} finally {
|
|
182
|
+
try {
|
|
183
|
+
await this.dataplane.destroy()
|
|
184
|
+
} finally {
|
|
185
|
+
try {
|
|
186
|
+
await this.sub.destroy()
|
|
187
|
+
} finally {
|
|
188
|
+
await this.db.close()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async [Symbol.asyncDispose]() {
|
|
196
|
+
await this.close()
|
|
197
|
+
}
|
|
198
|
+
}
|
package/src/bsync.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import getPort from 'get-port'
|
|
2
|
+
import * as bsync from '@atproto/bsync'
|
|
3
|
+
import type { BsyncConfig } from './types.js'
|
|
4
|
+
|
|
5
|
+
export class TestBsync {
|
|
6
|
+
constructor(
|
|
7
|
+
public url: string,
|
|
8
|
+
public port: number,
|
|
9
|
+
public service: bsync.BsyncService,
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
static async create(cfg: BsyncConfig): Promise<TestBsync> {
|
|
13
|
+
const port = cfg.port || (await getPort())
|
|
14
|
+
const url = `http://localhost:${port}`
|
|
15
|
+
|
|
16
|
+
const config = bsync.envToCfg({
|
|
17
|
+
port,
|
|
18
|
+
apiKeys: cfg.apiKeys ?? ['api-key'],
|
|
19
|
+
dbSchema: 'bsync',
|
|
20
|
+
// Keep the pool small: a bsync runs alongside every test network, so a
|
|
21
|
+
// default-sized pool per network quickly exhausts postgres connections.
|
|
22
|
+
dbPoolSize: cfg.dbPoolSize ?? 5,
|
|
23
|
+
...cfg,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const service = await bsync.BsyncService.create(config)
|
|
27
|
+
await service.ctx.db.migrateToLatestOrThrow()
|
|
28
|
+
await service.start()
|
|
29
|
+
|
|
30
|
+
return new TestBsync(url, port, service)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get ctx(): bsync.AppContext {
|
|
34
|
+
return this.service.ctx
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Current head (max id) of each operation stream. Used to wait for the bsky
|
|
39
|
+
* bsync subscription to catch up in tests, without waiting for the long-poll
|
|
40
|
+
* timeout.
|
|
41
|
+
*/
|
|
42
|
+
async getStreamHeads(): Promise<{
|
|
43
|
+
op?: string
|
|
44
|
+
mute?: string
|
|
45
|
+
notif?: string
|
|
46
|
+
}> {
|
|
47
|
+
const db = this.ctx.db.db
|
|
48
|
+
const max = async (table: 'operation' | 'mute_op' | 'notif_op') => {
|
|
49
|
+
const row = await db
|
|
50
|
+
.selectFrom(table)
|
|
51
|
+
.select((eb) => eb.fn.max('id').as('id'))
|
|
52
|
+
.executeTakeFirst()
|
|
53
|
+
return row?.id != null ? String(row.id) : undefined
|
|
54
|
+
}
|
|
55
|
+
const [op, mute, notif] = await Promise.all([
|
|
56
|
+
max('operation'),
|
|
57
|
+
max('mute_op'),
|
|
58
|
+
max('notif_op'),
|
|
59
|
+
])
|
|
60
|
+
return { op, mute, notif }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async close() {
|
|
64
|
+
await this.service.destroy()
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/const.ts
ADDED
package/src/env.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// NOTE: this file should be imported first, particularly before `@atproto/common` (for logging), to ensure that environment variables are respected in library code
|
|
2
|
+
import { config } from 'dotenv'
|
|
3
|
+
|
|
4
|
+
const env = process.env.ENV
|
|
5
|
+
if (env) {
|
|
6
|
+
config({ path: `./.${env}.env` })
|
|
7
|
+
} else {
|
|
8
|
+
config()
|
|
9
|
+
}
|