playcademy 0.16.10 → 0.16.11
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/dist/cli.js +1 -1
- package/dist/edge-play/src/entry/middleware.ts +2 -0
- package/dist/edge-play/src/lib/index.ts +1 -0
- package/dist/edge-play/src/lib/self-dispatch.test.ts +190 -0
- package/dist/edge-play/src/lib/self-dispatch.ts +41 -0
- package/dist/edge-play/src/types.ts +24 -0
- package/dist/index.js +14 -12
- package/dist/templates/playcademy-env.d.ts.template +2 -1
- package/dist/utils.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -8,6 +8,7 @@ import { cors } from 'hono/cors'
|
|
|
8
8
|
|
|
9
9
|
import { PlaycademyClient, verifyGameToken } from '@playcademy/sdk/server'
|
|
10
10
|
|
|
11
|
+
import { resolveRawSelfWorker, wrapSelfWorkerWithPathResolution } from '../lib/self-dispatch'
|
|
11
12
|
import { populateProcessEnv, reconstructSecrets } from './setup'
|
|
12
13
|
|
|
13
14
|
import type { Context, Hono } from 'hono'
|
|
@@ -47,6 +48,7 @@ export function registerEnvSetup(app: Hono<HonoEnv>, config: RuntimeConfig): voi
|
|
|
47
48
|
app.use('*', async (c, next) => {
|
|
48
49
|
populateProcessEnv(c.env)
|
|
49
50
|
c.env.secrets = reconstructSecrets(c.env)
|
|
51
|
+
c.env.SELF = wrapSelfWorkerWithPathResolution(resolveRawSelfWorker(c.env), c.req.url)
|
|
50
52
|
c.set('config', config)
|
|
51
53
|
c.set('routeMetadata', config.__routeMetadata || [])
|
|
52
54
|
await next()
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, mock } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
normalizeSelfFetchInput,
|
|
5
|
+
resolveRawSelfWorker,
|
|
6
|
+
wrapSelfWorkerWithPathResolution,
|
|
7
|
+
} from './self-dispatch'
|
|
8
|
+
|
|
9
|
+
import type { HonoEnv, SelfWorker } from '../types'
|
|
10
|
+
|
|
11
|
+
describe('normalizeSelfFetchInput', () => {
|
|
12
|
+
const requestUrl = 'https://vocabulon-staging.playcademy.gg/api/admin/genai/sprint'
|
|
13
|
+
|
|
14
|
+
it('resolves root-relative paths to the current origin', () => {
|
|
15
|
+
const input = normalizeSelfFetchInput('/api/internal/process-batch', requestUrl)
|
|
16
|
+
expect(input).toBeInstanceOf(URL)
|
|
17
|
+
expect((input as URL).toString()).toBe(
|
|
18
|
+
'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
|
|
19
|
+
)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('treats bare paths as root-relative', () => {
|
|
23
|
+
const input = normalizeSelfFetchInput('api/internal/process-batch', requestUrl)
|
|
24
|
+
expect(input).toBeInstanceOf(URL)
|
|
25
|
+
expect((input as URL).toString()).toBe(
|
|
26
|
+
'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('preserves absolute URL strings', () => {
|
|
31
|
+
const input = normalizeSelfFetchInput(
|
|
32
|
+
'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
|
|
33
|
+
requestUrl,
|
|
34
|
+
)
|
|
35
|
+
expect(input).toBeInstanceOf(URL)
|
|
36
|
+
expect((input as URL).toString()).toBe(
|
|
37
|
+
'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('preserves query and hash on normalized paths', () => {
|
|
42
|
+
const input = normalizeSelfFetchInput('/api/internal/process-batch?attempt=2#step', requestUrl)
|
|
43
|
+
expect(input).toBeInstanceOf(URL)
|
|
44
|
+
expect((input as URL).toString()).toBe(
|
|
45
|
+
'https://vocabulon-staging.playcademy.gg/api/internal/process-batch?attempt=2#step',
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('passes through URL objects unchanged', () => {
|
|
50
|
+
const url = new URL('https://vocabulon-staging.playcademy.gg/api/internal/process-batch')
|
|
51
|
+
const input = normalizeSelfFetchInput(url, requestUrl)
|
|
52
|
+
expect(input).toBe(url)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('supports non-http absolute schemes', () => {
|
|
56
|
+
const input = normalizeSelfFetchInput('mailto:test@example.com', requestUrl)
|
|
57
|
+
expect(input).toBeInstanceOf(URL)
|
|
58
|
+
expect((input as URL).toString()).toBe('mailto:test@example.com')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('does not allow protocol-relative input to escape origin', () => {
|
|
62
|
+
const input = normalizeSelfFetchInput('//evil.example.com/path', requestUrl)
|
|
63
|
+
expect(input).toBeInstanceOf(URL)
|
|
64
|
+
expect((input as URL).toString()).toBe('https://vocabulon-staging.playcademy.gg/evil.example.com/path')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('passes through Request objects unchanged', () => {
|
|
68
|
+
const request = new Request('https://vocabulon-staging.playcademy.gg/api/internal/process-batch')
|
|
69
|
+
const input = normalizeSelfFetchInput(request, requestUrl)
|
|
70
|
+
expect(input).toBe(request)
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('wrapSelfWorkerWithPathResolution', () => {
|
|
75
|
+
const requestUrl = 'https://vocabulon-staging.playcademy.gg/api/admin/genai/sprint'
|
|
76
|
+
|
|
77
|
+
it('normalizes string path inputs before delegating to raw self worker', async () => {
|
|
78
|
+
const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []
|
|
79
|
+
const raw: SelfWorker = {
|
|
80
|
+
fetch: (input, init) => {
|
|
81
|
+
calls.push({ input, init })
|
|
82
|
+
return Promise.resolve(new Response(null, { status: 202 }))
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const self = wrapSelfWorkerWithPathResolution(raw, requestUrl)
|
|
87
|
+
const init = { method: 'POST' }
|
|
88
|
+
const response = await self.fetch('api/internal/process-batch', init)
|
|
89
|
+
|
|
90
|
+
expect(response.status).toBe(202)
|
|
91
|
+
expect(calls).toHaveLength(1)
|
|
92
|
+
expect(calls[0]?.input).toBeInstanceOf(URL)
|
|
93
|
+
expect((calls[0]?.input as URL).toString()).toBe(
|
|
94
|
+
'https://vocabulon-staging.playcademy.gg/api/internal/process-batch',
|
|
95
|
+
)
|
|
96
|
+
expect(calls[0]?.init).toEqual(init)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('passes Request objects through to the raw self worker', async () => {
|
|
100
|
+
const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []
|
|
101
|
+
const raw: SelfWorker = {
|
|
102
|
+
fetch: (input, init) => {
|
|
103
|
+
calls.push({ input, init })
|
|
104
|
+
return Promise.resolve(new Response(null, { status: 204 }))
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
const self = wrapSelfWorkerWithPathResolution(raw, requestUrl)
|
|
108
|
+
const req = new Request('https://vocabulon-staging.playcademy.gg/api/internal/process-batch')
|
|
109
|
+
|
|
110
|
+
await self.fetch(req)
|
|
111
|
+
|
|
112
|
+
expect(calls).toHaveLength(1)
|
|
113
|
+
expect(calls[0]?.input).toBe(req)
|
|
114
|
+
expect(calls[0]?.init).toBeUndefined()
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('resolveRawSelfWorker', () => {
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
mock.restore()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('uses dispatch namespace binding when available', async () => {
|
|
124
|
+
const targetWorker: SelfWorker = {
|
|
125
|
+
fetch: () => Promise.resolve(new Response('ok')),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const env = {
|
|
129
|
+
__PLAYCADEMY_DISPATCH: {
|
|
130
|
+
get: (name: string) => {
|
|
131
|
+
expect(name).toBe('staging-vocabulon')
|
|
132
|
+
return targetWorker
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
__PLAYCADEMY_WORKER_NAME: 'staging-vocabulon',
|
|
136
|
+
} as unknown as HonoEnv['Bindings']
|
|
137
|
+
|
|
138
|
+
const resolved = resolveRawSelfWorker(env)
|
|
139
|
+
const response = await resolved.fetch('https://vocabulon-staging.playcademy.gg/api/internal/process')
|
|
140
|
+
expect(response.status).toBe(200)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('falls back to global fetch when dispatch binding is unavailable', async () => {
|
|
144
|
+
const fetchMock = mock(() => Promise.resolve(new Response('fallback', { status: 204 })))
|
|
145
|
+
const originalFetch = globalThis.fetch
|
|
146
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const env = {} as HonoEnv['Bindings']
|
|
150
|
+
const resolved = resolveRawSelfWorker(env)
|
|
151
|
+
const response = await resolved.fetch(
|
|
152
|
+
'https://vocabulon-staging.playcademy.gg/api/internal/process',
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
expect(response.status).toBe(204)
|
|
156
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
157
|
+
const firstCall = fetchMock.mock.calls[0] as unknown[] | undefined
|
|
158
|
+
expect(firstCall).toBeDefined()
|
|
159
|
+
const arg = firstCall?.[0]
|
|
160
|
+
expect(arg).toBeInstanceOf(Request)
|
|
161
|
+
} finally {
|
|
162
|
+
globalThis.fetch = originalFetch
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('falls back to global fetch when worker name is missing', async () => {
|
|
167
|
+
const fetchMock = mock(() => Promise.resolve(new Response('fallback', { status: 206 })))
|
|
168
|
+
const originalFetch = globalThis.fetch
|
|
169
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const env = {
|
|
173
|
+
__PLAYCADEMY_DISPATCH: {
|
|
174
|
+
get: () => {
|
|
175
|
+
throw new Error('should not be called when worker name is missing')
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
} as unknown as HonoEnv['Bindings']
|
|
179
|
+
const resolved = resolveRawSelfWorker(env)
|
|
180
|
+
const response = await resolved.fetch(
|
|
181
|
+
'https://vocabulon-staging.playcademy.gg/api/internal/process',
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
expect(response.status).toBe(206)
|
|
185
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
186
|
+
} finally {
|
|
187
|
+
globalThis.fetch = originalFetch
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { HonoEnv, SelfWorker } from '../types'
|
|
2
|
+
|
|
3
|
+
function isAbsoluteUrl(value: string): boolean {
|
|
4
|
+
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(value)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function normalizeSelfFetchInput(
|
|
8
|
+
input: RequestInfo | URL,
|
|
9
|
+
requestUrl: string,
|
|
10
|
+
): RequestInfo | URL {
|
|
11
|
+
if (typeof input !== 'string') {
|
|
12
|
+
return input
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (isAbsoluteUrl(input)) {
|
|
16
|
+
return new URL(input)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const origin = new URL(requestUrl).origin
|
|
20
|
+
const sanitized = input.startsWith('//') ? `/${input.replace(/^\/+/, '')}` : input
|
|
21
|
+
const pathname = sanitized.startsWith('/') ? sanitized : `/${sanitized}`
|
|
22
|
+
|
|
23
|
+
return new URL(pathname, origin)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function wrapSelfWorkerWithPathResolution(raw: SelfWorker, requestUrl: string): SelfWorker {
|
|
27
|
+
return {
|
|
28
|
+
fetch: (input, init) => raw.fetch(normalizeSelfFetchInput(input, requestUrl), init),
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolveRawSelfWorker(env: HonoEnv['Bindings']): SelfWorker {
|
|
33
|
+
const dispatchBinding = env.__PLAYCADEMY_DISPATCH
|
|
34
|
+
const workerName = env.__PLAYCADEMY_WORKER_NAME
|
|
35
|
+
|
|
36
|
+
if (dispatchBinding && workerName) {
|
|
37
|
+
return dispatchBinding.get(workerName)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return { fetch: (input, init) => fetch(new Request(input, init)) }
|
|
41
|
+
}
|
|
@@ -17,6 +17,21 @@ import type { RouteMetadata } from './entry/types'
|
|
|
17
17
|
*/
|
|
18
18
|
export type AssetsFetcher = { fetch(request: Request): Promise<Response> }
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Internal self-dispatch worker fetcher.
|
|
22
|
+
* Exposed to games as env.SELF for background request chaining.
|
|
23
|
+
*/
|
|
24
|
+
export type SelfWorker = {
|
|
25
|
+
fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Dispatch namespace binding used to resolve the current worker.
|
|
30
|
+
*/
|
|
31
|
+
export type DispatchNamespaceBinding = {
|
|
32
|
+
get(name: string): SelfWorker
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
/**
|
|
21
36
|
* Enabled integrations from playcademy.config.js
|
|
22
37
|
*/
|
|
@@ -60,6 +75,15 @@ export interface ServerEnv {
|
|
|
60
75
|
/** R2 bucket binding (optional, Cloudflare-specific) */
|
|
61
76
|
BUCKET?: R2Bucket
|
|
62
77
|
|
|
78
|
+
/** Self-dispatch API for internal loopback requests */
|
|
79
|
+
SELF?: SelfWorker
|
|
80
|
+
|
|
81
|
+
/** Internal dispatch namespace binding (hydrated to SELF by middleware) */
|
|
82
|
+
__PLAYCADEMY_DISPATCH?: DispatchNamespaceBinding
|
|
83
|
+
|
|
84
|
+
/** Internal worker name for self-resolution in dispatch namespace */
|
|
85
|
+
__PLAYCADEMY_WORKER_NAME?: string
|
|
86
|
+
|
|
63
87
|
/** Allow dynamic secret bindings (secrets_KEY_NAME) */
|
|
64
88
|
[key: string]: unknown
|
|
65
89
|
}
|
package/dist/index.js
CHANGED
|
@@ -1357,7 +1357,7 @@ function ensureEnvironment(env) {
|
|
|
1357
1357
|
}
|
|
1358
1358
|
return getEnvironment();
|
|
1359
1359
|
}
|
|
1360
|
-
function requireEnvironment(env,
|
|
1360
|
+
function requireEnvironment(env, usage) {
|
|
1361
1361
|
if (env) {
|
|
1362
1362
|
return ensureEnvironment(env);
|
|
1363
1363
|
}
|
|
@@ -1365,12 +1365,12 @@ function requireEnvironment(env, hostname) {
|
|
|
1365
1365
|
if (envVar) {
|
|
1366
1366
|
return ensureEnvironment(envVar);
|
|
1367
1367
|
}
|
|
1368
|
-
const
|
|
1368
|
+
const command = usage || "<command> [args]";
|
|
1369
1369
|
logger.error("Missing required --env flag");
|
|
1370
1370
|
logger.newLine();
|
|
1371
1371
|
logger.admonition("tip", "Usage", [
|
|
1372
|
-
`\`playcademy
|
|
1373
|
-
`\`playcademy
|
|
1372
|
+
`\`playcademy ${command} --env production\``,
|
|
1373
|
+
`\`playcademy ${command} --env staging\``,
|
|
1374
1374
|
"",
|
|
1375
1375
|
"Or set `PLAYCADEMY_ENV` in your environment."
|
|
1376
1376
|
]);
|
|
@@ -4016,7 +4016,7 @@ import { join as join12 } from "path";
|
|
|
4016
4016
|
// package.json with { type: 'json' }
|
|
4017
4017
|
var package_default2 = {
|
|
4018
4018
|
name: "playcademy",
|
|
4019
|
-
version: "0.16.
|
|
4019
|
+
version: "0.16.10",
|
|
4020
4020
|
type: "module",
|
|
4021
4021
|
exports: {
|
|
4022
4022
|
".": {
|
|
@@ -11220,7 +11220,7 @@ var getStatusCommand = new Command13("status").description("Check your developer
|
|
|
11220
11220
|
});
|
|
11221
11221
|
|
|
11222
11222
|
// package.json
|
|
11223
|
-
var version2 = "0.16.
|
|
11223
|
+
var version2 = "0.16.10";
|
|
11224
11224
|
|
|
11225
11225
|
// src/commands/dev/server.ts
|
|
11226
11226
|
function setupCleanupHandlers(workspace, getServer) {
|
|
@@ -14627,7 +14627,7 @@ async function runDomainAdd(hostname, options) {
|
|
|
14627
14627
|
process.exit(1);
|
|
14628
14628
|
}
|
|
14629
14629
|
const normalizedHostname = hostname.trim().toLowerCase();
|
|
14630
|
-
requireEnvironment(options.env, normalizedHostname);
|
|
14630
|
+
const environment = requireEnvironment(options.env, `domain add ${normalizedHostname}`);
|
|
14631
14631
|
await requireConfigFile();
|
|
14632
14632
|
const config = await loadConfig();
|
|
14633
14633
|
const slug = getSlugFromConfig(config);
|
|
@@ -14643,7 +14643,7 @@ async function runDomainAdd(hostname, options) {
|
|
|
14643
14643
|
displayDomainValidationRecords(domain, normalizedHostname);
|
|
14644
14644
|
logger.admonition("tip", "Next Steps", [
|
|
14645
14645
|
"1. Add the DNS records shown above to your domain registrar",
|
|
14646
|
-
`2. Run \`playcademy domain verify ${normalizedHostname} --env ${
|
|
14646
|
+
`2. Run \`playcademy domain verify ${normalizedHostname} --env ${environment}\` to check validation status`,
|
|
14647
14647
|
"",
|
|
14648
14648
|
"Note: DNS propagation can take up to 24 hours. SSL certificates",
|
|
14649
14649
|
"are provisioned automatically once DNS records are verified.",
|
|
@@ -14665,7 +14665,7 @@ import { confirm as confirm16 } from "@inquirer/prompts";
|
|
|
14665
14665
|
async function runDomainDelete(hostname, options) {
|
|
14666
14666
|
try {
|
|
14667
14667
|
logger.newLine();
|
|
14668
|
-
requireEnvironment(options.env, hostname);
|
|
14668
|
+
requireEnvironment(options.env, `domain delete ${hostname}`);
|
|
14669
14669
|
await requireConfigFile();
|
|
14670
14670
|
const config = await loadConfig();
|
|
14671
14671
|
const slug = getSlugFromConfig(config);
|
|
@@ -14707,7 +14707,7 @@ async function runDomainList(options) {
|
|
|
14707
14707
|
if (!options.raw && !options.json) {
|
|
14708
14708
|
logger.newLine();
|
|
14709
14709
|
}
|
|
14710
|
-
requireEnvironment(options.env);
|
|
14710
|
+
requireEnvironment(options.env, "domain list");
|
|
14711
14711
|
await requireConfigFile();
|
|
14712
14712
|
const config = await loadConfig();
|
|
14713
14713
|
const slug = getSlugFromConfig(config);
|
|
@@ -14757,7 +14757,7 @@ async function runDomainVerify(hostname, options) {
|
|
|
14757
14757
|
if (!options.json) {
|
|
14758
14758
|
logger.newLine();
|
|
14759
14759
|
}
|
|
14760
|
-
requireEnvironment(options.env, hostname);
|
|
14760
|
+
const environment = requireEnvironment(options.env, `domain verify ${hostname}`);
|
|
14761
14761
|
await requireConfigFile();
|
|
14762
14762
|
const config = await loadConfig();
|
|
14763
14763
|
const slug = getSlugFromConfig(config);
|
|
@@ -14783,7 +14783,9 @@ async function runDomainVerify(hostname, options) {
|
|
|
14783
14783
|
logger.newLine();
|
|
14784
14784
|
} else if (domain.status === "pending" || domain.status === "pending_validation" || domain.sslStatus === "pending_validation") {
|
|
14785
14785
|
logger.admonition("tip", "Still Pending", [
|
|
14786
|
-
"DNS records can take up to 24 hours to propagate."
|
|
14786
|
+
"DNS records can take up to 24 hours to propagate.",
|
|
14787
|
+
"",
|
|
14788
|
+
`Run \`playcademy domain verify ${hostname} --env ${environment}\` again to check status.`
|
|
14787
14789
|
]);
|
|
14788
14790
|
logger.newLine();
|
|
14789
14791
|
}
|
|
@@ -15,7 +15,8 @@ declare global {
|
|
|
15
15
|
interface PlaycademyEnv {
|
|
16
16
|
PLAYCADEMY_API_KEY: string
|
|
17
17
|
GAME_ID: string
|
|
18
|
-
PLAYCADEMY_BASE_URL: string
|
|
18
|
+
PLAYCADEMY_BASE_URL: string
|
|
19
|
+
SELF: { fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> }{{BINDINGS}}{{SECRETS}}
|
|
19
20
|
}{{VARIABLES}}
|
|
20
21
|
|
|
21
22
|
type Context = HonoContext<{ Bindings: PlaycademyEnv{{CONTEXT_VARS}} }>
|
package/dist/utils.js
CHANGED
package/dist/version.js
CHANGED