Skip to content

Commit e76ee94

Browse files
Copilothotlong
andcommitted
feat: add package artifact, platform compatibility, artifact distribution, upgrade migration protocols
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent a6d123e commit e76ee94

11 files changed

Lines changed: 804 additions & 1 deletion

packages/spec/src/cloud/marketplace.test.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
MarketplaceSearchResponseSchema,
1212
MarketplaceInstallRequestSchema,
1313
MarketplaceInstallResponseSchema,
14+
ArtifactReferenceSchema,
15+
ArtifactDownloadResponseSchema,
1416
} from './marketplace.zod';
1517

1618
describe('PublisherVerificationSchema', () => {
@@ -340,3 +342,116 @@ describe('MarketplaceInstallResponseSchema', () => {
340342
expect(parsed.success).toBe(false);
341343
});
342344
});
345+
346+
describe('ArtifactReferenceSchema', () => {
347+
it('should accept valid artifact reference', () => {
348+
const ref = {
349+
url: 'https://registry.objectstack.io/packages/com.acme.crm-2.0.0.tgz',
350+
sha256: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
351+
size: 1048576,
352+
uploadedAt: '2025-06-01T12:00:00Z',
353+
};
354+
const parsed = ArtifactReferenceSchema.parse(ref);
355+
expect(parsed.format).toBe('tgz');
356+
expect(parsed.size).toBe(1048576);
357+
});
358+
359+
it('should accept zip format', () => {
360+
const ref = {
361+
url: 'https://registry.objectstack.io/packages/com.acme.crm-2.0.0.zip',
362+
sha256: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
363+
size: 2048576,
364+
format: 'zip' as const,
365+
uploadedAt: '2025-06-01T12:00:00Z',
366+
};
367+
const parsed = ArtifactReferenceSchema.parse(ref);
368+
expect(parsed.format).toBe('zip');
369+
});
370+
371+
it('should reject invalid SHA256', () => {
372+
expect(() => ArtifactReferenceSchema.parse({
373+
url: 'https://registry.objectstack.io/pkg.tgz',
374+
sha256: 'invalid-hash',
375+
size: 1024,
376+
uploadedAt: '2025-06-01T12:00:00Z',
377+
})).toThrow();
378+
});
379+
380+
it('should reject non-positive size', () => {
381+
expect(() => ArtifactReferenceSchema.parse({
382+
url: 'https://registry.objectstack.io/pkg.tgz',
383+
sha256: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
384+
size: 0,
385+
uploadedAt: '2025-06-01T12:00:00Z',
386+
})).toThrow();
387+
});
388+
});
389+
390+
describe('ArtifactDownloadResponseSchema', () => {
391+
it('should accept valid download response', () => {
392+
const response = {
393+
downloadUrl: 'https://cdn.objectstack.io/artifacts/com.acme.crm-2.0.0.tgz?token=abc',
394+
sha256: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
395+
size: 1048576,
396+
format: 'tgz' as const,
397+
expiresAt: '2025-06-01T13:00:00Z',
398+
};
399+
const parsed = ArtifactDownloadResponseSchema.parse(response);
400+
expect(parsed.expiresAt).toBe('2025-06-01T13:00:00Z');
401+
});
402+
403+
it('should accept response without expiration', () => {
404+
const response = {
405+
downloadUrl: 'https://cdn.objectstack.io/artifacts/com.acme.crm-2.0.0.tgz',
406+
sha256: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
407+
size: 1048576,
408+
format: 'tgz' as const,
409+
};
410+
const parsed = ArtifactDownloadResponseSchema.parse(response);
411+
expect(parsed.expiresAt).toBeUndefined();
412+
});
413+
});
414+
415+
describe('MarketplaceListingSchema - Artifact References', () => {
416+
it('should accept listing with artifact references in versions', () => {
417+
const listing = {
418+
id: 'listing-001',
419+
packageId: 'com.acme.crm',
420+
publisherId: 'pub-001',
421+
name: 'Acme CRM',
422+
category: 'crm' as const,
423+
latestVersion: '2.0.0',
424+
versions: [
425+
{
426+
version: '2.0.0',
427+
releaseDate: '2025-06-01T00:00:00Z',
428+
artifact: {
429+
url: 'https://registry.objectstack.io/packages/com.acme.crm-2.0.0.tgz',
430+
sha256: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
431+
size: 1048576,
432+
uploadedAt: '2025-06-01T00:00:00Z',
433+
},
434+
},
435+
],
436+
};
437+
const parsed = MarketplaceListingSchema.parse(listing);
438+
expect(parsed.versions![0].artifact?.sha256).toBe('a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2');
439+
});
440+
});
441+
442+
describe('MarketplaceInstallRequestSchema - Artifact Reference', () => {
443+
it('should accept install request with artifact reference', () => {
444+
const request = {
445+
listingId: 'listing-001',
446+
version: '2.0.0',
447+
artifactRef: {
448+
url: 'https://registry.objectstack.io/packages/com.acme.crm-2.0.0.tgz',
449+
sha256: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2',
450+
size: 1048576,
451+
uploadedAt: '2025-06-01T00:00:00Z',
452+
},
453+
};
454+
const parsed = MarketplaceInstallRequestSchema.parse(request);
455+
expect(parsed.artifactRef?.url).toContain('com.acme.crm');
456+
});
457+
});

packages/spec/src/cloud/marketplace.zod.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,61 @@ export const PublisherSchema = z.object({
8686
registeredAt: z.string().datetime().optional(),
8787
});
8888

89+
// ==========================================
90+
// Artifact Reference & Distribution
91+
// ==========================================
92+
93+
/**
94+
* Artifact Reference Schema
95+
*
96+
* Points to a downloadable package artifact with integrity verification.
97+
* Used in marketplace listings and install workflows.
98+
*/
99+
export const ArtifactReferenceSchema = z.object({
100+
/** Artifact download URL */
101+
url: z.string().url().describe('Artifact download URL'),
102+
103+
/** SHA256 integrity checksum */
104+
sha256: z.string().regex(/^[a-f0-9]{64}$/).describe('SHA256 checksum'),
105+
106+
/** File size in bytes */
107+
size: z.number().int().positive().describe('Artifact size in bytes'),
108+
109+
/** Artifact format */
110+
format: z.enum(['tgz', 'zip']).default('tgz').describe('Artifact format'),
111+
112+
/** Upload timestamp */
113+
uploadedAt: z.string().datetime().describe('Upload timestamp'),
114+
}).describe('Reference to a downloadable package artifact');
115+
116+
export type ArtifactReference = z.infer<typeof ArtifactReferenceSchema>;
117+
118+
/**
119+
* Artifact Download Response Schema
120+
*
121+
* Response from the artifact download API endpoint.
122+
* Provides a time-limited download URL with integrity metadata.
123+
*/
124+
export const ArtifactDownloadResponseSchema = z.object({
125+
/** Pre-signed or direct download URL */
126+
downloadUrl: z.string().url().describe('Artifact download URL (may be pre-signed)'),
127+
128+
/** SHA256 checksum for download verification */
129+
sha256: z.string().regex(/^[a-f0-9]{64}$/).describe('SHA256 checksum for verification'),
130+
131+
/** File size in bytes */
132+
size: z.number().int().positive().describe('Artifact size in bytes'),
133+
134+
/** Artifact format */
135+
format: z.enum(['tgz', 'zip']).describe('Artifact format'),
136+
137+
/** URL expiration time (for pre-signed URLs) */
138+
expiresAt: z.string().datetime().optional()
139+
.describe('URL expiration timestamp for pre-signed URLs'),
140+
}).describe('Artifact download response with integrity metadata');
141+
142+
export type ArtifactDownloadResponse = z.infer<typeof ArtifactDownloadResponseSchema>;
143+
89144
// ==========================================
90145
// Marketplace Listing
91146
// ==========================================
@@ -211,6 +266,9 @@ export const MarketplaceListingSchema = z.object({
211266
releaseNotes: z.string().optional(),
212267
minPlatformVersion: z.string().optional(),
213268
deprecated: z.boolean().default(false),
269+
/** Artifact reference for this version */
270+
artifact: ArtifactReferenceSchema.optional()
271+
.describe('Downloadable artifact for this version'),
214272
})).optional().describe('Published versions'),
215273

216274
/** Aggregate statistics */
@@ -399,6 +457,10 @@ export const MarketplaceInstallRequestSchema = z.object({
399457
/** Whether to enable immediately after install */
400458
enableOnInstall: z.boolean().default(true),
401459

460+
/** Artifact reference (resolved from listing version, or provided directly) */
461+
artifactRef: ArtifactReferenceSchema.optional()
462+
.describe('Artifact reference for direct installation'),
463+
402464
/** Tenant ID */
403465
tenantId: z.string().optional(),
404466
});
@@ -435,3 +497,5 @@ export type MarketplaceSearchRequest = z.infer<typeof MarketplaceSearchRequestSc
435497
export type MarketplaceSearchResponse = z.infer<typeof MarketplaceSearchResponseSchema>;
436498
export type MarketplaceInstallRequest = z.infer<typeof MarketplaceInstallRequestSchema>;
437499
export type MarketplaceInstallResponse = z.infer<typeof MarketplaceInstallResponseSchema>;
500+
export type ArtifactReference = z.infer<typeof ArtifactReferenceSchema>;
501+
export type ArtifactDownloadResponse = z.infer<typeof ArtifactDownloadResponseSchema>;

packages/spec/src/kernel/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export * from './manifest.zod';
99
export * from './metadata-customization.zod';
1010
export * from './metadata-loader.zod';
1111
export * from './metadata-plugin.zod';
12+
export * from './package-artifact.zod';
1213
export * from './package-registry.zod';
1314
export * from './package-upgrade.zod';
1415
export * from './plugin-capability.zod';

packages/spec/src/kernel/manifest.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,58 @@ describe('ManifestSchema', () => {
382382
});
383383
});
384384

385+
describe('Platform Compatibility (engine)', () => {
386+
it('should accept manifest with engine requirements', () => {
387+
const manifest = {
388+
id: 'com.acme.crm',
389+
version: '1.0.0',
390+
type: 'app' as const,
391+
name: 'Acme CRM',
392+
engine: {
393+
objectstack: '>=3.0.0',
394+
},
395+
};
396+
const parsed = ManifestSchema.parse(manifest);
397+
expect(parsed.engine?.objectstack).toBe('>=3.0.0');
398+
});
399+
400+
it('should accept various semver range formats', () => {
401+
const ranges = ['>=3.0.0', '^2.1.0', '~1.5.0', '>=1.0.0', '3.0.0'];
402+
ranges.forEach(range => {
403+
const manifest = {
404+
id: 'com.test.app',
405+
version: '1.0.0',
406+
type: 'app' as const,
407+
name: 'Test',
408+
engine: { objectstack: range },
409+
};
410+
expect(() => ManifestSchema.parse(manifest)).not.toThrow();
411+
});
412+
});
413+
414+
it('should reject invalid engine version format', () => {
415+
const manifest = {
416+
id: 'com.test.app',
417+
version: '1.0.0',
418+
type: 'app' as const,
419+
name: 'Test',
420+
engine: { objectstack: 'latest' },
421+
};
422+
expect(() => ManifestSchema.parse(manifest)).toThrow();
423+
});
424+
425+
it('should accept manifest without engine (backward compatible)', () => {
426+
const manifest = {
427+
id: 'com.test.app',
428+
version: '1.0.0',
429+
type: 'app' as const,
430+
name: 'Test',
431+
};
432+
const parsed = ManifestSchema.parse(manifest);
433+
expect(parsed.engine).toBeUndefined();
434+
});
435+
});
436+
385437
describe('Reverse Domain Notation', () => {
386438
it('should accept various reverse domain notation formats', () => {
387439
const validIds = [

packages/spec/src/kernel/manifest.zod.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,24 @@ export const ManifestSchema = z.object({
339339
*/
340340
loading: PluginLoadingConfigSchema.optional()
341341
.describe('Plugin loading and runtime behavior configuration'),
342+
343+
/**
344+
* Platform Compatibility Requirements.
345+
* Specifies the minimum ObjectStack platform version required to run this package.
346+
* Used at install time to prevent incompatible packages from being installed.
347+
*
348+
* @example
349+
* ```yaml
350+
* engine:
351+
* objectstack: ">=3.0.0"
352+
* ```
353+
*/
354+
engine: z.object({
355+
/** ObjectStack platform version requirement (SemVer range) */
356+
objectstack: z.string()
357+
.regex(/^[><=~^]*\d+\.\d+\.\d+/)
358+
.describe('ObjectStack platform version requirement (SemVer range, e.g. ">=3.0.0")'),
359+
}).optional().describe('Platform compatibility requirements'),
342360
});
343361

344362
/**

0 commit comments

Comments
 (0)