@verdaccio/store 9.0.0-next-9.28 → 9.0.0-next-9.30

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/build/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { Storage } from './storage';
2
+ export * from './stage-storage';
2
3
  export { applyManifestFilters, loadFilterPlugins } from './filter-pipeline';
3
4
  export type { Filters } from './filter-pipeline';
4
5
  export * from './lib/storage-utils';
package/build/index.js CHANGED
@@ -6,8 +6,11 @@ const require_filter_pipeline = require("./filter-pipeline.js");
6
6
  const require_publish_utils = require("./lib/publish-utils.js");
7
7
  const require_versions_utils = require("./lib/versions-utils.js");
8
8
  const require_storage_utils = require("./lib/storage-utils.js");
9
+ const require_stage_storage = require("./stage-storage.js");
9
10
  const require_storage = require("./storage.js");
11
+ exports.STAGE_NAMESPACE = require_stage_storage.STAGE_NAMESPACE;
10
12
  exports.STORAGE = require_storage_utils.STORAGE;
13
+ exports.StageStorage = require_stage_storage.StageStorage;
11
14
  exports.Storage = require_storage.Storage;
12
15
  exports.WHITELIST = require_storage_utils.WHITELIST;
13
16
  exports.applyManifestFilters = require_filter_pipeline.applyManifestFilters;
@@ -35,4 +38,5 @@ exports.removeLowerVersions = require_versions_utils.removeLowerVersions;
35
38
  exports.sortVersionsAndFilterInvalid = require_versions_utils.sortVersionsAndFilterInvalid;
36
39
  exports.tagVersion = require_versions_utils.tagVersion;
37
40
  exports.tagVersionNext = require_versions_utils.tagVersionNext;
41
+ exports.toStagePackageVersion = require_stage_storage.toStagePackageVersion;
38
42
  exports.updateUpLinkMetadata = require_storage_utils.updateUpLinkMetadata;
package/build/index.mjs CHANGED
@@ -2,5 +2,6 @@ import { applyManifestFilters, loadFilterPlugins } from "./filter-pipeline.mjs";
2
2
  import { isPublishablePackage } from "./lib/publish-utils.mjs";
3
3
  import { getVersion, isNewerVersion, removeLowerVersions, sortVersionsAndFilterInvalid, tagVersion, tagVersionNext } from "./lib/versions-utils.mjs";
4
4
  import { STORAGE, WHITELIST, checkPackageRemote, cleanUpLinksRef, cleanUpReadme, generatePackageTemplate, generateRevision, getLatestReadme, hasDeprecatedVersions, isDeprecatedManifest, mapManifestToSearchPackageBody, mergeUplinkTimeIntoLocal, mergeUplinkTimeIntoLocalNext, mergeVersions, normalizeContributors, normalizeDistTags, normalizePackage, prepareSearchPackage, updateUpLinkMetadata } from "./lib/storage-utils.mjs";
5
+ import { STAGE_NAMESPACE, StageStorage, toStagePackageVersion } from "./stage-storage.mjs";
5
6
  import { Storage } from "./storage.mjs";
6
- export { STORAGE, Storage, WHITELIST, applyManifestFilters, checkPackageRemote, cleanUpLinksRef, cleanUpReadme, generatePackageTemplate, generateRevision, getLatestReadme, getVersion, hasDeprecatedVersions, isDeprecatedManifest, isNewerVersion, isPublishablePackage, loadFilterPlugins, mapManifestToSearchPackageBody, mergeUplinkTimeIntoLocal, mergeUplinkTimeIntoLocalNext, mergeVersions, normalizeContributors, normalizeDistTags, normalizePackage, prepareSearchPackage, removeLowerVersions, sortVersionsAndFilterInvalid, tagVersion, tagVersionNext, updateUpLinkMetadata };
7
+ export { STAGE_NAMESPACE, STORAGE, StageStorage, Storage, WHITELIST, applyManifestFilters, checkPackageRemote, cleanUpLinksRef, cleanUpReadme, generatePackageTemplate, generateRevision, getLatestReadme, getVersion, hasDeprecatedVersions, isDeprecatedManifest, isNewerVersion, isPublishablePackage, loadFilterPlugins, mapManifestToSearchPackageBody, mergeUplinkTimeIntoLocal, mergeUplinkTimeIntoLocalNext, mergeVersions, normalizeContributors, normalizeDistTags, normalizePackage, prepareSearchPackage, removeLowerVersions, sortVersionsAndFilterInvalid, tagVersion, tagVersionNext, toStagePackageVersion, updateUpLinkMetadata };
@@ -61,7 +61,7 @@ export declare function updateUpLinkMetadata(uplinkName: string, manifest: Manif
61
61
  _attachments: import("@verdaccio/types").AttachMents;
62
62
  publisher?: import("@verdaccio/types").Publisher;
63
63
  publishedPackage?: string;
64
- publishType?: 'publish' | 'unpublish';
64
+ publishType?: 'publish' | 'unpublish' | 'stage' | 'unstage';
65
65
  _uplinks: {
66
66
  [x: string]: import("@verdaccio/types").UpLinkMetadata | {
67
67
  etag: string;
@@ -0,0 +1,120 @@
1
+ import { Readable } from 'node:stream';
2
+ import type { pluginUtils } from '@verdaccio/core';
3
+ import type { Logger, Manifest } from '@verdaccio/types';
4
+ /**
5
+ * Storage namespace holding every staged item.
6
+ *
7
+ * It is deliberately NOT registered through `storagePlugin.add()`: doing so
8
+ * would list it in the plugin database and leak it into search results and the
9
+ * web UI package list. The staged items are enumerated through {@link STAGE_INDEX}
10
+ * instead.
11
+ */
12
+ export declare const STAGE_NAMESPACE = ".stage";
13
+ /**
14
+ * Metadata of a package version awaiting review.
15
+ *
16
+ * Mirrors the `StagePackageVersion` schema documented by npmjs, plus
17
+ * {@link StageRecord.tarballFilename}, which is internal and must be stripped
18
+ * before serving the record over the API (see {@link toStagePackageVersion}).
19
+ */
20
+ export interface StageRecord {
21
+ /** Unique identifier. Must be a UUID: the npm CLI validates the format client side. */
22
+ id: string;
23
+ packageName: string;
24
+ version: string;
25
+ /** dist-tag the version will be published under once approved. */
26
+ tag: string;
27
+ /** ISO timestamp of when the version was staged. */
28
+ createdAt: string;
29
+ /** Username of whoever staged the version. */
30
+ actor: string;
31
+ actorType: 'user';
32
+ access: 'public' | 'private';
33
+ /** sha1 of the tarball, as reported by `npm stage list`. */
34
+ shasum: string;
35
+ /** Internal: name of the tarball inside the staged item folder. */
36
+ tarballFilename: string;
37
+ }
38
+ /** Shape served to clients: {@link StageRecord} without the internal fields. */
39
+ export type StagePackageVersion = Omit<StageRecord, 'tarballFilename'>;
40
+ /**
41
+ * The publish payload kept for a staged item, minus `_attachments` (the tarball
42
+ * is stored separately as a real tarball, not as base64 inside this document).
43
+ */
44
+ export type StagedPackument = Omit<Manifest, '_attachments'>;
45
+ export interface AddStageInput {
46
+ packageName: string;
47
+ version: string;
48
+ tag: string;
49
+ actor: string;
50
+ access: 'public' | 'private';
51
+ shasum: string;
52
+ tarballFilename: string;
53
+ packument: StagedPackument;
54
+ }
55
+ export interface ListStageFilter {
56
+ /** Restrict the result to a single package name. */
57
+ packageName?: string;
58
+ }
59
+ /** Strip the internal fields before the record leaves the registry. */
60
+ export declare function toStagePackageVersion(record: StageRecord): StagePackageVersion;
61
+ /**
62
+ * Persistence for the staged publish workflow (`npm stage`).
63
+ *
64
+ * Everything goes through {@link pluginUtils.StorageHandler}, so it works with
65
+ * any storage plugin (local-storage, memory, S3, GCS) without the plugin having
66
+ * to know that staging exists. On disk it looks like:
67
+ *
68
+ * ```
69
+ * <storage>/.stage/package.json index of every staged item
70
+ * <storage>/.stage/<uuid>/package.json the staged item (record + packument)
71
+ * <storage>/.stage/<uuid>/<name>-<v>.tgz the staged tarball
72
+ * ```
73
+ */
74
+ export declare class StageStorage {
75
+ private readonly plugin;
76
+ private readonly logger;
77
+ /**
78
+ * Serializes index mutations within this process.
79
+ *
80
+ * `StorageHandler.updatePackage()` takes a lock in local-storage but releases
81
+ * it before we get to save, and other plugins do not lock at all, so a
82
+ * read-modify-write on the index would race. This keeps a single Verdaccio
83
+ * process consistent; several processes sharing one storage backend are NOT
84
+ * supported.
85
+ */
86
+ private indexQueue;
87
+ constructor(plugin: pluginUtils.Storage<any>, logger: Logger);
88
+ /** Stage a new version. Returns the persisted record, including its generated id. */
89
+ add(input: AddStageInput, tarball: Buffer, { signal }: {
90
+ signal: AbortSignal;
91
+ }): Promise<StageRecord>;
92
+ /** Every staged item, newest first, optionally narrowed to one package. */
93
+ list(filter?: ListStageFilter): Promise<StageRecord[]>;
94
+ /** A single staged item, or `undefined` when the id is unknown. */
95
+ get(id: string): Promise<StageRecord | undefined>;
96
+ /** Whether a version of a package is already staged (used to answer 409). */
97
+ findByVersion(packageName: string, version: string): Promise<StageRecord | undefined>;
98
+ /** The publish payload kept for a staged item, needed to approve it. */
99
+ getPackument(id: string): Promise<StagedPackument | undefined>;
100
+ /** Read the staged tarball as a stream. */
101
+ readTarball(id: string, { signal }: {
102
+ signal: AbortSignal;
103
+ }): Promise<Readable>;
104
+ /**
105
+ * Drop a staged item and its tarball.
106
+ *
107
+ * @returns `false` when the id was unknown, `true` when something was removed.
108
+ */
109
+ remove(id: string): Promise<boolean>;
110
+ private itemStorage;
111
+ private indexStorage;
112
+ private readDocument;
113
+ private readIndex;
114
+ /** Read-modify-write the index, serialized against other callers (see {@link indexQueue}). */
115
+ private updateIndex;
116
+ private enqueueIndexMutation;
117
+ private writeIndex;
118
+ private writeTarball;
119
+ private waitForTarball;
120
+ }
@@ -0,0 +1,207 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.js");
2
+ const require_storage_utils = require("./lib/storage-utils.js");
3
+ let debug = require("debug");
4
+ debug = require_runtime.__toESM(debug);
5
+ let node_stream = require("node:stream");
6
+ let node_stream_promises = require("node:stream/promises");
7
+ let _verdaccio_core = require("@verdaccio/core");
8
+ let node_crypto = require("node:crypto");
9
+ //#region src/stage-storage.ts
10
+ var debug$1 = (0, debug.default)("verdaccio:storage:stage");
11
+ /**
12
+ * Storage namespace holding every staged item.
13
+ *
14
+ * It is deliberately NOT registered through `storagePlugin.add()`: doing so
15
+ * would list it in the plugin database and leak it into search results and the
16
+ * web UI package list. The staged items are enumerated through {@link STAGE_INDEX}
17
+ * instead.
18
+ */
19
+ var STAGE_NAMESPACE = ".stage";
20
+ /**
21
+ * Logical name used when reading/writing the index document. Storage handlers
22
+ * are scoped to a folder and ignore this argument when resolving the path, but
23
+ * it is what shows up in plugin logs, so keep it descriptive.
24
+ */
25
+ var STAGE_INDEX = "stage-index";
26
+ /** Strip the internal fields before the record leaves the registry. */
27
+ function toStagePackageVersion(record) {
28
+ const { tarballFilename: _tarballFilename, ...rest } = record;
29
+ return rest;
30
+ }
31
+ function isNotFound(err) {
32
+ return err?.code === "ENOENT" || err?.code === 404 || err?.status === 404;
33
+ }
34
+ /**
35
+ * Persistence for the staged publish workflow (`npm stage`).
36
+ *
37
+ * Everything goes through {@link pluginUtils.StorageHandler}, so it works with
38
+ * any storage plugin (local-storage, memory, S3, GCS) without the plugin having
39
+ * to know that staging exists. On disk it looks like:
40
+ *
41
+ * ```
42
+ * <storage>/.stage/package.json index of every staged item
43
+ * <storage>/.stage/<uuid>/package.json the staged item (record + packument)
44
+ * <storage>/.stage/<uuid>/<name>-<v>.tgz the staged tarball
45
+ * ```
46
+ */
47
+ var StageStorage = class {
48
+ plugin;
49
+ logger;
50
+ /**
51
+ * Serializes index mutations within this process.
52
+ *
53
+ * `StorageHandler.updatePackage()` takes a lock in local-storage but releases
54
+ * it before we get to save, and other plugins do not lock at all, so a
55
+ * read-modify-write on the index would race. This keeps a single Verdaccio
56
+ * process consistent; several processes sharing one storage backend are NOT
57
+ * supported.
58
+ */
59
+ indexQueue = Promise.resolve();
60
+ constructor(plugin, logger) {
61
+ this.plugin = plugin;
62
+ this.logger = logger;
63
+ }
64
+ /** Stage a new version. Returns the persisted record, including its generated id. */
65
+ async add(input, tarball, { signal }) {
66
+ if (_verdaccio_core.validationUtils.validateName(input.tarballFilename) === false) throw _verdaccio_core.errorUtils.getBadRequest(`invalid tarball name ${input.tarballFilename}`);
67
+ const record = {
68
+ id: (0, node_crypto.randomUUID)(),
69
+ packageName: input.packageName,
70
+ version: input.version,
71
+ tag: input.tag,
72
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
73
+ actor: input.actor,
74
+ actorType: "user",
75
+ access: input.access,
76
+ shasum: input.shasum,
77
+ tarballFilename: input.tarballFilename
78
+ };
79
+ debug$1("staging %o@%o as %o", record.packageName, record.version, record.id);
80
+ return this.enqueueIndexMutation(async () => {
81
+ const items = await this.readIndex();
82
+ if (items.find((item) => item.packageName === record.packageName && item.version === record.version)) throw _verdaccio_core.errorUtils.getConflict(`${record.packageName}@${record.version} is already staged`);
83
+ const handler = this.itemStorage(record.id);
84
+ await this.writeTarball(handler, record.tarballFilename, tarball, { signal });
85
+ const document = {
86
+ record,
87
+ packument: input.packument
88
+ };
89
+ await handler.savePackage(record.id, document);
90
+ await this.writeIndex([record, ...items]);
91
+ this.logger.info({
92
+ packageName: record.packageName,
93
+ version: record.version,
94
+ stageId: record.id
95
+ }, "staged @{packageName}@@{version} as @{stageId}");
96
+ return record;
97
+ });
98
+ }
99
+ /** Every staged item, newest first, optionally narrowed to one package. */
100
+ async list(filter = {}) {
101
+ const items = await this.readIndex();
102
+ if (typeof filter.packageName === "string") return items.filter((item) => item.packageName === filter.packageName);
103
+ return items;
104
+ }
105
+ /** A single staged item, or `undefined` when the id is unknown. */
106
+ async get(id) {
107
+ return (await this.readIndex()).find((item) => item.id === id);
108
+ }
109
+ /** Whether a version of a package is already staged (used to answer 409). */
110
+ async findByVersion(packageName, version) {
111
+ return (await this.readIndex()).find((item) => item.packageName === packageName && item.version === version);
112
+ }
113
+ /** The publish payload kept for a staged item, needed to approve it. */
114
+ async getPackument(id) {
115
+ return (await this.readDocument(id))?.packument;
116
+ }
117
+ /** Read the staged tarball as a stream. */
118
+ async readTarball(id, { signal }) {
119
+ const record = await this.get(id);
120
+ if (!record) throw _verdaccio_core.errorUtils.getNotFound("no such staged package version");
121
+ return this.itemStorage(id).readTarball(record.tarballFilename, { signal });
122
+ }
123
+ /**
124
+ * Drop a staged item and its tarball.
125
+ *
126
+ * @returns `false` when the id was unknown, `true` when something was removed.
127
+ */
128
+ async remove(id) {
129
+ const record = await this.get(id);
130
+ if (!record) return false;
131
+ const handler = this.itemStorage(id);
132
+ await this.updateIndex((items) => items.filter((item) => item.id !== id));
133
+ for (const file of [record.tarballFilename, require_storage_utils.STORAGE.PACKAGE_FILE_NAME]) try {
134
+ await handler.deletePackage(file);
135
+ } catch (err) {
136
+ debug$1("could not delete staged file %o: %s", file, err?.message);
137
+ }
138
+ try {
139
+ await handler.removePackage(id);
140
+ } catch (err) {
141
+ debug$1("could not delete staged folder %o: %s", id, err?.message);
142
+ }
143
+ debug$1("removed staged item %o", id);
144
+ return true;
145
+ }
146
+ itemStorage(id) {
147
+ return this.plugin.getPackageStorage(`${STAGE_NAMESPACE}/${id}`);
148
+ }
149
+ indexStorage() {
150
+ return this.plugin.getPackageStorage(STAGE_NAMESPACE);
151
+ }
152
+ async readDocument(id) {
153
+ try {
154
+ return await this.itemStorage(id).readPackage(id);
155
+ } catch (err) {
156
+ if (isNotFound(err)) return;
157
+ throw err;
158
+ }
159
+ }
160
+ async readIndex() {
161
+ try {
162
+ const index = await this.indexStorage().readPackage(STAGE_INDEX);
163
+ return Array.isArray(index?.items) ? index.items : [];
164
+ } catch (err) {
165
+ if (isNotFound(err)) {
166
+ debug$1("no stage index yet");
167
+ return [];
168
+ }
169
+ throw err;
170
+ }
171
+ }
172
+ /** Read-modify-write the index, serialized against other callers (see {@link indexQueue}). */
173
+ async updateIndex(mutate) {
174
+ return this.enqueueIndexMutation(async () => {
175
+ const items = mutate(await this.readIndex());
176
+ await this.writeIndex(items);
177
+ return items;
178
+ });
179
+ }
180
+ async enqueueIndexMutation(run) {
181
+ const queued = this.indexQueue.then(run, run);
182
+ this.indexQueue = queued.catch(() => void 0);
183
+ return queued;
184
+ }
185
+ async writeIndex(items) {
186
+ const index = { items };
187
+ await this.indexStorage().savePackage(STAGE_INDEX, index);
188
+ }
189
+ async writeTarball(handler, filename, tarball, { signal }) {
190
+ const writeStream = await handler.writeTarball(filename, { signal });
191
+ await (0, node_stream_promises.pipeline)(node_stream.Readable.from(tarball), writeStream);
192
+ await this.waitForTarball(handler, filename);
193
+ }
194
+ async waitForTarball(handler, filename, attempts = 20) {
195
+ for (let attempt = 0; attempt < attempts; attempt++) {
196
+ if (await handler.hasTarball(filename)) return;
197
+ await new Promise((resolve) => setTimeout(resolve, 10));
198
+ }
199
+ throw _verdaccio_core.errorUtils.getInternalError(`staged tarball ${filename} was never persisted`);
200
+ }
201
+ };
202
+ //#endregion
203
+ exports.STAGE_NAMESPACE = STAGE_NAMESPACE;
204
+ exports.StageStorage = StageStorage;
205
+ exports.toStagePackageVersion = toStagePackageVersion;
206
+
207
+ //# sourceMappingURL=stage-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-storage.js","names":[],"sources":["../src/stage-storage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport { randomUUID } from 'node:crypto';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\n\nimport type { pluginUtils } from '@verdaccio/core';\nimport { errorUtils, validationUtils } from '@verdaccio/core';\nimport type { Logger, Manifest } from '@verdaccio/types';\n\nimport { STORAGE } from './lib/storage-utils';\n\nconst debug = buildDebug('verdaccio:storage:stage');\n\n/**\n * Storage namespace holding every staged item.\n *\n * It is deliberately NOT registered through `storagePlugin.add()`: doing so\n * would list it in the plugin database and leak it into search results and the\n * web UI package list. The staged items are enumerated through {@link STAGE_INDEX}\n * instead.\n */\nexport const STAGE_NAMESPACE = '.stage';\n\n/**\n * Logical name used when reading/writing the index document. Storage handlers\n * are scoped to a folder and ignore this argument when resolving the path, but\n * it is what shows up in plugin logs, so keep it descriptive.\n */\nconst STAGE_INDEX = 'stage-index';\n\n/**\n * Metadata of a package version awaiting review.\n *\n * Mirrors the `StagePackageVersion` schema documented by npmjs, plus\n * {@link StageRecord.tarballFilename}, which is internal and must be stripped\n * before serving the record over the API (see {@link toStagePackageVersion}).\n */\nexport interface StageRecord {\n /** Unique identifier. Must be a UUID: the npm CLI validates the format client side. */\n id: string;\n packageName: string;\n version: string;\n /** dist-tag the version will be published under once approved. */\n tag: string;\n /** ISO timestamp of when the version was staged. */\n createdAt: string;\n /** Username of whoever staged the version. */\n actor: string;\n actorType: 'user';\n access: 'public' | 'private';\n /** sha1 of the tarball, as reported by `npm stage list`. */\n shasum: string;\n /** Internal: name of the tarball inside the staged item folder. */\n tarballFilename: string;\n}\n\n/** Shape served to clients: {@link StageRecord} without the internal fields. */\nexport type StagePackageVersion = Omit<StageRecord, 'tarballFilename'>;\n\n/**\n * The publish payload kept for a staged item, minus `_attachments` (the tarball\n * is stored separately as a real tarball, not as base64 inside this document).\n */\nexport type StagedPackument = Omit<Manifest, '_attachments'>;\n\n/** Persisted document of a single staged item. */\ninterface StageDocument {\n record: StageRecord;\n packument: StagedPackument;\n}\n\n/** Persisted index of every staged item, newest first. */\ninterface StageIndex {\n items: StageRecord[];\n}\n\nexport interface AddStageInput {\n packageName: string;\n version: string;\n tag: string;\n actor: string;\n access: 'public' | 'private';\n shasum: string;\n tarballFilename: string;\n packument: StagedPackument;\n}\n\nexport interface ListStageFilter {\n /** Restrict the result to a single package name. */\n packageName?: string;\n}\n\n/** Strip the internal fields before the record leaves the registry. */\nexport function toStagePackageVersion(record: StageRecord): StagePackageVersion {\n const { tarballFilename: _tarballFilename, ...rest } = record;\n return rest;\n}\n\nfunction isNotFound(err: any): boolean {\n return err?.code === 'ENOENT' || err?.code === 404 || err?.status === 404;\n}\n\n/**\n * Persistence for the staged publish workflow (`npm stage`).\n *\n * Everything goes through {@link pluginUtils.StorageHandler}, so it works with\n * any storage plugin (local-storage, memory, S3, GCS) without the plugin having\n * to know that staging exists. On disk it looks like:\n *\n * ```\n * <storage>/.stage/package.json index of every staged item\n * <storage>/.stage/<uuid>/package.json the staged item (record + packument)\n * <storage>/.stage/<uuid>/<name>-<v>.tgz the staged tarball\n * ```\n */\nexport class StageStorage {\n private readonly plugin: pluginUtils.Storage<any>;\n private readonly logger: Logger;\n /**\n * Serializes index mutations within this process.\n *\n * `StorageHandler.updatePackage()` takes a lock in local-storage but releases\n * it before we get to save, and other plugins do not lock at all, so a\n * read-modify-write on the index would race. This keeps a single Verdaccio\n * process consistent; several processes sharing one storage backend are NOT\n * supported.\n */\n private indexQueue: Promise<unknown> = Promise.resolve();\n\n public constructor(plugin: pluginUtils.Storage<any>, logger: Logger) {\n this.plugin = plugin;\n this.logger = logger;\n }\n\n /** Stage a new version. Returns the persisted record, including its generated id. */\n public async add(\n input: AddStageInput,\n tarball: Buffer,\n { signal }: { signal: AbortSignal }\n ): Promise<StageRecord> {\n // the filename reaches us from the `_attachments` key of the request, so it\n // is attacker controlled. The regular publish path asserts the same thing\n // before writing; without it a name like `..` escapes the item folder and\n // the write fails inside an unawaited stream handler, taking the process\n // down rather than the request.\n if (validationUtils.validateName(input.tarballFilename) === false) {\n throw errorUtils.getBadRequest(`invalid tarball name ${input.tarballFilename}`);\n }\n\n const record: StageRecord = {\n id: randomUUID(),\n packageName: input.packageName,\n version: input.version,\n tag: input.tag,\n createdAt: new Date().toISOString(),\n actor: input.actor,\n actorType: 'user',\n access: input.access,\n shasum: input.shasum,\n tarballFilename: input.tarballFilename,\n };\n debug('staging %o@%o as %o', record.packageName, record.version, record.id);\n\n return this.enqueueIndexMutation(async () => {\n const items = await this.readIndex();\n const alreadyStaged = items.find(\n (item) => item.packageName === record.packageName && item.version === record.version\n );\n if (alreadyStaged) {\n throw errorUtils.getConflict(`${record.packageName}@${record.version} is already staged`);\n }\n\n const handler = this.itemStorage(record.id);\n // the tarball goes first: if it fails there is no record pointing at a\n // tarball that never landed.\n await this.writeTarball(handler, record.tarballFilename, tarball, { signal });\n const document: StageDocument = { record, packument: input.packument };\n await handler.savePackage(record.id, document as unknown as Manifest);\n await this.writeIndex([record, ...items]);\n\n this.logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'staged @{packageName}@@{version} as @{stageId}'\n );\n return record;\n });\n }\n\n /** Every staged item, newest first, optionally narrowed to one package. */\n public async list(filter: ListStageFilter = {}): Promise<StageRecord[]> {\n const items = await this.readIndex();\n if (typeof filter.packageName === 'string') {\n return items.filter((item) => item.packageName === filter.packageName);\n }\n return items;\n }\n\n /** A single staged item, or `undefined` when the id is unknown. */\n public async get(id: string): Promise<StageRecord | undefined> {\n const items = await this.readIndex();\n return items.find((item) => item.id === id);\n }\n\n /** Whether a version of a package is already staged (used to answer 409). */\n public async findByVersion(\n packageName: string,\n version: string\n ): Promise<StageRecord | undefined> {\n const items = await this.readIndex();\n return items.find((item) => item.packageName === packageName && item.version === version);\n }\n\n /** The publish payload kept for a staged item, needed to approve it. */\n public async getPackument(id: string): Promise<StagedPackument | undefined> {\n const document = await this.readDocument(id);\n return document?.packument;\n }\n\n /** Read the staged tarball as a stream. */\n public async readTarball(id: string, { signal }: { signal: AbortSignal }): Promise<Readable> {\n const record = await this.get(id);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n return this.itemStorage(id).readTarball(record.tarballFilename, { signal });\n }\n\n /**\n * Drop a staged item and its tarball.\n *\n * @returns `false` when the id was unknown, `true` when something was removed.\n */\n public async remove(id: string): Promise<boolean> {\n const record = await this.get(id);\n if (!record) {\n return false;\n }\n const handler = this.itemStorage(id);\n // the index entry goes first: a leftover folder is invisible, whereas a\n // leftover index entry would surface a staged item that cannot be read.\n await this.updateIndex((items) => items.filter((item) => item.id !== id));\n // the folder can only be dropped once it is empty, so both files go first\n for (const file of [record.tarballFilename, STORAGE.PACKAGE_FILE_NAME]) {\n try {\n await handler.deletePackage(file);\n } catch (err: any) {\n debug('could not delete staged file %o: %s', file, err?.message);\n }\n }\n try {\n await handler.removePackage(id);\n } catch (err: any) {\n debug('could not delete staged folder %o: %s', id, err?.message);\n }\n debug('removed staged item %o', id);\n return true;\n }\n\n private itemStorage(id: string): pluginUtils.StorageHandler {\n return this.plugin.getPackageStorage(`${STAGE_NAMESPACE}/${id}`);\n }\n\n private indexStorage(): pluginUtils.StorageHandler {\n return this.plugin.getPackageStorage(STAGE_NAMESPACE);\n }\n\n private async readDocument(id: string): Promise<StageDocument | undefined> {\n try {\n const document = (await this.itemStorage(id).readPackage(id)) as unknown as StageDocument;\n return document;\n } catch (err: any) {\n if (isNotFound(err)) {\n return undefined;\n }\n throw err;\n }\n }\n\n private async readIndex(): Promise<StageRecord[]> {\n try {\n const index = (await this.indexStorage().readPackage(STAGE_INDEX)) as unknown as StageIndex;\n return Array.isArray(index?.items) ? index.items : [];\n } catch (err: any) {\n if (isNotFound(err)) {\n debug('no stage index yet');\n return [];\n }\n throw err;\n }\n }\n\n /** Read-modify-write the index, serialized against other callers (see {@link indexQueue}). */\n private async updateIndex(\n mutate: (items: StageRecord[]) => StageRecord[]\n ): Promise<StageRecord[]> {\n return this.enqueueIndexMutation(async (): Promise<StageRecord[]> => {\n const items = mutate(await this.readIndex());\n await this.writeIndex(items);\n return items;\n });\n }\n\n private async enqueueIndexMutation<T>(run: () => Promise<T>): Promise<T> {\n const queued = this.indexQueue.then(run, run);\n // keep the chain alive even when a caller rejects\n this.indexQueue = queued.catch(() => undefined);\n return queued;\n }\n\n private async writeIndex(items: StageRecord[]): Promise<void> {\n const index: StageIndex = { items };\n await this.indexStorage().savePackage(STAGE_INDEX, index as unknown as Manifest);\n }\n\n private async writeTarball(\n handler: pluginUtils.StorageHandler,\n filename: string,\n tarball: Buffer,\n { signal }: { signal: AbortSignal }\n ): Promise<void> {\n const writeStream = await handler.writeTarball(filename, { signal });\n\n // `StorageHandler.writeTarball` only promises a Writable. The two bundled\n // plugins happen to emit 'open' before accepting writes, but nothing in the\n // interface says a plugin must, and waiting for it would hang staging\n // forever on one that does not. Piping straight away is correct either way:\n // a stream that is not ready yet buffers the writes.\n await pipeline(Readable.from(tarball), writeStream);\n\n // Both plugins rename the temporary file inside an async 'close' handler\n // that nobody awaits, so 'close' does not guarantee the tarball is in its\n // final place yet. Give the rename a bounded chance to land.\n await this.waitForTarball(handler, filename);\n }\n\n private async waitForTarball(\n handler: pluginUtils.StorageHandler,\n filename: string,\n attempts = 20\n ): Promise<void> {\n for (let attempt = 0; attempt < attempts; attempt++) {\n if (await handler.hasTarball(filename)) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n throw errorUtils.getInternalError(`staged tarball ${filename} was never persisted`);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,203 @@
1
+ import { STORAGE } from "./lib/storage-utils.mjs";
2
+ import buildDebug from "debug";
3
+ import { Readable } from "node:stream";
4
+ import { pipeline } from "node:stream/promises";
5
+ import { errorUtils, validationUtils } from "@verdaccio/core";
6
+ import { randomUUID } from "node:crypto";
7
+ //#region src/stage-storage.ts
8
+ var debug = buildDebug("verdaccio:storage:stage");
9
+ /**
10
+ * Storage namespace holding every staged item.
11
+ *
12
+ * It is deliberately NOT registered through `storagePlugin.add()`: doing so
13
+ * would list it in the plugin database and leak it into search results and the
14
+ * web UI package list. The staged items are enumerated through {@link STAGE_INDEX}
15
+ * instead.
16
+ */
17
+ var STAGE_NAMESPACE = ".stage";
18
+ /**
19
+ * Logical name used when reading/writing the index document. Storage handlers
20
+ * are scoped to a folder and ignore this argument when resolving the path, but
21
+ * it is what shows up in plugin logs, so keep it descriptive.
22
+ */
23
+ var STAGE_INDEX = "stage-index";
24
+ /** Strip the internal fields before the record leaves the registry. */
25
+ function toStagePackageVersion(record) {
26
+ const { tarballFilename: _tarballFilename, ...rest } = record;
27
+ return rest;
28
+ }
29
+ function isNotFound(err) {
30
+ return err?.code === "ENOENT" || err?.code === 404 || err?.status === 404;
31
+ }
32
+ /**
33
+ * Persistence for the staged publish workflow (`npm stage`).
34
+ *
35
+ * Everything goes through {@link pluginUtils.StorageHandler}, so it works with
36
+ * any storage plugin (local-storage, memory, S3, GCS) without the plugin having
37
+ * to know that staging exists. On disk it looks like:
38
+ *
39
+ * ```
40
+ * <storage>/.stage/package.json index of every staged item
41
+ * <storage>/.stage/<uuid>/package.json the staged item (record + packument)
42
+ * <storage>/.stage/<uuid>/<name>-<v>.tgz the staged tarball
43
+ * ```
44
+ */
45
+ var StageStorage = class {
46
+ plugin;
47
+ logger;
48
+ /**
49
+ * Serializes index mutations within this process.
50
+ *
51
+ * `StorageHandler.updatePackage()` takes a lock in local-storage but releases
52
+ * it before we get to save, and other plugins do not lock at all, so a
53
+ * read-modify-write on the index would race. This keeps a single Verdaccio
54
+ * process consistent; several processes sharing one storage backend are NOT
55
+ * supported.
56
+ */
57
+ indexQueue = Promise.resolve();
58
+ constructor(plugin, logger) {
59
+ this.plugin = plugin;
60
+ this.logger = logger;
61
+ }
62
+ /** Stage a new version. Returns the persisted record, including its generated id. */
63
+ async add(input, tarball, { signal }) {
64
+ if (validationUtils.validateName(input.tarballFilename) === false) throw errorUtils.getBadRequest(`invalid tarball name ${input.tarballFilename}`);
65
+ const record = {
66
+ id: randomUUID(),
67
+ packageName: input.packageName,
68
+ version: input.version,
69
+ tag: input.tag,
70
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
71
+ actor: input.actor,
72
+ actorType: "user",
73
+ access: input.access,
74
+ shasum: input.shasum,
75
+ tarballFilename: input.tarballFilename
76
+ };
77
+ debug("staging %o@%o as %o", record.packageName, record.version, record.id);
78
+ return this.enqueueIndexMutation(async () => {
79
+ const items = await this.readIndex();
80
+ if (items.find((item) => item.packageName === record.packageName && item.version === record.version)) throw errorUtils.getConflict(`${record.packageName}@${record.version} is already staged`);
81
+ const handler = this.itemStorage(record.id);
82
+ await this.writeTarball(handler, record.tarballFilename, tarball, { signal });
83
+ const document = {
84
+ record,
85
+ packument: input.packument
86
+ };
87
+ await handler.savePackage(record.id, document);
88
+ await this.writeIndex([record, ...items]);
89
+ this.logger.info({
90
+ packageName: record.packageName,
91
+ version: record.version,
92
+ stageId: record.id
93
+ }, "staged @{packageName}@@{version} as @{stageId}");
94
+ return record;
95
+ });
96
+ }
97
+ /** Every staged item, newest first, optionally narrowed to one package. */
98
+ async list(filter = {}) {
99
+ const items = await this.readIndex();
100
+ if (typeof filter.packageName === "string") return items.filter((item) => item.packageName === filter.packageName);
101
+ return items;
102
+ }
103
+ /** A single staged item, or `undefined` when the id is unknown. */
104
+ async get(id) {
105
+ return (await this.readIndex()).find((item) => item.id === id);
106
+ }
107
+ /** Whether a version of a package is already staged (used to answer 409). */
108
+ async findByVersion(packageName, version) {
109
+ return (await this.readIndex()).find((item) => item.packageName === packageName && item.version === version);
110
+ }
111
+ /** The publish payload kept for a staged item, needed to approve it. */
112
+ async getPackument(id) {
113
+ return (await this.readDocument(id))?.packument;
114
+ }
115
+ /** Read the staged tarball as a stream. */
116
+ async readTarball(id, { signal }) {
117
+ const record = await this.get(id);
118
+ if (!record) throw errorUtils.getNotFound("no such staged package version");
119
+ return this.itemStorage(id).readTarball(record.tarballFilename, { signal });
120
+ }
121
+ /**
122
+ * Drop a staged item and its tarball.
123
+ *
124
+ * @returns `false` when the id was unknown, `true` when something was removed.
125
+ */
126
+ async remove(id) {
127
+ const record = await this.get(id);
128
+ if (!record) return false;
129
+ const handler = this.itemStorage(id);
130
+ await this.updateIndex((items) => items.filter((item) => item.id !== id));
131
+ for (const file of [record.tarballFilename, STORAGE.PACKAGE_FILE_NAME]) try {
132
+ await handler.deletePackage(file);
133
+ } catch (err) {
134
+ debug("could not delete staged file %o: %s", file, err?.message);
135
+ }
136
+ try {
137
+ await handler.removePackage(id);
138
+ } catch (err) {
139
+ debug("could not delete staged folder %o: %s", id, err?.message);
140
+ }
141
+ debug("removed staged item %o", id);
142
+ return true;
143
+ }
144
+ itemStorage(id) {
145
+ return this.plugin.getPackageStorage(`${STAGE_NAMESPACE}/${id}`);
146
+ }
147
+ indexStorage() {
148
+ return this.plugin.getPackageStorage(STAGE_NAMESPACE);
149
+ }
150
+ async readDocument(id) {
151
+ try {
152
+ return await this.itemStorage(id).readPackage(id);
153
+ } catch (err) {
154
+ if (isNotFound(err)) return;
155
+ throw err;
156
+ }
157
+ }
158
+ async readIndex() {
159
+ try {
160
+ const index = await this.indexStorage().readPackage(STAGE_INDEX);
161
+ return Array.isArray(index?.items) ? index.items : [];
162
+ } catch (err) {
163
+ if (isNotFound(err)) {
164
+ debug("no stage index yet");
165
+ return [];
166
+ }
167
+ throw err;
168
+ }
169
+ }
170
+ /** Read-modify-write the index, serialized against other callers (see {@link indexQueue}). */
171
+ async updateIndex(mutate) {
172
+ return this.enqueueIndexMutation(async () => {
173
+ const items = mutate(await this.readIndex());
174
+ await this.writeIndex(items);
175
+ return items;
176
+ });
177
+ }
178
+ async enqueueIndexMutation(run) {
179
+ const queued = this.indexQueue.then(run, run);
180
+ this.indexQueue = queued.catch(() => void 0);
181
+ return queued;
182
+ }
183
+ async writeIndex(items) {
184
+ const index = { items };
185
+ await this.indexStorage().savePackage(STAGE_INDEX, index);
186
+ }
187
+ async writeTarball(handler, filename, tarball, { signal }) {
188
+ const writeStream = await handler.writeTarball(filename, { signal });
189
+ await pipeline(Readable.from(tarball), writeStream);
190
+ await this.waitForTarball(handler, filename);
191
+ }
192
+ async waitForTarball(handler, filename, attempts = 20) {
193
+ for (let attempt = 0; attempt < attempts; attempt++) {
194
+ if (await handler.hasTarball(filename)) return;
195
+ await new Promise((resolve) => setTimeout(resolve, 10));
196
+ }
197
+ throw errorUtils.getInternalError(`staged tarball ${filename} was never persisted`);
198
+ }
199
+ };
200
+ //#endregion
201
+ export { STAGE_NAMESPACE, StageStorage, toStagePackageVersion };
202
+
203
+ //# sourceMappingURL=stage-storage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-storage.mjs","names":[],"sources":["../src/stage-storage.ts"],"sourcesContent":["import buildDebug from 'debug';\nimport { randomUUID } from 'node:crypto';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\n\nimport type { pluginUtils } from '@verdaccio/core';\nimport { errorUtils, validationUtils } from '@verdaccio/core';\nimport type { Logger, Manifest } from '@verdaccio/types';\n\nimport { STORAGE } from './lib/storage-utils';\n\nconst debug = buildDebug('verdaccio:storage:stage');\n\n/**\n * Storage namespace holding every staged item.\n *\n * It is deliberately NOT registered through `storagePlugin.add()`: doing so\n * would list it in the plugin database and leak it into search results and the\n * web UI package list. The staged items are enumerated through {@link STAGE_INDEX}\n * instead.\n */\nexport const STAGE_NAMESPACE = '.stage';\n\n/**\n * Logical name used when reading/writing the index document. Storage handlers\n * are scoped to a folder and ignore this argument when resolving the path, but\n * it is what shows up in plugin logs, so keep it descriptive.\n */\nconst STAGE_INDEX = 'stage-index';\n\n/**\n * Metadata of a package version awaiting review.\n *\n * Mirrors the `StagePackageVersion` schema documented by npmjs, plus\n * {@link StageRecord.tarballFilename}, which is internal and must be stripped\n * before serving the record over the API (see {@link toStagePackageVersion}).\n */\nexport interface StageRecord {\n /** Unique identifier. Must be a UUID: the npm CLI validates the format client side. */\n id: string;\n packageName: string;\n version: string;\n /** dist-tag the version will be published under once approved. */\n tag: string;\n /** ISO timestamp of when the version was staged. */\n createdAt: string;\n /** Username of whoever staged the version. */\n actor: string;\n actorType: 'user';\n access: 'public' | 'private';\n /** sha1 of the tarball, as reported by `npm stage list`. */\n shasum: string;\n /** Internal: name of the tarball inside the staged item folder. */\n tarballFilename: string;\n}\n\n/** Shape served to clients: {@link StageRecord} without the internal fields. */\nexport type StagePackageVersion = Omit<StageRecord, 'tarballFilename'>;\n\n/**\n * The publish payload kept for a staged item, minus `_attachments` (the tarball\n * is stored separately as a real tarball, not as base64 inside this document).\n */\nexport type StagedPackument = Omit<Manifest, '_attachments'>;\n\n/** Persisted document of a single staged item. */\ninterface StageDocument {\n record: StageRecord;\n packument: StagedPackument;\n}\n\n/** Persisted index of every staged item, newest first. */\ninterface StageIndex {\n items: StageRecord[];\n}\n\nexport interface AddStageInput {\n packageName: string;\n version: string;\n tag: string;\n actor: string;\n access: 'public' | 'private';\n shasum: string;\n tarballFilename: string;\n packument: StagedPackument;\n}\n\nexport interface ListStageFilter {\n /** Restrict the result to a single package name. */\n packageName?: string;\n}\n\n/** Strip the internal fields before the record leaves the registry. */\nexport function toStagePackageVersion(record: StageRecord): StagePackageVersion {\n const { tarballFilename: _tarballFilename, ...rest } = record;\n return rest;\n}\n\nfunction isNotFound(err: any): boolean {\n return err?.code === 'ENOENT' || err?.code === 404 || err?.status === 404;\n}\n\n/**\n * Persistence for the staged publish workflow (`npm stage`).\n *\n * Everything goes through {@link pluginUtils.StorageHandler}, so it works with\n * any storage plugin (local-storage, memory, S3, GCS) without the plugin having\n * to know that staging exists. On disk it looks like:\n *\n * ```\n * <storage>/.stage/package.json index of every staged item\n * <storage>/.stage/<uuid>/package.json the staged item (record + packument)\n * <storage>/.stage/<uuid>/<name>-<v>.tgz the staged tarball\n * ```\n */\nexport class StageStorage {\n private readonly plugin: pluginUtils.Storage<any>;\n private readonly logger: Logger;\n /**\n * Serializes index mutations within this process.\n *\n * `StorageHandler.updatePackage()` takes a lock in local-storage but releases\n * it before we get to save, and other plugins do not lock at all, so a\n * read-modify-write on the index would race. This keeps a single Verdaccio\n * process consistent; several processes sharing one storage backend are NOT\n * supported.\n */\n private indexQueue: Promise<unknown> = Promise.resolve();\n\n public constructor(plugin: pluginUtils.Storage<any>, logger: Logger) {\n this.plugin = plugin;\n this.logger = logger;\n }\n\n /** Stage a new version. Returns the persisted record, including its generated id. */\n public async add(\n input: AddStageInput,\n tarball: Buffer,\n { signal }: { signal: AbortSignal }\n ): Promise<StageRecord> {\n // the filename reaches us from the `_attachments` key of the request, so it\n // is attacker controlled. The regular publish path asserts the same thing\n // before writing; without it a name like `..` escapes the item folder and\n // the write fails inside an unawaited stream handler, taking the process\n // down rather than the request.\n if (validationUtils.validateName(input.tarballFilename) === false) {\n throw errorUtils.getBadRequest(`invalid tarball name ${input.tarballFilename}`);\n }\n\n const record: StageRecord = {\n id: randomUUID(),\n packageName: input.packageName,\n version: input.version,\n tag: input.tag,\n createdAt: new Date().toISOString(),\n actor: input.actor,\n actorType: 'user',\n access: input.access,\n shasum: input.shasum,\n tarballFilename: input.tarballFilename,\n };\n debug('staging %o@%o as %o', record.packageName, record.version, record.id);\n\n return this.enqueueIndexMutation(async () => {\n const items = await this.readIndex();\n const alreadyStaged = items.find(\n (item) => item.packageName === record.packageName && item.version === record.version\n );\n if (alreadyStaged) {\n throw errorUtils.getConflict(`${record.packageName}@${record.version} is already staged`);\n }\n\n const handler = this.itemStorage(record.id);\n // the tarball goes first: if it fails there is no record pointing at a\n // tarball that never landed.\n await this.writeTarball(handler, record.tarballFilename, tarball, { signal });\n const document: StageDocument = { record, packument: input.packument };\n await handler.savePackage(record.id, document as unknown as Manifest);\n await this.writeIndex([record, ...items]);\n\n this.logger.info(\n { packageName: record.packageName, version: record.version, stageId: record.id },\n 'staged @{packageName}@@{version} as @{stageId}'\n );\n return record;\n });\n }\n\n /** Every staged item, newest first, optionally narrowed to one package. */\n public async list(filter: ListStageFilter = {}): Promise<StageRecord[]> {\n const items = await this.readIndex();\n if (typeof filter.packageName === 'string') {\n return items.filter((item) => item.packageName === filter.packageName);\n }\n return items;\n }\n\n /** A single staged item, or `undefined` when the id is unknown. */\n public async get(id: string): Promise<StageRecord | undefined> {\n const items = await this.readIndex();\n return items.find((item) => item.id === id);\n }\n\n /** Whether a version of a package is already staged (used to answer 409). */\n public async findByVersion(\n packageName: string,\n version: string\n ): Promise<StageRecord | undefined> {\n const items = await this.readIndex();\n return items.find((item) => item.packageName === packageName && item.version === version);\n }\n\n /** The publish payload kept for a staged item, needed to approve it. */\n public async getPackument(id: string): Promise<StagedPackument | undefined> {\n const document = await this.readDocument(id);\n return document?.packument;\n }\n\n /** Read the staged tarball as a stream. */\n public async readTarball(id: string, { signal }: { signal: AbortSignal }): Promise<Readable> {\n const record = await this.get(id);\n if (!record) {\n throw errorUtils.getNotFound('no such staged package version');\n }\n return this.itemStorage(id).readTarball(record.tarballFilename, { signal });\n }\n\n /**\n * Drop a staged item and its tarball.\n *\n * @returns `false` when the id was unknown, `true` when something was removed.\n */\n public async remove(id: string): Promise<boolean> {\n const record = await this.get(id);\n if (!record) {\n return false;\n }\n const handler = this.itemStorage(id);\n // the index entry goes first: a leftover folder is invisible, whereas a\n // leftover index entry would surface a staged item that cannot be read.\n await this.updateIndex((items) => items.filter((item) => item.id !== id));\n // the folder can only be dropped once it is empty, so both files go first\n for (const file of [record.tarballFilename, STORAGE.PACKAGE_FILE_NAME]) {\n try {\n await handler.deletePackage(file);\n } catch (err: any) {\n debug('could not delete staged file %o: %s', file, err?.message);\n }\n }\n try {\n await handler.removePackage(id);\n } catch (err: any) {\n debug('could not delete staged folder %o: %s', id, err?.message);\n }\n debug('removed staged item %o', id);\n return true;\n }\n\n private itemStorage(id: string): pluginUtils.StorageHandler {\n return this.plugin.getPackageStorage(`${STAGE_NAMESPACE}/${id}`);\n }\n\n private indexStorage(): pluginUtils.StorageHandler {\n return this.plugin.getPackageStorage(STAGE_NAMESPACE);\n }\n\n private async readDocument(id: string): Promise<StageDocument | undefined> {\n try {\n const document = (await this.itemStorage(id).readPackage(id)) as unknown as StageDocument;\n return document;\n } catch (err: any) {\n if (isNotFound(err)) {\n return undefined;\n }\n throw err;\n }\n }\n\n private async readIndex(): Promise<StageRecord[]> {\n try {\n const index = (await this.indexStorage().readPackage(STAGE_INDEX)) as unknown as StageIndex;\n return Array.isArray(index?.items) ? index.items : [];\n } catch (err: any) {\n if (isNotFound(err)) {\n debug('no stage index yet');\n return [];\n }\n throw err;\n }\n }\n\n /** Read-modify-write the index, serialized against other callers (see {@link indexQueue}). */\n private async updateIndex(\n mutate: (items: StageRecord[]) => StageRecord[]\n ): Promise<StageRecord[]> {\n return this.enqueueIndexMutation(async (): Promise<StageRecord[]> => {\n const items = mutate(await this.readIndex());\n await this.writeIndex(items);\n return items;\n });\n }\n\n private async enqueueIndexMutation<T>(run: () => Promise<T>): Promise<T> {\n const queued = this.indexQueue.then(run, run);\n // keep the chain alive even when a caller rejects\n this.indexQueue = queued.catch(() => undefined);\n return queued;\n }\n\n private async writeIndex(items: StageRecord[]): Promise<void> {\n const index: StageIndex = { items };\n await this.indexStorage().savePackage(STAGE_INDEX, index as unknown as Manifest);\n }\n\n private async writeTarball(\n handler: pluginUtils.StorageHandler,\n filename: string,\n tarball: Buffer,\n { signal }: { signal: AbortSignal }\n ): Promise<void> {\n const writeStream = await handler.writeTarball(filename, { signal });\n\n // `StorageHandler.writeTarball` only promises a Writable. The two bundled\n // plugins happen to emit 'open' before accepting writes, but nothing in the\n // interface says a plugin must, and waiting for it would hang staging\n // forever on one that does not. Piping straight away is correct either way:\n // a stream that is not ready yet buffers the writes.\n await pipeline(Readable.from(tarball), writeStream);\n\n // Both plugins rename the temporary file inside an async 'close' handler\n // that nobody awaits, so 'close' does not guarantee the tarball is in its\n // final place yet. Give the rename a bounded chance to land.\n await this.waitForTarball(handler, filename);\n }\n\n private async waitForTarball(\n handler: pluginUtils.StorageHandler,\n filename: string,\n attempts = 20\n ): Promise<void> {\n for (let attempt = 0; attempt < attempts; attempt++) {\n if (await handler.hasTarball(filename)) {\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, 10));\n }\n throw errorUtils.getInternalError(`staged tarball ${filename} was never persisted`);\n }\n}\n"],"mappings":"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"}