@solana-mobile/dapp-store-publishing-tools 0.16.0 → 1.0.0

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.
Files changed (53) hide show
  1. package/lib/CoreUtils.js +0 -3
  2. package/lib/index.js +1 -0
  3. package/lib/portal/attestation.js +189 -0
  4. package/lib/portal/compat.js +3 -0
  5. package/lib/portal/index.js +5 -0
  6. package/lib/portal/signer.js +432 -0
  7. package/lib/portal/types.js +1 -0
  8. package/lib/portal/workflow/contracts.js +1 -0
  9. package/lib/portal/workflow/execution.js +493 -0
  10. package/lib/portal/workflow/ingestion.js +265 -0
  11. package/lib/portal/workflow/lifecycle.js +616 -0
  12. package/lib/portal/workflow/logging.js +8 -0
  13. package/lib/portal/workflow/source/files.js +304 -0
  14. package/lib/portal/workflow/source/preparation.js +318 -0
  15. package/lib/portal/workflow/state/bundle.js +260 -0
  16. package/lib/portal/workflow/state/checkpoints.js +53 -0
  17. package/lib/portal/workflow/state/session.js +100 -0
  18. package/lib/portal/workflow.js +1 -0
  19. package/lib/publish/PublishCoreAttestation.js +18 -17
  20. package/lib/publish/PublishCoreRemove.js +7 -89
  21. package/lib/publish/PublishCoreSubmit.js +7 -117
  22. package/lib/publish/PublishCoreSupport.js +7 -86
  23. package/lib/publish/PublishCoreUpdate.js +7 -117
  24. package/lib/publish/index.js +1 -0
  25. package/lib/schemas/releaseJsonMetadata.json +1 -2
  26. package/package.json +2 -4
  27. package/src/CoreUtils.ts +0 -6
  28. package/src/index.ts +1 -0
  29. package/src/portal/attestation.ts +76 -0
  30. package/src/portal/compat.ts +5 -0
  31. package/src/portal/index.ts +5 -0
  32. package/src/portal/signer.ts +327 -0
  33. package/src/portal/types.ts +447 -0
  34. package/src/portal/workflow/contracts.ts +108 -0
  35. package/src/portal/workflow/execution.ts +412 -0
  36. package/src/portal/workflow/ingestion.ts +187 -0
  37. package/src/portal/workflow/lifecycle.ts +435 -0
  38. package/src/portal/workflow/logging.ts +17 -0
  39. package/src/portal/workflow/source/files.ts +49 -0
  40. package/src/portal/workflow/source/preparation.ts +189 -0
  41. package/src/portal/workflow/state/bundle.ts +193 -0
  42. package/src/portal/workflow/state/checkpoints.ts +70 -0
  43. package/src/portal/workflow/state/session.ts +87 -0
  44. package/src/portal/workflow.ts +9 -0
  45. package/src/publish/PublishCoreAttestation.ts +21 -26
  46. package/src/publish/PublishCoreRemove.ts +13 -109
  47. package/src/publish/PublishCoreSubmit.ts +18 -150
  48. package/src/publish/PublishCoreSupport.ts +13 -102
  49. package/src/publish/PublishCoreUpdate.ts +17 -155
  50. package/src/publish/index.ts +2 -1
  51. package/src/schemas/releaseJsonMetadata.json +1 -2
  52. package/lib/publish/dapp_publisher_portal.js +0 -206
  53. package/src/publish/dapp_publisher_portal.ts +0 -81
@@ -0,0 +1,412 @@
1
+ import {
2
+ createAttestationPayloadFromClient,
3
+ type PublicationAttestationResult,
4
+ } from "../attestation.js";
5
+ import {
6
+ signSerializedTransaction,
7
+ type PublicationTransactionValidation,
8
+ } from "../signer.js";
9
+ import type {
10
+ PublicationAttestationClient,
11
+ PublicationBundle,
12
+ PublicationSession,
13
+ PublicationSigner,
14
+ PublicationWorkflowLogger,
15
+ PublicationWorkflowResult,
16
+ } from "../types.js";
17
+ import type { PublicationWorkflowClient } from "./contracts.js";
18
+ import { logWorkflowInfo } from "./logging.js";
19
+ import {
20
+ checkpointAtLeast,
21
+ publicationStageToCheckpoint,
22
+ } from "./state/checkpoints.js";
23
+ import {
24
+ normalizePublicationBundle,
25
+ resolvePublicationFeePayer,
26
+ resolvePublicationSignerAddress,
27
+ resolveReleaseDisplayName,
28
+ resolveReleaseMetadataUri,
29
+ validatePublicationBundle,
30
+ } from "./state/bundle.js";
31
+ import {
32
+ normalizePublicationSession,
33
+ resolveReleaseMintAddress,
34
+ } from "./state/session.js";
35
+
36
+ type PublicationExecutionContext = {
37
+ client: PublicationWorkflowClient;
38
+ bundle: PublicationBundle;
39
+ signer: PublicationSigner;
40
+ attestationClient: PublicationAttestationClient;
41
+ publicationSession: PublicationSession;
42
+ publicationCheckpoint: NonNullable<PublicationSession["checkpoint"]>;
43
+ logger?: PublicationWorkflowLogger;
44
+ releaseMetadataUri: string;
45
+ publisherAddress: string;
46
+ payerAddress: string;
47
+ };
48
+
49
+ type PublicationExecutionState = {
50
+ releaseTransactionSignature?: string;
51
+ collectionTransactionSignature?: string;
52
+ attestationResult?: PublicationAttestationResult;
53
+ hubspotTicketId?: string;
54
+ releaseMintAddress?: string;
55
+ };
56
+
57
+ async function signPreparedTransaction(
58
+ signer: PublicationSigner,
59
+ serializedTransaction: string,
60
+ validation: PublicationTransactionValidation
61
+ ): Promise<string> {
62
+ return signSerializedTransaction(signer, serializedTransaction, validation);
63
+ }
64
+
65
+ async function submitReleaseMintIfNeeded(
66
+ context: PublicationExecutionContext,
67
+ state: PublicationExecutionState
68
+ ) {
69
+ if (checkpointAtLeast(context.publicationCheckpoint, "mint-submitted")) {
70
+ return;
71
+ }
72
+
73
+ logWorkflowInfo(context.logger, "Preparing release NFT transaction", {
74
+ step: "mint.prepare",
75
+ status: "running",
76
+ releaseId: context.bundle.releaseId,
77
+ publicationSessionId: context.publicationSession.id,
78
+ });
79
+
80
+ const preparedReleaseTransaction =
81
+ await context.client.prepareReleaseNftTransaction({
82
+ releaseId: context.bundle.releaseId,
83
+ releaseName: resolveReleaseDisplayName(context.bundle),
84
+ releaseMetadataUri: context.releaseMetadataUri,
85
+ appMintAddress: context.bundle.signerAuthority.appMintAddress,
86
+ publisherAddress: context.publisherAddress,
87
+ payerAddress: context.payerAddress,
88
+ });
89
+
90
+ state.releaseMintAddress = preparedReleaseTransaction.mintAddress;
91
+ state.releaseTransactionSignature = await signPreparedTransaction(
92
+ context.signer,
93
+ preparedReleaseTransaction.transaction,
94
+ {
95
+ kind: "release-mint",
96
+ expectedBlockhash: preparedReleaseTransaction.blockhash,
97
+ expectedFeePayerAddress: context.payerAddress,
98
+ expectedSignerAddress: context.publisherAddress,
99
+ expectedMintAddress: preparedReleaseTransaction.mintAddress,
100
+ expectedAppMintAddress: context.bundle.signerAuthority.appMintAddress,
101
+ }
102
+ );
103
+
104
+ const signedTransactionResult = await context.client.submitSignedTransaction({
105
+ signedTransaction: state.releaseTransactionSignature,
106
+ publicationSessionId: context.publicationSession.id,
107
+ });
108
+
109
+ state.releaseTransactionSignature =
110
+ signedTransactionResult.transactionSignature;
111
+
112
+ logWorkflowInfo(context.logger, "Release NFT transaction submitted", {
113
+ step: "mint.submit",
114
+ status: "complete",
115
+ releaseId: context.bundle.releaseId,
116
+ publicationSessionId: context.publicationSession.id,
117
+ transactionSignature: state.releaseTransactionSignature,
118
+ });
119
+ }
120
+
121
+ async function saveReleaseMintIfNeeded(
122
+ context: PublicationExecutionContext,
123
+ state: PublicationExecutionState
124
+ ) {
125
+ if (checkpointAtLeast(context.publicationCheckpoint, "mint-saved")) {
126
+ return;
127
+ }
128
+
129
+ const mintAddress =
130
+ context.publicationSession.releaseMintAddress ??
131
+ context.publicationSession.expectedMintAddress ??
132
+ context.bundle.release.releaseMintAddress ??
133
+ state.releaseMintAddress;
134
+
135
+ if (!mintAddress) {
136
+ throw new Error(
137
+ "Publication bundle did not include a release mint address"
138
+ );
139
+ }
140
+
141
+ if (!state.releaseTransactionSignature) {
142
+ throw new Error("Release transaction signature is missing");
143
+ }
144
+
145
+ logWorkflowInfo(context.logger, "Saving release NFT data", {
146
+ step: "mint.save",
147
+ status: "running",
148
+ releaseId: context.bundle.releaseId,
149
+ publicationSessionId: context.publicationSession.id,
150
+ mintAddress,
151
+ });
152
+
153
+ await context.client.saveReleaseNftData({
154
+ releaseId: context.bundle.releaseId,
155
+ mintAddress,
156
+ transactionSignature: state.releaseTransactionSignature,
157
+ metadataUri: context.releaseMetadataUri,
158
+ ownerAddress: context.publisherAddress,
159
+ releaseName: resolveReleaseDisplayName(context.bundle),
160
+ releaseVersion: context.bundle.release.versionName,
161
+ androidPackage: context.bundle.release.androidPackage,
162
+ appMintAddress: context.bundle.signerAuthority.appMintAddress,
163
+ });
164
+
165
+ state.releaseMintAddress = mintAddress;
166
+
167
+ logWorkflowInfo(context.logger, "Release NFT data saved", {
168
+ step: "mint.save",
169
+ status: "complete",
170
+ releaseId: context.bundle.releaseId,
171
+ publicationSessionId: context.publicationSession.id,
172
+ mintAddress,
173
+ });
174
+ }
175
+
176
+ async function verifyReleaseCollectionIfNeeded(
177
+ context: PublicationExecutionContext,
178
+ state: PublicationExecutionState
179
+ ) {
180
+ if (
181
+ checkpointAtLeast(context.publicationCheckpoint, "verification-submitted")
182
+ ) {
183
+ return;
184
+ }
185
+
186
+ if (!state.releaseMintAddress) {
187
+ throw new Error(
188
+ "Publication bundle did not include a release mint address for collection verification"
189
+ );
190
+ }
191
+
192
+ logWorkflowInfo(
193
+ context.logger,
194
+ "Preparing collection verification transaction",
195
+ {
196
+ step: "verify.prepare",
197
+ status: "running",
198
+ releaseId: context.bundle.releaseId,
199
+ publicationSessionId: context.publicationSession.id,
200
+ mintAddress: state.releaseMintAddress,
201
+ }
202
+ );
203
+
204
+ const preparedVerifyTransaction =
205
+ await context.client.prepareVerifyCollectionTransaction({
206
+ dappId: context.bundle.signerAuthority.dappId ?? context.bundle.dapp.id,
207
+ nftMintAddress: state.releaseMintAddress,
208
+ collectionMintAddress: context.bundle.signerAuthority.appMintAddress,
209
+ collectionAuthority: context.bundle.signerAuthority.collectionAuthority,
210
+ payerAddress: context.payerAddress,
211
+ });
212
+
213
+ state.collectionTransactionSignature = await signPreparedTransaction(
214
+ context.signer,
215
+ preparedVerifyTransaction.transaction,
216
+ {
217
+ kind: "verify-collection",
218
+ expectedBlockhash: preparedVerifyTransaction.blockhash,
219
+ expectedFeePayerAddress: context.payerAddress,
220
+ expectedSignerAddress: context.publisherAddress,
221
+ expectedNftMintAddress: state.releaseMintAddress,
222
+ expectedCollectionMintAddress:
223
+ context.bundle.signerAuthority.appMintAddress,
224
+ expectedCollectionAuthority:
225
+ context.bundle.signerAuthority.collectionAuthority,
226
+ }
227
+ );
228
+
229
+ logWorkflowInfo(
230
+ context.logger,
231
+ "Submitting collection verification transaction",
232
+ {
233
+ step: "verify.submit",
234
+ status: "running",
235
+ releaseId: context.bundle.releaseId,
236
+ publicationSessionId: context.publicationSession.id,
237
+ mintAddress: state.releaseMintAddress,
238
+ }
239
+ );
240
+
241
+ const signedVerifyTransactionResult =
242
+ await context.client.submitSignedTransaction({
243
+ signedTransaction: state.collectionTransactionSignature,
244
+ publicationSessionId: context.publicationSession.id,
245
+ });
246
+
247
+ state.collectionTransactionSignature =
248
+ signedVerifyTransactionResult.transactionSignature;
249
+
250
+ await context.client.markReleaseCollectionAsVerified({
251
+ releaseId: context.bundle.releaseId,
252
+ });
253
+
254
+ logWorkflowInfo(context.logger, "Release collection verified", {
255
+ step: "verify.submit",
256
+ status: "complete",
257
+ releaseId: context.bundle.releaseId,
258
+ publicationSessionId: context.publicationSession.id,
259
+ transactionSignature: state.collectionTransactionSignature,
260
+ });
261
+ }
262
+
263
+ async function attestAndSubmitIfNeeded(
264
+ context: PublicationExecutionContext,
265
+ state: PublicationExecutionState
266
+ ) {
267
+ if (checkpointAtLeast(context.publicationCheckpoint, "submitted")) {
268
+ return;
269
+ }
270
+
271
+ logWorkflowInfo(context.logger, "Creating attestation payload", {
272
+ step: "attestation.create",
273
+ status: "running",
274
+ releaseId: context.bundle.releaseId,
275
+ publicationSessionId: context.publicationSession.id,
276
+ });
277
+
278
+ state.attestationResult = await createAttestationPayloadFromClient(
279
+ context.attestationClient,
280
+ context.signer
281
+ );
282
+
283
+ logWorkflowInfo(context.logger, "Attestation payload created", {
284
+ step: "attestation.create",
285
+ status: "complete",
286
+ releaseId: context.bundle.releaseId,
287
+ publicationSessionId: context.publicationSession.id,
288
+ requestUniqueId: state.attestationResult.requestUniqueId,
289
+ });
290
+
291
+ logWorkflowInfo(context.logger, "Submitting release to store", {
292
+ step: "submit.store",
293
+ status: "running",
294
+ releaseId: context.bundle.releaseId,
295
+ publicationSessionId: context.publicationSession.id,
296
+ });
297
+
298
+ const submissionResult = await context.client.submitToStore({
299
+ releaseId: context.bundle.releaseId,
300
+ whatsNew: context.bundle.release.newInVersion,
301
+ attestation: {
302
+ payload: state.attestationResult.payload,
303
+ requestUniqueId: state.attestationResult.requestUniqueId,
304
+ },
305
+ });
306
+
307
+ state.hubspotTicketId =
308
+ submissionResult.hubspotTicketId ?? state.hubspotTicketId;
309
+
310
+ logWorkflowInfo(context.logger, "Release submitted to store", {
311
+ step: "submit.store",
312
+ status: "complete",
313
+ releaseId: context.bundle.releaseId,
314
+ publicationSessionId: context.publicationSession.id,
315
+ hubspotTicketId: state.hubspotTicketId,
316
+ });
317
+ }
318
+
319
+ export async function runPublicationWorkflow(
320
+ client: PublicationWorkflowClient,
321
+ bundle: PublicationBundle,
322
+ signer: PublicationSigner,
323
+ attestationClient: PublicationAttestationClient,
324
+ session: PublicationSession,
325
+ logger?: PublicationWorkflowLogger
326
+ ): Promise<PublicationWorkflowResult> {
327
+ const normalizedBundle = normalizePublicationBundle(bundle);
328
+ const normalizedSession = normalizePublicationSession(session);
329
+ validatePublicationBundle(normalizedBundle);
330
+
331
+ const requiredSignerAddress =
332
+ resolvePublicationSignerAddress(normalizedBundle);
333
+ if (signer.publicKey !== requiredSignerAddress) {
334
+ throw new Error(
335
+ `Publication signer mismatch. Expected ${requiredSignerAddress}; received ${signer.publicKey}.`
336
+ );
337
+ }
338
+
339
+ if (normalizedSession.stage === "Failed") {
340
+ throw new Error(
341
+ normalizedSession.lastError ||
342
+ normalizedSession.error ||
343
+ "Publication session failed"
344
+ );
345
+ }
346
+
347
+ const publicationSession = normalizedSession;
348
+ const publicationCheckpoint =
349
+ publicationSession.checkpoint ??
350
+ publicationStageToCheckpoint(publicationSession.stage);
351
+ const state: PublicationExecutionState = {
352
+ releaseTransactionSignature:
353
+ publicationSession.mintTransactionSignature ?? undefined,
354
+ collectionTransactionSignature:
355
+ publicationSession.verifyTransactionSignature ??
356
+ publicationSession.verificationTransactionSignature ??
357
+ undefined,
358
+ hubspotTicketId: publicationSession.hubspotTicketId ?? undefined,
359
+ releaseMintAddress: resolveReleaseMintAddress(
360
+ normalizedBundle,
361
+ publicationSession
362
+ ),
363
+ };
364
+
365
+ const context: PublicationExecutionContext = {
366
+ client,
367
+ bundle: normalizedBundle,
368
+ signer,
369
+ attestationClient,
370
+ publicationSession,
371
+ publicationCheckpoint,
372
+ logger,
373
+ releaseMetadataUri: resolveReleaseMetadataUri(
374
+ normalizedBundle,
375
+ publicationSession
376
+ ),
377
+ publisherAddress: resolvePublicationSignerAddress(normalizedBundle),
378
+ payerAddress: resolvePublicationFeePayer(normalizedBundle, signer),
379
+ };
380
+
381
+ await submitReleaseMintIfNeeded(context, state);
382
+ await saveReleaseMintIfNeeded(context, state);
383
+ await verifyReleaseCollectionIfNeeded(context, state);
384
+ await attestAndSubmitIfNeeded(context, state);
385
+
386
+ if (!state.releaseMintAddress) {
387
+ throw new Error(
388
+ "Publication session did not resolve a release mint address"
389
+ );
390
+ }
391
+
392
+ return {
393
+ ingestionSessionId:
394
+ publicationSession.ingestionSessionId ??
395
+ normalizedBundle.ingestionSessionId ??
396
+ "",
397
+ publicationSessionId:
398
+ publicationSession.id || normalizedBundle.publicationSessionId || "",
399
+ releaseId: normalizedBundle.releaseId || publicationSession.releaseId || "",
400
+ releaseMintAddress: state.releaseMintAddress,
401
+ collectionMintAddress: normalizedBundle.signerAuthority.appMintAddress,
402
+ releaseTransactionSignature: state.releaseTransactionSignature,
403
+ collectionTransactionSignature: state.collectionTransactionSignature,
404
+ attestationRequestUniqueId:
405
+ state.attestationResult?.requestUniqueId ??
406
+ publicationSession.attestationRequestUniqueId ??
407
+ undefined,
408
+ hubspotTicketId: state.hubspotTicketId,
409
+ publicationBundle: normalizedBundle,
410
+ publicationSession,
411
+ };
412
+ }
@@ -0,0 +1,187 @@
1
+ import type {
2
+ PublicationIngestionSession,
3
+ PublicationWorkflowLogger,
4
+ } from "../types.js";
5
+ import type {
6
+ PublicationWorkflowClient,
7
+ PublicationWorkflowPollOptions,
8
+ } from "./contracts.js";
9
+ import { logWorkflowInfo } from "./logging.js";
10
+
11
+ function isReadyIngestionSession(
12
+ session: PublicationIngestionSession
13
+ ): boolean {
14
+ return session.status === "Ready" || session.status === "ready";
15
+ }
16
+
17
+ function buildIngestionStatusProgress(
18
+ session: PublicationIngestionSession
19
+ ): number {
20
+ if (
21
+ typeof session.processingProgress === "number" &&
22
+ Number.isFinite(session.processingProgress)
23
+ ) {
24
+ return Math.max(0, Math.min(1, session.processingProgress / 100));
25
+ }
26
+
27
+ switch (session.status) {
28
+ case "created":
29
+ return 0.15;
30
+ case "queued":
31
+ return 0.3;
32
+ case "processing":
33
+ return 0.7;
34
+ case "Ready":
35
+ case "ready":
36
+ case "Failed":
37
+ case "failed":
38
+ return 1;
39
+ default:
40
+ return 0.15;
41
+ }
42
+ }
43
+
44
+ function buildIngestionStatusMessage(
45
+ session: PublicationIngestionSession
46
+ ): string | null {
47
+ if (
48
+ typeof session.processingDetail === "string" &&
49
+ session.processingDetail.trim().length > 0
50
+ ) {
51
+ return session.processingDetail.trim();
52
+ }
53
+
54
+ if (
55
+ typeof session.processingStage === "string" &&
56
+ session.processingStage.trim().length > 0
57
+ ) {
58
+ return session.processingStage.trim();
59
+ }
60
+
61
+ switch (session.status) {
62
+ case "created":
63
+ return "Portal ingestion request created";
64
+ case "queued":
65
+ return "Portal ingestion queued";
66
+ case "processing":
67
+ return "Portal ingestion is processing the APK";
68
+ case "Ready":
69
+ case "ready":
70
+ return "Portal ingestion is ready";
71
+ default:
72
+ return null;
73
+ }
74
+ }
75
+
76
+ export async function waitForIngestionSessionReady(
77
+ client: PublicationWorkflowClient,
78
+ ingestionSessionId: string,
79
+ options: PublicationWorkflowPollOptions,
80
+ logger?: PublicationWorkflowLogger
81
+ ): Promise<PublicationIngestionSession> {
82
+ logWorkflowInfo(logger, "Waiting for portal ingestion to finish", {
83
+ step: "ingestion.wait",
84
+ status: "running",
85
+ ingestionSessionId,
86
+ stepProgress: 0,
87
+ });
88
+
89
+ let previousSnapshot:
90
+ | {
91
+ status: PublicationIngestionSession["status"];
92
+ progress: number | null;
93
+ stage: string | null;
94
+ detail: string | null;
95
+ }
96
+ | undefined;
97
+
98
+ for (let attempt = 1; attempt <= options.maxPollAttempts; attempt += 1) {
99
+ const session = await client.getIngestionSession({
100
+ sessionId: ingestionSessionId,
101
+ ingestionSessionId,
102
+ });
103
+ if (session.status === "Failed" || session.status === "failed") {
104
+ throw new Error(
105
+ session.error ||
106
+ session.processingError ||
107
+ "Publication ingestion failed before the bundle was ready"
108
+ );
109
+ }
110
+
111
+ const nextSnapshot = {
112
+ status: session.status,
113
+ progress:
114
+ typeof session.processingProgress === "number"
115
+ ? session.processingProgress
116
+ : null,
117
+ stage:
118
+ typeof session.processingStage === "string"
119
+ ? session.processingStage
120
+ : null,
121
+ detail:
122
+ typeof session.processingDetail === "string"
123
+ ? session.processingDetail
124
+ : null,
125
+ };
126
+
127
+ if (
128
+ !previousSnapshot ||
129
+ previousSnapshot.status !== nextSnapshot.status ||
130
+ previousSnapshot.progress !== nextSnapshot.progress ||
131
+ previousSnapshot.stage !== nextSnapshot.stage ||
132
+ previousSnapshot.detail !== nextSnapshot.detail
133
+ ) {
134
+ previousSnapshot = nextSnapshot;
135
+ const statusMessage = buildIngestionStatusMessage(session);
136
+
137
+ if (statusMessage) {
138
+ logWorkflowInfo(logger, statusMessage, {
139
+ step: "ingestion.wait",
140
+ status: "running",
141
+ ingestionSessionId,
142
+ releaseId: session.releaseId ?? undefined,
143
+ publicationSessionId: session.publicationSessionId ?? undefined,
144
+ androidPackage: session.androidPackage ?? undefined,
145
+ versionName: session.versionName ?? undefined,
146
+ ingestionStatus:
147
+ session.processingDetail ??
148
+ session.processingStage ??
149
+ session.status,
150
+ ingestionProgress: nextSnapshot.progress ?? undefined,
151
+ ingestionStage: nextSnapshot.stage ?? undefined,
152
+ ingestionDetail: nextSnapshot.detail ?? undefined,
153
+ stepProgress: buildIngestionStatusProgress(session),
154
+ });
155
+ }
156
+ }
157
+
158
+ if (isReadyIngestionSession(session)) {
159
+ logWorkflowInfo(logger, "Portal ingestion is ready", {
160
+ step: "ingestion.wait",
161
+ status: "complete",
162
+ ingestionSessionId,
163
+ releaseId: session.releaseId ?? undefined,
164
+ publicationSessionId: session.publicationSessionId ?? undefined,
165
+ androidPackage: session.androidPackage ?? undefined,
166
+ versionName: session.versionName ?? undefined,
167
+ ingestionStatus:
168
+ session.processingDetail ?? session.processingStage ?? session.status,
169
+ ingestionProgress:
170
+ typeof session.processingProgress === "number"
171
+ ? session.processingProgress
172
+ : 100,
173
+ ingestionStage: session.processingStage ?? "Ready",
174
+ ingestionDetail:
175
+ session.processingDetail ?? "Publication ingestion is ready",
176
+ stepProgress: 1,
177
+ });
178
+ return session;
179
+ }
180
+
181
+ await new Promise((resolve) => setTimeout(resolve, options.pollIntervalMs));
182
+ }
183
+
184
+ throw new Error(
185
+ `Timed out waiting for ingestion session ${ingestionSessionId} to become ready`
186
+ );
187
+ }