uniweb 0.12.26 → 0.12.28
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/package.json +6 -6
- package/partials/agents.md +3 -0
- package/src/backend/client.js +339 -0
- package/src/commands/clone.js +18 -32
- package/src/commands/deploy.js +218 -1783
- package/src/commands/handoff.js +9 -246
- package/src/commands/invite.js +10 -318
- package/src/commands/org.js +6 -8
- package/src/commands/publish.js +128 -1153
- package/src/commands/pull.js +22 -36
- package/src/commands/push.js +43 -101
- package/src/commands/register.js +184 -39
- package/src/commands/runtime.js +141 -0
- package/src/commands/template.js +13 -221
- package/src/framework-index.json +18 -7
- package/src/index.js +74 -100
- package/src/utils/asset-upload.js +162 -0
- package/src/utils/code-upload.js +245 -0
- package/src/utils/config.js +11 -44
- package/src/utils/registry-auth.js +35 -1
- package/src/utils/registry-orgs.js +141 -73
- package/src/utils/runtime-upload.js +163 -0
- package/src/commands/login.js +0 -230
- package/src/utils/auth.js +0 -212
- package/src/utils/registry.js +0 -466
package/src/utils/registry.js
DELETED
|
@@ -1,466 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Local Foundation Registry
|
|
3
|
-
*
|
|
4
|
-
* Manages published foundations in .unicloud/registry/. The on-disk
|
|
5
|
-
* shape mirrors uniweb-edge's registry index (versions as an array of
|
|
6
|
-
* { version, ... } objects, plus top-level namespace and latest) so
|
|
7
|
-
* `--local` exercises the same data shape that ships in production.
|
|
8
|
-
*
|
|
9
|
-
* Layout:
|
|
10
|
-
* .unicloud/
|
|
11
|
-
* registry/
|
|
12
|
-
* index.json # see "Index format" below
|
|
13
|
-
* packages/
|
|
14
|
-
* name/
|
|
15
|
-
* 1.0.0/
|
|
16
|
-
* foundation.js
|
|
17
|
-
* schema.json
|
|
18
|
-
* assets/...
|
|
19
|
-
*
|
|
20
|
-
* Index format:
|
|
21
|
-
* {
|
|
22
|
-
* "@ns/name": {
|
|
23
|
-
* namespace: "ns",
|
|
24
|
-
* versions: [
|
|
25
|
-
* { version: "1.0.0", publishedAt, publishedBy, ... },
|
|
26
|
-
* ...
|
|
27
|
-
* ],
|
|
28
|
-
* latest: "1.0.0"
|
|
29
|
-
* }
|
|
30
|
-
* }
|
|
31
|
-
*
|
|
32
|
-
* Legacy entries (versions as an object keyed by version) are migrated
|
|
33
|
-
* to this shape on read. The next write persists the new shape.
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import { existsSync } from 'node:fs'
|
|
37
|
-
import { readFile, writeFile, readdir, mkdir, cp } from 'node:fs/promises'
|
|
38
|
-
import { join, dirname, relative } from 'node:path'
|
|
39
|
-
|
|
40
|
-
import { findWorkspaceRoot } from './workspace.js'
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Get the .unicloud/registry/ directory path.
|
|
44
|
-
* Looks for workspace root first; falls back to cwd.
|
|
45
|
-
* @param {string} [startDir]
|
|
46
|
-
* @returns {string}
|
|
47
|
-
*/
|
|
48
|
-
export function getRegistryDir(startDir = process.cwd()) {
|
|
49
|
-
const root = findWorkspaceRoot(startDir)
|
|
50
|
-
const base = root || startDir
|
|
51
|
-
return join(base, '.unicloud', 'registry')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Sanitize a package name for filesystem use.
|
|
56
|
-
* '@org/pkg' → 'org/pkg'
|
|
57
|
-
* @param {string} name
|
|
58
|
-
* @returns {string}
|
|
59
|
-
*/
|
|
60
|
-
function sanitizeName(name) {
|
|
61
|
-
// Strip leading @ for directory structure: @org/name → org/name
|
|
62
|
-
return name.startsWith('@') ? name.slice(1) : name
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Parse the namespace out of a scoped package name. '@org/pkg' → 'org';
|
|
67
|
-
* unscoped → ''.
|
|
68
|
-
* @param {string} name
|
|
69
|
-
* @returns {string}
|
|
70
|
-
*/
|
|
71
|
-
function parseNamespace(name) {
|
|
72
|
-
const m = /^@([a-z0-9_-]+)\//.exec(name)
|
|
73
|
-
return m ? m[1] : ''
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Migrate a legacy index entry (versions as object, no namespace/latest)
|
|
78
|
-
* to the current shape (versions as array, namespace + latest at top).
|
|
79
|
-
* Mutates and returns the entry.
|
|
80
|
-
*/
|
|
81
|
-
function normalizeEntry(name, entry) {
|
|
82
|
-
if (!entry) return entry
|
|
83
|
-
if (entry.versions && !Array.isArray(entry.versions) && typeof entry.versions === 'object') {
|
|
84
|
-
entry.versions = Object.entries(entry.versions).map(([version, data]) => ({
|
|
85
|
-
version,
|
|
86
|
-
...data,
|
|
87
|
-
}))
|
|
88
|
-
}
|
|
89
|
-
if (!Array.isArray(entry.versions)) entry.versions = []
|
|
90
|
-
if (!entry.namespace) entry.namespace = parseNamespace(name)
|
|
91
|
-
if (!entry.latest && entry.versions.length > 0) {
|
|
92
|
-
entry.latest = entry.versions[entry.versions.length - 1].version
|
|
93
|
-
}
|
|
94
|
-
return entry
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Local registry — stores published foundations in .unicloud/registry/
|
|
99
|
-
*/
|
|
100
|
-
export class LocalRegistry {
|
|
101
|
-
constructor(startDir) {
|
|
102
|
-
this.registryDir = getRegistryDir(startDir)
|
|
103
|
-
this.indexPath = join(this.registryDir, 'index.json')
|
|
104
|
-
this.packagesDir = join(this.registryDir, 'packages')
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
async _readIndex() {
|
|
108
|
-
if (!existsSync(this.indexPath)) return {}
|
|
109
|
-
const raw = JSON.parse(await readFile(this.indexPath, 'utf8'))
|
|
110
|
-
for (const name of Object.keys(raw)) {
|
|
111
|
-
normalizeEntry(name, raw[name])
|
|
112
|
-
}
|
|
113
|
-
return raw
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async _writeIndex(index) {
|
|
117
|
-
await mkdir(this.registryDir, { recursive: true })
|
|
118
|
-
await writeFile(this.indexPath, JSON.stringify(index, null, 2))
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Check if a specific version exists.
|
|
123
|
-
* @param {string} name
|
|
124
|
-
* @param {string} version
|
|
125
|
-
* @returns {Promise<boolean>}
|
|
126
|
-
*/
|
|
127
|
-
async exists(name, version) {
|
|
128
|
-
const index = await this._readIndex()
|
|
129
|
-
const versions = index[name]?.versions
|
|
130
|
-
if (!Array.isArray(versions)) return false
|
|
131
|
-
return versions.some(v => v.version === version)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Get all published versions for a package as an array of
|
|
136
|
-
* `{ version, publishedAt, ... }` entries (matches uniweb-edge).
|
|
137
|
-
* @param {string} name
|
|
138
|
-
* @returns {Promise<Array>}
|
|
139
|
-
*/
|
|
140
|
-
async getVersions(name) {
|
|
141
|
-
const index = await this._readIndex()
|
|
142
|
-
return index[name]?.versions || []
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Get the full version entry for `name@version`, or null if absent.
|
|
147
|
-
* Used by `publish` (pre-flight duplicate check) and `deploy`
|
|
148
|
-
* (staleness check via git provenance comparison).
|
|
149
|
-
*
|
|
150
|
-
* @param {string} name
|
|
151
|
-
* @param {string} version
|
|
152
|
-
* @returns {Promise<Object|null>}
|
|
153
|
-
*/
|
|
154
|
-
async getVersionEntry(name, version) {
|
|
155
|
-
const versions = await this.getVersions(name)
|
|
156
|
-
return versions.find(v => v.version === version) || null
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Publish a foundation to the local registry.
|
|
161
|
-
* Copies the dist directory and updates the index.
|
|
162
|
-
* @param {string} name
|
|
163
|
-
* @param {string} version
|
|
164
|
-
* @param {string} distDir - Path to the foundation's dist/ directory
|
|
165
|
-
* @param {Object} [metadata] - Additional metadata (publishedBy, etc.)
|
|
166
|
-
*/
|
|
167
|
-
async publish(name, version, distDir, metadata = {}) {
|
|
168
|
-
const safeName = sanitizeName(name)
|
|
169
|
-
const destDir = join(this.packagesDir, safeName, version)
|
|
170
|
-
|
|
171
|
-
await mkdir(destDir, { recursive: true })
|
|
172
|
-
await cp(distDir, destDir, { recursive: true })
|
|
173
|
-
|
|
174
|
-
const index = await this._readIndex()
|
|
175
|
-
if (!index[name]) {
|
|
176
|
-
index[name] = {
|
|
177
|
-
namespace: parseNamespace(name),
|
|
178
|
-
versions: [],
|
|
179
|
-
latest: null,
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
const versionEntry = {
|
|
184
|
-
version,
|
|
185
|
-
publishedAt: new Date().toISOString(),
|
|
186
|
-
...metadata,
|
|
187
|
-
}
|
|
188
|
-
const existingIdx = index[name].versions.findIndex(v => v.version === version)
|
|
189
|
-
if (existingIdx >= 0) {
|
|
190
|
-
index[name].versions[existingIdx] = versionEntry
|
|
191
|
-
} else {
|
|
192
|
-
index[name].versions.push(versionEntry)
|
|
193
|
-
}
|
|
194
|
-
index[name].latest = version
|
|
195
|
-
|
|
196
|
-
await this._writeIndex(index)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Get the filesystem path for a published package version.
|
|
201
|
-
* @param {string} name
|
|
202
|
-
* @param {string} version
|
|
203
|
-
* @returns {string}
|
|
204
|
-
*/
|
|
205
|
-
getPackagePath(name, version) {
|
|
206
|
-
return join(this.packagesDir, sanitizeName(name), version)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Create a local registry instance.
|
|
212
|
-
* @param {string} [startDir]
|
|
213
|
-
* @returns {LocalRegistry}
|
|
214
|
-
*/
|
|
215
|
-
export function createLocalRegistry(startDir) {
|
|
216
|
-
return new LocalRegistry(startDir)
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
* Remote registry — publishes foundations to a cloud server via HTTP.
|
|
221
|
-
*/
|
|
222
|
-
export class RemoteRegistry {
|
|
223
|
-
/**
|
|
224
|
-
* @param {string} apiUrl - Registry server URL (e.g. "http://localhost:4001")
|
|
225
|
-
* @param {string} [token] - Bearer token for authentication
|
|
226
|
-
*/
|
|
227
|
-
constructor(apiUrl, token) {
|
|
228
|
-
this.apiUrl = apiUrl.replace(/\/$/, '')
|
|
229
|
-
this.token = token
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Fetch the registry index from the server.
|
|
234
|
-
* @returns {Promise<Object>}
|
|
235
|
-
*/
|
|
236
|
-
async _fetchIndex() {
|
|
237
|
-
const res = await fetch(`${this.apiUrl}/`)
|
|
238
|
-
if (!res.ok) throw new Error(`Registry request failed: ${res.status}`)
|
|
239
|
-
return res.json()
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Check if a specific version exists on the remote.
|
|
244
|
-
* @param {string} name
|
|
245
|
-
* @param {string} version
|
|
246
|
-
* @returns {Promise<boolean>}
|
|
247
|
-
*/
|
|
248
|
-
async exists(name, version) {
|
|
249
|
-
try {
|
|
250
|
-
const index = await this._fetchIndex()
|
|
251
|
-
const versions = index[name]?.versions
|
|
252
|
-
if (!Array.isArray(versions)) return false
|
|
253
|
-
return versions.some(v => v.version === version)
|
|
254
|
-
} catch {
|
|
255
|
-
return false
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* Get all published versions for a package as an array of
|
|
261
|
-
* `{ version, publishedAt, ... }` entries.
|
|
262
|
-
* @param {string} name
|
|
263
|
-
* @returns {Promise<Array>}
|
|
264
|
-
*/
|
|
265
|
-
async getVersions(name) {
|
|
266
|
-
const index = await this._fetchIndex()
|
|
267
|
-
return index[name]?.versions || []
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Get the full version entry for `name@version`, or null if absent.
|
|
272
|
-
* Used by `publish` (pre-flight duplicate check) and `deploy`
|
|
273
|
-
* (staleness check via git provenance comparison).
|
|
274
|
-
*
|
|
275
|
-
* @param {string} name
|
|
276
|
-
* @param {string} version
|
|
277
|
-
* @returns {Promise<Object|null>}
|
|
278
|
-
*/
|
|
279
|
-
async getVersionEntry(name, version) {
|
|
280
|
-
try {
|
|
281
|
-
const versions = await this.getVersions(name)
|
|
282
|
-
return versions.find(v => v.version === version) || null
|
|
283
|
-
} catch {
|
|
284
|
-
return null
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Publish a foundation to the remote registry.
|
|
290
|
-
* Reads files from distDir, encodes as base64, and POSTs to the server.
|
|
291
|
-
*
|
|
292
|
-
* @param {string} name
|
|
293
|
-
* @param {string} version
|
|
294
|
-
* @param {string} distDir - Path to the foundation's dist/ directory
|
|
295
|
-
* @param {Object} [metadata] - Additional metadata
|
|
296
|
-
* @returns {Promise<{ name: string, version: string, filesCount: number }>}
|
|
297
|
-
*/
|
|
298
|
-
async publish(name, version, distDir, metadata = {}) {
|
|
299
|
-
// Walk distDir recursively and encode files as base64
|
|
300
|
-
const files = {}
|
|
301
|
-
const entries = await readdir(distDir, { withFileTypes: true, recursive: true })
|
|
302
|
-
|
|
303
|
-
for (const entry of entries) {
|
|
304
|
-
if (!entry.isFile()) continue
|
|
305
|
-
const fullPath = join(entry.parentPath || entry.path, entry.name)
|
|
306
|
-
const relPath = relative(distDir, fullPath)
|
|
307
|
-
const content = await readFile(fullPath)
|
|
308
|
-
files[relPath] = content.toString('base64')
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const { editAccess, ...restMetadata } = metadata
|
|
312
|
-
const payload = { name, version, files, metadata: restMetadata }
|
|
313
|
-
if (editAccess) {
|
|
314
|
-
payload.editAccess = editAccess
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const headers = { 'Content-Type': 'application/json' }
|
|
318
|
-
if (this.token) {
|
|
319
|
-
headers['Authorization'] = `Bearer ${this.token}`
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
const res = await fetch(`${this.apiUrl}/foundations`, {
|
|
323
|
-
method: 'POST',
|
|
324
|
-
headers,
|
|
325
|
-
body: JSON.stringify(payload),
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
const body = await res.json()
|
|
329
|
-
|
|
330
|
-
if (!res.ok) {
|
|
331
|
-
if (res.status === 409) {
|
|
332
|
-
throw Object.assign(new Error(body.error || `${name}@${version} already exists`), { code: 'CONFLICT' })
|
|
333
|
-
}
|
|
334
|
-
if (res.status === 401) {
|
|
335
|
-
throw Object.assign(new Error(body.error || 'Unauthorized'), { code: 'UNAUTHORIZED' })
|
|
336
|
-
}
|
|
337
|
-
throw new Error(body.error || `Server error (${res.status})`)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return body
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/**
|
|
344
|
-
* Common fetch helper with auth headers.
|
|
345
|
-
* @param {string} url
|
|
346
|
-
* @param {Object} [options]
|
|
347
|
-
* @returns {Promise<Response>}
|
|
348
|
-
*/
|
|
349
|
-
_authHeaders() {
|
|
350
|
-
const headers = { 'Content-Type': 'application/json' }
|
|
351
|
-
if (this.token) {
|
|
352
|
-
headers['Authorization'] = `Bearer ${this.token}`
|
|
353
|
-
}
|
|
354
|
-
return headers
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Create a foundation invite.
|
|
359
|
-
* @param {string} foundationName
|
|
360
|
-
* @param {Object} payload - { email, majorVersion, maxUses?, expiresInDays? }
|
|
361
|
-
* @returns {Promise<Object>}
|
|
362
|
-
*/
|
|
363
|
-
async createInvite(foundationName, payload) {
|
|
364
|
-
const res = await fetch(`${this.apiUrl}/api/foundations/${encodeURIComponent(foundationName)}/invites`, {
|
|
365
|
-
method: 'POST',
|
|
366
|
-
headers: this._authHeaders(),
|
|
367
|
-
body: JSON.stringify(payload),
|
|
368
|
-
})
|
|
369
|
-
const body = await res.json()
|
|
370
|
-
if (!res.ok) {
|
|
371
|
-
throw Object.assign(new Error(body.error || `Server error (${res.status})`), { statusCode: res.status })
|
|
372
|
-
}
|
|
373
|
-
return body
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
/**
|
|
377
|
-
* List invites for a foundation.
|
|
378
|
-
* @param {string} foundationName
|
|
379
|
-
* @returns {Promise<Array>}
|
|
380
|
-
*/
|
|
381
|
-
async listInvites(foundationName) {
|
|
382
|
-
const res = await fetch(`${this.apiUrl}/api/foundations/${encodeURIComponent(foundationName)}/invites`, {
|
|
383
|
-
headers: this._authHeaders(),
|
|
384
|
-
})
|
|
385
|
-
const body = await res.json()
|
|
386
|
-
if (!res.ok) {
|
|
387
|
-
throw Object.assign(new Error(body.error || `Server error (${res.status})`), { statusCode: res.status })
|
|
388
|
-
}
|
|
389
|
-
return body.invites || []
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Revoke a foundation invite.
|
|
394
|
-
* @param {string} foundationName
|
|
395
|
-
* @param {string} inviteId
|
|
396
|
-
* @returns {Promise<Object>}
|
|
397
|
-
*/
|
|
398
|
-
async revokeInvite(foundationName, inviteId) {
|
|
399
|
-
const res = await fetch(`${this.apiUrl}/api/foundations/${encodeURIComponent(foundationName)}/invites/${inviteId}`, {
|
|
400
|
-
method: 'DELETE',
|
|
401
|
-
headers: this._authHeaders(),
|
|
402
|
-
})
|
|
403
|
-
const body = await res.json()
|
|
404
|
-
if (!res.ok) {
|
|
405
|
-
throw Object.assign(new Error(body.error || `Server error (${res.status})`), { statusCode: res.status })
|
|
406
|
-
}
|
|
407
|
-
return body
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Resend a foundation invite.
|
|
412
|
-
* @param {string} foundationName
|
|
413
|
-
* @param {string} inviteId
|
|
414
|
-
* @returns {Promise<Object>}
|
|
415
|
-
*/
|
|
416
|
-
async resendInvite(foundationName, inviteId) {
|
|
417
|
-
const res = await fetch(`${this.apiUrl}/api/foundations/${encodeURIComponent(foundationName)}/invites/${inviteId}/resend`, {
|
|
418
|
-
method: 'POST',
|
|
419
|
-
headers: this._authHeaders(),
|
|
420
|
-
})
|
|
421
|
-
const body = await res.json()
|
|
422
|
-
if (!res.ok) {
|
|
423
|
-
throw Object.assign(new Error(body.error || `Server error (${res.status})`), { statusCode: res.status })
|
|
424
|
-
}
|
|
425
|
-
return body
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Create a site record on Unicloud.
|
|
430
|
-
* @param {string} siteId
|
|
431
|
-
* @param {Object} options
|
|
432
|
-
* @param {Object} options.foundation - { name }
|
|
433
|
-
* @returns {Promise<Object>}
|
|
434
|
-
*/
|
|
435
|
-
async createSite(siteId, { foundation }) {
|
|
436
|
-
const res = await fetch(`${this.apiUrl}/api/sites`, {
|
|
437
|
-
method: 'POST',
|
|
438
|
-
headers: this._authHeaders(),
|
|
439
|
-
body: JSON.stringify({ siteId, foundation }),
|
|
440
|
-
})
|
|
441
|
-
const body = await res.json()
|
|
442
|
-
if (!res.ok) {
|
|
443
|
-
throw Object.assign(new Error(body.error || `Server error (${res.status})`), { statusCode: res.status })
|
|
444
|
-
}
|
|
445
|
-
return body
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
/**
|
|
449
|
-
* Transfer site ownership.
|
|
450
|
-
* @param {string} siteId
|
|
451
|
-
* @param {string} newOwner - Email of the new owner
|
|
452
|
-
* @returns {Promise<Object>}
|
|
453
|
-
*/
|
|
454
|
-
async transferSiteOwnership(siteId, newOwner) {
|
|
455
|
-
const res = await fetch(`${this.apiUrl}/api/sites/${siteId}/owner`, {
|
|
456
|
-
method: 'PATCH',
|
|
457
|
-
headers: this._authHeaders(),
|
|
458
|
-
body: JSON.stringify({ newOwner }),
|
|
459
|
-
})
|
|
460
|
-
const body = await res.json()
|
|
461
|
-
if (!res.ok) {
|
|
462
|
-
throw Object.assign(new Error(body.error || `Server error (${res.status})`), { statusCode: res.status })
|
|
463
|
-
}
|
|
464
|
-
return body
|
|
465
|
-
}
|
|
466
|
-
}
|