@absolutejs/deploy 0.21.1 → 0.22.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.
package/README.md CHANGED
@@ -12,6 +12,42 @@ atomically, `rollback(releaseId)` re-points the symlink and restarts.
12
12
  Zero `ssh2` / `node-ssh` dependency — `sshTarget` shells out to the system
13
13
  `ssh` / `rsync` binaries that already ship on Mac, Linux, and WSL.
14
14
 
15
+ ## Native application releases (0.22.0)
16
+
17
+ `@absolutejs/deploy/native-release` publishes the immutable release directory
18
+ created by `absolute mobile build android` through any `@absolutejs/blob`
19
+ adapter. It verifies the local AAB's declared size and SHA-256 digest again,
20
+ requires a signed build by default, and writes the binary only once under its
21
+ content-derived release identity.
22
+
23
+ ```ts
24
+ import { createNativeReleaseRegistry } from '@absolutejs/deploy/native-release';
25
+ import { s3BlobStore } from '@absolutejs/blob/s3';
26
+
27
+ const store = s3BlobStore({ client, bucket: 'absolute-releases' });
28
+ const releases = createNativeReleaseRegistry({ store });
29
+
30
+ const published = await releases.publish({
31
+ releaseRoot:
32
+ '.absolutejs/mobile/releases/android/amobile_android_<sha256>',
33
+ channel: 'internal'
34
+ });
35
+
36
+ await releases.promote({
37
+ appId: published.record.metadata.appId,
38
+ platform: 'android',
39
+ releaseId: published.record.metadata.releaseId,
40
+ channel: 'production'
41
+ });
42
+ ```
43
+
44
+ Channels are small mutable pointers; release records and AAB bytes are
45
+ immutable. Promoting an older retained release is therefore a rollback without
46
+ rebuilding or copying the binary. Passing `allowUnsigned: true` is required on
47
+ both publication and promotion for intentionally non-publishable local-testing
48
+ artifacts. The registry uses the structural BlobStore shape, so Deploy does not
49
+ take a runtime dependency on `@absolutejs/blob` or a cloud SDK.
50
+
15
51
  ## Infrastructure providers (0.14.0)
16
52
 
17
53
  Control planes use the normalized `InfrastructureProvider` contract from
package/dist/index.d.ts CHANGED
@@ -18,5 +18,7 @@ export type { DeployContext, DeployHooks, DeployOptions, DeployResult, DeploySte
18
18
  export { createDeployer, defaultBunPipeline } from './deployer';
19
19
  export type { CreatedReleaseArtifact, ReleaseArtifactMetadata, } from './releaseArtifact';
20
20
  export { createReleaseArtifact, extractReleaseArtifact, receiveReleaseArtifact, ReleaseArtifactError, } from './releaseArtifact';
21
+ export type { AndroidNativeReleaseMetadata, NativeReleaseBlobObject, NativeReleaseBlobStore, NativeReleaseChannel, NativeReleaseMetadata, NativeReleasePublication, NativeReleaseRecord, NativeReleaseRegistry, NativeReleaseRegistryOptions, } from './nativeRelease';
22
+ export { createNativeReleaseRegistry, NATIVE_RELEASE_REGISTRY_FORMAT, NativeReleaseRegistryError, } from './nativeRelease';
21
23
  export type { EdgeIngress, EdgeIngressBackend, EdgeIngressCapabilities, EdgeIngressProtocol, EdgeIngressProvider, EdgeIngressSpec, EdgeIngressState, } from './edgeIngress';
22
24
  export { EdgeIngressValidationError, normalizedEdgeIngressBackends, validateEdgeIngressSpec, } from './edgeIngress';
package/dist/index.js CHANGED
@@ -129,7 +129,6 @@ var receiveReleaseArtifact = async (options) => {
129
129
  await rm(options.destination, { force: true });
130
130
  throw error;
131
131
  } finally {
132
- reader.releaseLock();
133
132
  await writer.end();
134
133
  }
135
134
  if (bytes !== options.expectedBytes || hasher.digest("hex") !== options.expectedSha256) {
@@ -165,6 +164,308 @@ var extractReleaseArtifact = async (options) => {
165
164
  return { extracted: true };
166
165
  };
167
166
 
167
+ // src/nativeRelease.ts
168
+ import { createHash } from "crypto";
169
+ import { readFile, stat as stat2 } from "fs/promises";
170
+ import path2 from "path";
171
+ var DEFAULT_PREFIX = "absolutejs/native-releases";
172
+ var DEFAULT_MAX_ARTIFACT_BYTES = 2147483648;
173
+ var NATIVE_RELEASE_REGISTRY_FORMAT = 1;
174
+ var SHA256_PATTERN = /^[a-f0-9]{64}$/;
175
+ var APP_ID_PATTERN = /^[A-Za-z][\w]*(?:\.[A-Za-z][\w]*)+$/;
176
+ var CHANNEL_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
177
+
178
+ class NativeReleaseRegistryError extends Error {
179
+ }
180
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
181
+ var requireString = (value, field) => {
182
+ if (typeof value !== "string" || value.length === 0)
183
+ throw new NativeReleaseRegistryError(`Native release ${field} is invalid`);
184
+ return value;
185
+ };
186
+ var parseMetadata = (value) => {
187
+ if (!isRecord(value))
188
+ throw new NativeReleaseRegistryError("Native release metadata is invalid");
189
+ const appId = requireString(value.appId, "appId");
190
+ const sha256 = requireString(value.sha256, "sha256");
191
+ const releaseId = requireString(value.releaseId, "releaseId");
192
+ if (!APP_ID_PATTERN.test(appId))
193
+ throw new NativeReleaseRegistryError("Native release appId is invalid");
194
+ if (!SHA256_PATTERN.test(sha256))
195
+ throw new NativeReleaseRegistryError("Native release sha256 is invalid");
196
+ if (releaseId !== `amobile_android_${sha256}`)
197
+ throw new NativeReleaseRegistryError("Native release id does not match its artifact digest");
198
+ if (value.artifact !== "app-release.aab" || value.engine !== "capacitor" || value.format !== 1 || value.platform !== "android" || value.type !== "aab" || typeof value.signed !== "boolean" || !Number.isSafeInteger(value.bytes) || Number(value.bytes) < 1)
199
+ throw new NativeReleaseRegistryError("Native release metadata is invalid");
200
+ return {
201
+ appBuild: requireString(value.appBuild, "appBuild"),
202
+ appId,
203
+ artifact: "app-release.aab",
204
+ bytes: Number(value.bytes),
205
+ engine: "capacitor",
206
+ format: 1,
207
+ platform: "android",
208
+ releaseId,
209
+ runtime: requireString(value.runtime, "runtime"),
210
+ sha256,
211
+ signed: value.signed,
212
+ type: "aab"
213
+ };
214
+ };
215
+ var normalizedPrefix = (value) => {
216
+ const prefix = value.replace(/^\/+|\/+$/g, "");
217
+ if (prefix.length === 0 || prefix.split("/").some((segment) => segment === "." || segment === ".."))
218
+ throw new NativeReleaseRegistryError("Native release prefix is invalid");
219
+ return prefix;
220
+ };
221
+ var requireChannel = (value) => {
222
+ if (!CHANNEL_PATTERN.test(value))
223
+ throw new NativeReleaseRegistryError("Native release channel is invalid");
224
+ return value;
225
+ };
226
+ var appIdentity = (appId) => createHash("sha256").update(appId).digest("hex");
227
+ var sha256Bytes = (bytes) => createHash("sha256").update(bytes).digest("hex");
228
+ var isIsoTimestamp = (value) => {
229
+ if (typeof value !== "string" || Number.isNaN(Date.parse(value)))
230
+ return false;
231
+ return new Date(value).toISOString() === value;
232
+ };
233
+ var sha256File2 = async (file) => {
234
+ const hasher = new Bun.CryptoHasher("sha256");
235
+ for await (const chunk of file.stream())
236
+ hasher.update(chunk);
237
+ return hasher.digest("hex");
238
+ };
239
+ var encodedJson = (value) => new TextEncoder().encode(`${JSON.stringify(value, null, 2)}
240
+ `);
241
+ var decodedJson = (bytes) => JSON.parse(new TextDecoder().decode(bytes));
242
+ var sameMetadata = (left, right) => JSON.stringify(left) === JSON.stringify(right);
243
+ var parseRecord = (value) => {
244
+ if (!isRecord(value) || value.format !== NATIVE_RELEASE_REGISTRY_FORMAT || typeof value.artifactKey !== "string")
245
+ throw new NativeReleaseRegistryError("Native release record is invalid");
246
+ const metadata = parseMetadata(value.metadata);
247
+ return {
248
+ artifactKey: value.artifactKey,
249
+ format: NATIVE_RELEASE_REGISTRY_FORMAT,
250
+ metadata
251
+ };
252
+ };
253
+ var parseChannel = (value) => {
254
+ if (!isRecord(value) || value.format !== NATIVE_RELEASE_REGISTRY_FORMAT || value.platform !== "android" || !isIsoTimestamp(value.promotedAt))
255
+ throw new NativeReleaseRegistryError("Native release channel is invalid");
256
+ const appId = requireString(value.appId, "channel appId");
257
+ const channel = requireChannel(requireString(value.channel, "channel"));
258
+ const sha256 = requireString(value.sha256, "channel sha256");
259
+ const releaseId = requireString(value.releaseId, "channel releaseId");
260
+ if (!APP_ID_PATTERN.test(appId) || !SHA256_PATTERN.test(sha256))
261
+ throw new NativeReleaseRegistryError("Native release channel is invalid");
262
+ if (releaseId !== `amobile_android_${sha256}`)
263
+ throw new NativeReleaseRegistryError("Native release channel identity does not match");
264
+ return {
265
+ appId,
266
+ channel,
267
+ format: NATIVE_RELEASE_REGISTRY_FORMAT,
268
+ platform: "android",
269
+ promotedAt: value.promotedAt,
270
+ releaseId,
271
+ sha256
272
+ };
273
+ };
274
+ var createNativeReleaseRegistry = (options) => {
275
+ const prefix = normalizedPrefix(options.prefix ?? DEFAULT_PREFIX);
276
+ const maxArtifactBytes = options.maxArtifactBytes ?? DEFAULT_MAX_ARTIFACT_BYTES;
277
+ if (!Number.isSafeInteger(maxArtifactBytes) || maxArtifactBytes < 1)
278
+ throw new NativeReleaseRegistryError("Native release maxArtifactBytes is invalid");
279
+ const clock = options.clock ?? (() => new Date);
280
+ const appRoot = (appId, platform) => `${prefix}/${appIdentity(appId)}/${platform}`;
281
+ const releaseRoot = (metadata) => `${appRoot(metadata.appId, metadata.platform)}/releases/${metadata.releaseId}`;
282
+ const recordKey = (metadata) => `${releaseRoot(metadata)}/release.json`;
283
+ const artifactKey = (metadata) => `${releaseRoot(metadata)}/${metadata.artifact}`;
284
+ const channelKey = (appId, platform, channel) => `${appRoot(appId, platform)}/channels/${requireChannel(channel)}.json`;
285
+ const requireStoredArtifact = async (key, metadata) => {
286
+ const stored = await options.store.head(key);
287
+ if (!stored || stored.size !== metadata.bytes || stored.metadata?.sha256 !== metadata.sha256 || stored.metadata?.releaseId !== metadata.releaseId)
288
+ throw new NativeReleaseRegistryError("Stored native release artifact does not match its immutable identity");
289
+ };
290
+ const read = async (input) => {
291
+ if (!APP_ID_PATTERN.test(input.appId))
292
+ throw new NativeReleaseRegistryError("Native release appId is invalid");
293
+ const digest = input.releaseId.replace(/^amobile_android_/, "");
294
+ if (!SHA256_PATTERN.test(digest) || input.releaseId !== `amobile_android_${digest}`)
295
+ throw new NativeReleaseRegistryError("Native release id is invalid");
296
+ const identity = {
297
+ appBuild: "lookup",
298
+ appId: input.appId,
299
+ artifact: "app-release.aab",
300
+ bytes: 1,
301
+ engine: "capacitor",
302
+ format: 1,
303
+ platform: "android",
304
+ releaseId: input.releaseId,
305
+ runtime: "lookup",
306
+ sha256: digest,
307
+ signed: true,
308
+ type: "aab"
309
+ };
310
+ const key = recordKey(identity);
311
+ const bytes = await options.store.get(key);
312
+ if (!bytes)
313
+ return null;
314
+ const storedRecord = await options.store.head(key);
315
+ if (!storedRecord || storedRecord.size !== bytes.byteLength || storedRecord.metadata?.releaseId !== input.releaseId || storedRecord.metadata?.sha256 !== sha256Bytes(bytes))
316
+ throw new NativeReleaseRegistryError("Stored native release record does not match its immutable identity");
317
+ const record = parseRecord(decodedJson(bytes));
318
+ if (record.metadata.appId !== input.appId || record.metadata.platform !== input.platform || record.metadata.releaseId !== input.releaseId || record.artifactKey !== artifactKey(record.metadata))
319
+ throw new NativeReleaseRegistryError("Stored native release record identity does not match");
320
+ await requireStoredArtifact(record.artifactKey, record.metadata);
321
+ return record;
322
+ };
323
+ const promote = async (input) => {
324
+ input.signal?.throwIfAborted();
325
+ const record = await read(input);
326
+ if (!record)
327
+ throw new NativeReleaseRegistryError("Native release was not published");
328
+ if (!record.metadata.signed && !input.allowUnsigned)
329
+ throw new NativeReleaseRegistryError("Unsigned native releases cannot be promoted");
330
+ const key = channelKey(input.appId, input.platform, input.channel);
331
+ const existingBytes = await options.store.get(key);
332
+ if (existingBytes) {
333
+ const existing = parseChannel(decodedJson(existingBytes));
334
+ if (existing.appId !== input.appId || existing.platform !== input.platform || existing.channel !== input.channel)
335
+ throw new NativeReleaseRegistryError("Stored native release channel identity does not match");
336
+ if (existing.releaseId === input.releaseId)
337
+ return existing;
338
+ }
339
+ const channel = {
340
+ appId: input.appId,
341
+ channel: requireChannel(input.channel),
342
+ format: NATIVE_RELEASE_REGISTRY_FORMAT,
343
+ platform: input.platform,
344
+ promotedAt: clock().toISOString(),
345
+ releaseId: record.metadata.releaseId,
346
+ sha256: record.metadata.sha256
347
+ };
348
+ const serialized = encodedJson(channel);
349
+ await options.store.put(key, serialized, {
350
+ cacheControl: "no-cache",
351
+ contentType: "application/json",
352
+ maxBytes: serialized.byteLength,
353
+ metadata: {
354
+ channel: channel.channel,
355
+ releaseId: channel.releaseId,
356
+ sha256: channel.sha256
357
+ },
358
+ signal: input.signal
359
+ });
360
+ const stored = await options.store.get(key);
361
+ if (!stored || JSON.stringify(parseChannel(decodedJson(stored))) !== JSON.stringify(channel))
362
+ throw new NativeReleaseRegistryError("Native release channel verification failed");
363
+ return channel;
364
+ };
365
+ return {
366
+ promote,
367
+ publish: async (input) => {
368
+ input.signal?.throwIfAborted();
369
+ const localRoot = path2.resolve(input.releaseRoot);
370
+ const metadata = parseMetadata(JSON.parse(await readFile(path2.join(localRoot, "release.json"), "utf8")));
371
+ if (metadata.bytes > maxArtifactBytes)
372
+ throw new NativeReleaseRegistryError("Native release exceeds the configured artifact limit");
373
+ if (!metadata.signed && !input.allowUnsigned)
374
+ throw new NativeReleaseRegistryError("Unsigned native releases cannot be published");
375
+ const localArtifact = path2.join(localRoot, metadata.artifact);
376
+ const artifactStats = await stat2(localArtifact).catch(() => null);
377
+ if (!artifactStats?.isFile() || artifactStats.size !== metadata.bytes)
378
+ throw new NativeReleaseRegistryError("Native release artifact size does not match its metadata");
379
+ if (await sha256File2(Bun.file(localArtifact)) !== metadata.sha256)
380
+ throw new NativeReleaseRegistryError("Native release artifact digest does not match its metadata");
381
+ const existing = await read({
382
+ appId: metadata.appId,
383
+ platform: metadata.platform,
384
+ releaseId: metadata.releaseId
385
+ });
386
+ let record;
387
+ let reused = false;
388
+ if (existing) {
389
+ if (!sameMetadata(existing.metadata, metadata))
390
+ throw new NativeReleaseRegistryError("Published native release metadata is immutable");
391
+ record = existing;
392
+ reused = true;
393
+ } else {
394
+ const key = artifactKey(metadata);
395
+ const storedArtifact = await options.store.head(key);
396
+ if (storedArtifact) {
397
+ await requireStoredArtifact(key, metadata);
398
+ } else {
399
+ await options.store.put(key, Bun.file(localArtifact).stream(), {
400
+ cacheControl: "public, max-age=31536000, immutable",
401
+ contentType: "application/octet-stream",
402
+ maxBytes: metadata.bytes,
403
+ metadata: {
404
+ appId: metadata.appId,
405
+ releaseId: metadata.releaseId,
406
+ sha256: metadata.sha256
407
+ },
408
+ signal: input.signal
409
+ });
410
+ await requireStoredArtifact(key, metadata);
411
+ }
412
+ record = {
413
+ artifactKey: key,
414
+ format: NATIVE_RELEASE_REGISTRY_FORMAT,
415
+ metadata
416
+ };
417
+ const serialized = encodedJson(record);
418
+ await options.store.put(recordKey(metadata), serialized, {
419
+ cacheControl: "public, max-age=31536000, immutable",
420
+ contentType: "application/json",
421
+ maxBytes: serialized.byteLength,
422
+ metadata: {
423
+ releaseId: metadata.releaseId,
424
+ sha256: sha256Bytes(serialized)
425
+ },
426
+ signal: input.signal
427
+ });
428
+ const verified = await read({
429
+ appId: metadata.appId,
430
+ platform: metadata.platform,
431
+ releaseId: metadata.releaseId
432
+ });
433
+ if (!verified || JSON.stringify(verified) !== JSON.stringify(record))
434
+ throw new NativeReleaseRegistryError("Native release publication verification failed");
435
+ }
436
+ const channel = input.channel ? await promote({
437
+ allowUnsigned: input.allowUnsigned,
438
+ appId: metadata.appId,
439
+ channel: input.channel,
440
+ platform: metadata.platform,
441
+ releaseId: metadata.releaseId,
442
+ signal: input.signal
443
+ }) : undefined;
444
+ return { ...channel ? { channel } : {}, record, reused };
445
+ },
446
+ read,
447
+ resolve: async (input) => {
448
+ if (!APP_ID_PATTERN.test(input.appId))
449
+ throw new NativeReleaseRegistryError("Native release appId is invalid");
450
+ const key = channelKey(input.appId, input.platform, input.channel);
451
+ const bytes = await options.store.get(key);
452
+ if (!bytes)
453
+ return null;
454
+ const channel = parseChannel(decodedJson(bytes));
455
+ if (channel.appId !== input.appId || channel.platform !== input.platform || channel.channel !== input.channel)
456
+ throw new NativeReleaseRegistryError("Stored native release channel identity does not match");
457
+ const record = await read({
458
+ appId: channel.appId,
459
+ platform: channel.platform,
460
+ releaseId: channel.releaseId
461
+ });
462
+ if (!record || record.metadata.sha256 !== channel.sha256)
463
+ throw new NativeReleaseRegistryError("Native release channel points to a missing or invalid release");
464
+ return { channel, record };
465
+ }
466
+ };
467
+ };
468
+
168
469
  // src/edgeIngress.ts
169
470
  var validPort = (port) => Number.isInteger(port) && port >= 1 && port <= 65535;
170
471
 
@@ -860,11 +1161,14 @@ export {
860
1161
  extractReleaseArtifact,
861
1162
  defaultBunPipeline,
862
1163
  createReleaseArtifact,
1164
+ createNativeReleaseRegistry,
863
1165
  createDeployer,
864
1166
  bareManager,
865
1167
  ReleaseArtifactError,
1168
+ NativeReleaseRegistryError,
1169
+ NATIVE_RELEASE_REGISTRY_FORMAT,
866
1170
  EdgeIngressValidationError
867
1171
  };
868
1172
 
869
- //# debugId=BC2DAC4B8EA4FAB664756E2164756E21
1173
+ //# debugId=19FB526D8301A46164756E2164756E21
870
1174
  //# sourceMappingURL=index.js.map