@ekanos/cli 0.17.1 → 0.18.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 +1 -0
- package/dist/commands/catalog.js +11 -7
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/names.d.ts +19 -0
- package/dist/commands/names.js +122 -0
- package/dist/commands/names.js.map +1 -0
- package/dist/commands/publish.d.ts +53 -3
- package/dist/commands/publish.js +58 -16
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/sources.js +15 -5
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +12 -12
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/teams-connect.js +1 -1
- package/dist/commands/teams-connect.js.map +1 -1
- package/dist/experience/boundary.d.ts +2 -2
- package/dist/experience/boundary.js +2 -11
- package/dist/experience/boundary.js.map +1 -1
- package/dist/experience/build.d.ts +3 -1
- package/dist/experience/build.js +15 -26
- package/dist/experience/build.js.map +1 -1
- package/dist/experience/clean-room.d.ts +40 -0
- package/dist/experience/clean-room.js +150 -0
- package/dist/experience/clean-room.js.map +1 -0
- package/dist/experience/definition-checks.d.ts +12 -1
- package/dist/experience/definition-checks.js +34 -11
- package/dist/experience/definition-checks.js.map +1 -1
- package/dist/experience/definition-shape.d.ts +20 -2
- package/dist/experience/definition-shape.js.map +1 -1
- package/dist/experience/publish.d.ts +42 -5
- package/dist/experience/publish.js +195 -11
- package/dist/experience/publish.js.map +1 -1
- package/dist/harness-scaffold.js +3 -1
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.js +25 -2
- package/dist/index.js.map +1 -1
- package/dist/names-api.d.ts +90 -0
- package/dist/names-api.js +123 -0
- package/dist/names-api.js.map +1 -0
- package/dist/product-kinds.d.ts +13 -0
- package/dist/product-kinds.js +17 -0
- package/dist/product-kinds.js.map +1 -0
- package/dist/publish-api.d.ts +38 -5
- package/dist/publish-api.js +59 -16
- package/dist/publish-api.js.map +1 -1
- package/dist/repo/publish.d.ts +6 -3
- package/dist/repo/publish.js +48 -14
- package/dist/repo/publish.js.map +1 -1
- package/dist/source-products-api.d.ts +3 -2
- package/dist/source-products-api.js +1 -1
- package/dist/source-products-api.js.map +1 -1
- package/dist/team-connect-api.d.ts +4 -0
- package/dist/team-connect-api.js +1 -1
- package/dist/team-connect-api.js.map +1 -1
- package/dist/toolchain-api.d.ts +15 -0
- package/dist/toolchain-api.js +12 -1
- package/dist/toolchain-api.js.map +1 -1
- package/package.json +2 -2
- package/templates/experience/package.json.tmpl +7 -4
- package/templates/harness/app/experience/layout/page.tsx.tmpl +1 -0
- package/templates/harness/app/experience/layout.tsx.tmpl +3 -0
- package/templates/harness/app/experience/pages/[id]/[...path]/page.tsx.tmpl +1 -0
- package/templates/package.json.tmpl +4 -4
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* The CLI's client for package names: the pre-flight that says what a slug
|
|
4
|
+
* will be called, and the global-name claims.
|
|
5
|
+
*
|
|
6
|
+
* GET /api/partner/slugs/<slug>?source=<slug|id>[&availability=global|restricted]
|
|
7
|
+
* → 200 { ok: true, data: { slug, status, available, namespace,
|
|
8
|
+
* displayName, token, availability, claim } }
|
|
9
|
+
* `status` is the host's word for the slug (`global_unapproved` when a
|
|
10
|
+
* global package has no approved name). Every field past `available` is
|
|
11
|
+
* optional here: an older host omits them.
|
|
12
|
+
*
|
|
13
|
+
* POST /api/partner/names/claims { source, slug, reason? }
|
|
14
|
+
* → 201 { ok: true, data: { id, slug, status: "requested", createdAt } }
|
|
15
|
+
* → 409 { code: "CLAIM_EXISTS" | "SLUG_RESERVED" | "SLUG_TAKEN" }
|
|
16
|
+
*
|
|
17
|
+
* GET /api/partner/names/claims/<slug>?source=<slug|id>
|
|
18
|
+
* → 200 { ok: true, data: { slug, status: "none" | "requested" |
|
|
19
|
+
* "approved" | "rejected", reason, requestedAt, decidedAt } }
|
|
20
|
+
*
|
|
21
|
+
* → 400 INVALID_REQUEST · 401 / 3xx refresh once and retry · 403 FORBIDDEN
|
|
22
|
+
* (no developer seat) or MFA_REQUIRED · 404 SOURCE_NOT_FOUND
|
|
23
|
+
*/
|
|
24
|
+
export type NamesApiResult<T> = {
|
|
25
|
+
status: 'ok';
|
|
26
|
+
data: T;
|
|
27
|
+
} | {
|
|
28
|
+
status: 'unauthorized';
|
|
29
|
+
};
|
|
30
|
+
export declare const SlugLookupSchema: z.ZodObject<{
|
|
31
|
+
slug: z.ZodString;
|
|
32
|
+
status: z.ZodString;
|
|
33
|
+
available: z.ZodBoolean;
|
|
34
|
+
namespace: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
35
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
36
|
+
token: z.ZodOptional<z.ZodString>;
|
|
37
|
+
availability: z.ZodOptional<z.ZodEnum<{
|
|
38
|
+
global: "global";
|
|
39
|
+
restricted: "restricted";
|
|
40
|
+
}>>;
|
|
41
|
+
claim: z.ZodOptional<z.ZodNullable<z.ZodObject<{
|
|
42
|
+
status: z.ZodEnum<{
|
|
43
|
+
approved: "approved";
|
|
44
|
+
rejected: "rejected";
|
|
45
|
+
requested: "requested";
|
|
46
|
+
}>;
|
|
47
|
+
reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
48
|
+
}, z.core.$strip>>>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
export type SlugLookup = z.infer<typeof SlugLookupSchema>;
|
|
51
|
+
export declare const NameClaimRequestedSchema: z.ZodObject<{
|
|
52
|
+
id: z.ZodString;
|
|
53
|
+
slug: z.ZodString;
|
|
54
|
+
status: z.ZodLiteral<"requested">;
|
|
55
|
+
createdAt: z.ZodString;
|
|
56
|
+
}, z.core.$strip>;
|
|
57
|
+
export type NameClaimRequested = z.infer<typeof NameClaimRequestedSchema>;
|
|
58
|
+
export declare const NameClaimStatusSchema: z.ZodObject<{
|
|
59
|
+
slug: z.ZodString;
|
|
60
|
+
status: z.ZodEnum<{
|
|
61
|
+
approved: "approved";
|
|
62
|
+
none: "none";
|
|
63
|
+
rejected: "rejected";
|
|
64
|
+
requested: "requested";
|
|
65
|
+
}>;
|
|
66
|
+
reason: z.ZodNullable<z.ZodString>;
|
|
67
|
+
requestedAt: z.ZodNullable<z.ZodString>;
|
|
68
|
+
decidedAt: z.ZodNullable<z.ZodString>;
|
|
69
|
+
}, z.core.$strip>;
|
|
70
|
+
export type NameClaimStatus = z.infer<typeof NameClaimStatusSchema>;
|
|
71
|
+
/**
|
|
72
|
+
* What the host will call a slug for this Source. `unavailable` is any answer
|
|
73
|
+
* that is not the lookup (an older host without the route, a server error, a
|
|
74
|
+
* shape this CLI does not read): the caller carries on with the bare slug.
|
|
75
|
+
*/
|
|
76
|
+
export type SlugLookupResult = {
|
|
77
|
+
status: 'ok';
|
|
78
|
+
lookup: SlugLookup;
|
|
79
|
+
} | {
|
|
80
|
+
status: 'unavailable';
|
|
81
|
+
} | {
|
|
82
|
+
status: 'unauthorized';
|
|
83
|
+
};
|
|
84
|
+
export declare function lookupSlug(host: string, accessToken: string, source: string, slug: string, availability?: 'global' | 'restricted'): Promise<SlugLookupResult>;
|
|
85
|
+
export declare function requestNameClaim(host: string, accessToken: string, body: {
|
|
86
|
+
source: string;
|
|
87
|
+
slug: string;
|
|
88
|
+
reason?: string;
|
|
89
|
+
}): Promise<NamesApiResult<NameClaimRequested>>;
|
|
90
|
+
export declare function getNameClaim(host: string, accessToken: string, source: string, slug: string): Promise<NamesApiResult<NameClaimStatus>>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { request } from './auth/ekanos-api.js';
|
|
3
|
+
import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, validationError, } from './errors.js';
|
|
4
|
+
export const SlugLookupSchema = z.object({
|
|
5
|
+
slug: z.string(),
|
|
6
|
+
status: z.string(),
|
|
7
|
+
available: z.boolean(),
|
|
8
|
+
namespace: z.string().nullable().optional(),
|
|
9
|
+
displayName: z.string().optional(),
|
|
10
|
+
token: z.string().optional(),
|
|
11
|
+
availability: z.enum(['global', 'restricted']).optional(),
|
|
12
|
+
claim: z
|
|
13
|
+
.object({
|
|
14
|
+
status: z.enum(['requested', 'approved', 'rejected']),
|
|
15
|
+
reason: z.string().nullable().optional(),
|
|
16
|
+
})
|
|
17
|
+
.nullable()
|
|
18
|
+
.optional(),
|
|
19
|
+
});
|
|
20
|
+
export const NameClaimRequestedSchema = z.object({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
slug: z.string(),
|
|
23
|
+
status: z.literal('requested'),
|
|
24
|
+
createdAt: z.string(),
|
|
25
|
+
});
|
|
26
|
+
export const NameClaimStatusSchema = z.object({
|
|
27
|
+
slug: z.string(),
|
|
28
|
+
status: z.enum(['none', 'requested', 'approved', 'rejected']),
|
|
29
|
+
reason: z.string().nullable(),
|
|
30
|
+
requestedAt: z.string().nullable(),
|
|
31
|
+
decidedAt: z.string().nullable(),
|
|
32
|
+
});
|
|
33
|
+
export async function lookupSlug(host, accessToken, source, slug, availability) {
|
|
34
|
+
const query = new URLSearchParams({ source });
|
|
35
|
+
if (availability)
|
|
36
|
+
query.set('availability', availability);
|
|
37
|
+
const response = await request(`${host}/api/partner/slugs/${encodeURIComponent(slug)}?${query}`, { method: 'GET', headers: headers(accessToken) });
|
|
38
|
+
if (isUnauthorized(response.status))
|
|
39
|
+
return { status: 'unauthorized' };
|
|
40
|
+
if (!response.ok)
|
|
41
|
+
return { status: 'unavailable' };
|
|
42
|
+
const body = await readOptionalJson(response);
|
|
43
|
+
const parsed = SlugLookupSchema.safeParse(body.data);
|
|
44
|
+
return parsed.success
|
|
45
|
+
? { status: 'ok', lookup: parsed.data }
|
|
46
|
+
: { status: 'unavailable' };
|
|
47
|
+
}
|
|
48
|
+
export function requestNameClaim(host, accessToken, body) {
|
|
49
|
+
return call(host, accessToken, '/api/partner/names/claims', NameClaimRequestedSchema, { method: 'POST', body });
|
|
50
|
+
}
|
|
51
|
+
export function getNameClaim(host, accessToken, source, slug) {
|
|
52
|
+
return call(host, accessToken, `/api/partner/names/claims/${encodeURIComponent(slug)}?${new URLSearchParams({ source })}`, NameClaimStatusSchema, { method: 'GET' });
|
|
53
|
+
}
|
|
54
|
+
async function call(host, accessToken, path, schema, init) {
|
|
55
|
+
const response = await request(`${host}${path}`, {
|
|
56
|
+
method: init.method,
|
|
57
|
+
headers: Object.assign(Object.assign({}, headers(accessToken)), (init.body === undefined
|
|
58
|
+
? {}
|
|
59
|
+
: { 'content-type': 'application/json' })),
|
|
60
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
61
|
+
});
|
|
62
|
+
if (isUnauthorized(response.status))
|
|
63
|
+
return { status: 'unauthorized' };
|
|
64
|
+
const body = await readOptionalJson(response);
|
|
65
|
+
const code = typeof body.code === 'string' ? body.code : null;
|
|
66
|
+
const message = typeof body.error === 'string' && body.error ? body.error : null;
|
|
67
|
+
const route = path.split('?')[0];
|
|
68
|
+
if (response.status === 403 && code === 'MFA_REQUIRED') {
|
|
69
|
+
throw authRequiredError(message !== null && message !== void 0 ? message : `${host} requires multi-factor authentication.`, 'Complete MFA in the browser, then run "ekanos login" again.');
|
|
70
|
+
}
|
|
71
|
+
if (response.status === 403) {
|
|
72
|
+
throw forbiddenError(message !== null && message !== void 0 ? message : 'This account does not hold the developer seat on the Source.', 'Ask the Source operator for the developer seat, or run "ekanos sources" to list the sources you hold a seat on.');
|
|
73
|
+
}
|
|
74
|
+
if (response.status === 404) {
|
|
75
|
+
throw notFoundError(message !== null && message !== void 0 ? message : 'Source not found.', 'Check --source against "ekanos sources".');
|
|
76
|
+
}
|
|
77
|
+
if (response.status === 409) {
|
|
78
|
+
throw invalidStateError(message !== null && message !== void 0 ? message : 'The host refused the claim.', conflictHint(code));
|
|
79
|
+
}
|
|
80
|
+
if (response.status === 400) {
|
|
81
|
+
throw validationError(message !== null && message !== void 0 ? message : 'The host refused the request.', 'Names are kebab-case, e.g. "google-drive". See "ekanos names --help".');
|
|
82
|
+
}
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
throw networkError(`${host}${route} responded ${response.status}.`, response.status >= 500
|
|
85
|
+
? 'The Ekanos deployment returned a server error. Retry shortly.'
|
|
86
|
+
: 'Confirm the host runs a build that includes the names routes.');
|
|
87
|
+
}
|
|
88
|
+
const parsed = schema.safeParse(body.data);
|
|
89
|
+
if (!parsed.success) {
|
|
90
|
+
throw networkError(`${host}${route} answered in a shape this CLI does not read.`, 'Run "ekanos upgrade" - the CLI and the host disagree on the names contract.');
|
|
91
|
+
}
|
|
92
|
+
return { status: 'ok', data: parsed.data };
|
|
93
|
+
}
|
|
94
|
+
function conflictHint(code) {
|
|
95
|
+
switch (code) {
|
|
96
|
+
case 'CLAIM_EXISTS':
|
|
97
|
+
return 'A claim for this name is already open; check it with "ekanos names status <slug>".';
|
|
98
|
+
case 'SLUG_RESERVED':
|
|
99
|
+
return 'This name is reserved. Pick another, or publish restricted under your Source namespace.';
|
|
100
|
+
case 'SLUG_TAKEN':
|
|
101
|
+
return 'Another package already holds this name. Pick another, or publish restricted under your Source namespace.';
|
|
102
|
+
default:
|
|
103
|
+
return 'Check the claim with "ekanos names status <slug>".';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function headers(accessToken) {
|
|
107
|
+
return { accept: 'application/json', authorization: `Bearer ${accessToken}` };
|
|
108
|
+
}
|
|
109
|
+
function isUnauthorized(status) {
|
|
110
|
+
return status === 401 || (status >= 300 && status < 400);
|
|
111
|
+
}
|
|
112
|
+
async function readOptionalJson(response) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = await response.json();
|
|
115
|
+
return typeof parsed === 'object' && parsed !== null
|
|
116
|
+
? parsed
|
|
117
|
+
: {};
|
|
118
|
+
}
|
|
119
|
+
catch (_a) {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=names-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"names-api.js","sourceRoot":"","sources":["../src/names-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for package names: the pre-flight that says what a slug\n * will be called, and the global-name claims.\n *\n * GET /api/partner/slugs/<slug>?source=<slug|id>[&availability=global|restricted]\n * → 200 { ok: true, data: { slug, status, available, namespace,\n * displayName, token, availability, claim } }\n * `status` is the host's word for the slug (`global_unapproved` when a\n * global package has no approved name). Every field past `available` is\n * optional here: an older host omits them.\n *\n * POST /api/partner/names/claims { source, slug, reason? }\n * → 201 { ok: true, data: { id, slug, status: \"requested\", createdAt } }\n * → 409 { code: \"CLAIM_EXISTS\" | \"SLUG_RESERVED\" | \"SLUG_TAKEN\" }\n *\n * GET /api/partner/names/claims/<slug>?source=<slug|id>\n * → 200 { ok: true, data: { slug, status: \"none\" | \"requested\" |\n * \"approved\" | \"rejected\", reason, requestedAt, decidedAt } }\n *\n * → 400 INVALID_REQUEST · 401 / 3xx refresh once and retry · 403 FORBIDDEN\n * (no developer seat) or MFA_REQUIRED · 404 SOURCE_NOT_FOUND\n */\n\nexport type NamesApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nexport const SlugLookupSchema = z.object({\n slug: z.string(),\n status: z.string(),\n available: z.boolean(),\n namespace: z.string().nullable().optional(),\n displayName: z.string().optional(),\n token: z.string().optional(),\n availability: z.enum(['global', 'restricted']).optional(),\n claim: z\n .object({\n status: z.enum(['requested', 'approved', 'rejected']),\n reason: z.string().nullable().optional(),\n })\n .nullable()\n .optional(),\n});\n\nexport type SlugLookup = z.infer<typeof SlugLookupSchema>;\n\nexport const NameClaimRequestedSchema = z.object({\n id: z.string(),\n slug: z.string(),\n status: z.literal('requested'),\n createdAt: z.string(),\n});\n\nexport type NameClaimRequested = z.infer<typeof NameClaimRequestedSchema>;\n\nexport const NameClaimStatusSchema = z.object({\n slug: z.string(),\n status: z.enum(['none', 'requested', 'approved', 'rejected']),\n reason: z.string().nullable(),\n requestedAt: z.string().nullable(),\n decidedAt: z.string().nullable(),\n});\n\nexport type NameClaimStatus = z.infer<typeof NameClaimStatusSchema>;\n\n/**\n * What the host will call a slug for this Source. `unavailable` is any answer\n * that is not the lookup (an older host without the route, a server error, a\n * shape this CLI does not read): the caller carries on with the bare slug.\n */\nexport type SlugLookupResult =\n | { status: 'ok'; lookup: SlugLookup }\n | { status: 'unavailable' }\n | { status: 'unauthorized' };\n\nexport async function lookupSlug(\n host: string,\n accessToken: string,\n source: string,\n slug: string,\n availability?: 'global' | 'restricted',\n): Promise<SlugLookupResult> {\n const query = new URLSearchParams({ source });\n if (availability) query.set('availability', availability);\n\n const response = await request(\n `${host}/api/partner/slugs/${encodeURIComponent(slug)}?${query}`,\n { method: 'GET', headers: headers(accessToken) },\n );\n\n if (isUnauthorized(response.status)) return { status: 'unauthorized' };\n if (!response.ok) return { status: 'unavailable' };\n\n const body = await readOptionalJson(response);\n const parsed = SlugLookupSchema.safeParse(body.data);\n\n return parsed.success\n ? { status: 'ok', lookup: parsed.data }\n : { status: 'unavailable' };\n}\n\nexport function requestNameClaim(\n host: string,\n accessToken: string,\n body: { source: string; slug: string; reason?: string },\n): Promise<NamesApiResult<NameClaimRequested>> {\n return call(\n host,\n accessToken,\n '/api/partner/names/claims',\n NameClaimRequestedSchema,\n { method: 'POST', body },\n );\n}\n\nexport function getNameClaim(\n host: string,\n accessToken: string,\n source: string,\n slug: string,\n): Promise<NamesApiResult<NameClaimStatus>> {\n return call(\n host,\n accessToken,\n `/api/partner/names/claims/${encodeURIComponent(slug)}?${new URLSearchParams({ source })}`,\n NameClaimStatusSchema,\n { method: 'GET' },\n );\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n path: string,\n schema: z.ZodType<T>,\n init: { method: 'GET' | 'POST'; body?: unknown },\n): Promise<NamesApiResult<T>> {\n const response = await request(`${host}${path}`, {\n method: init.method,\n headers: {\n ...headers(accessToken),\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n if (isUnauthorized(response.status)) return { status: 'unauthorized' };\n\n const body = await readOptionalJson(response);\n const code = typeof body.code === 'string' ? body.code : null;\n const message =\n typeof body.error === 'string' && body.error ? body.error : null;\n const route = path.split('?')[0];\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n 'Complete MFA in the browser, then run \"ekanos login\" again.',\n );\n }\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'This account does not hold the developer seat on the Source.',\n 'Ask the Source operator for the developer seat, or run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 404) {\n throw notFoundError(\n message ?? 'Source not found.',\n 'Check --source against \"ekanos sources\".',\n );\n }\n if (response.status === 409) {\n throw invalidStateError(\n message ?? 'The host refused the claim.',\n conflictHint(code),\n );\n }\n if (response.status === 400) {\n throw validationError(\n message ?? 'The host refused the request.',\n 'Names are kebab-case, e.g. \"google-drive\". See \"ekanos names --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${route} responded ${response.status}.`,\n response.status >= 500\n ? 'The Ekanos deployment returned a server error. Retry shortly.'\n : 'Confirm the host runs a build that includes the names routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${route} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" - the CLI and the host disagree on the names contract.',\n );\n }\n\n return { status: 'ok', data: parsed.data };\n}\n\nfunction conflictHint(code: string | null): string {\n switch (code) {\n case 'CLAIM_EXISTS':\n return 'A claim for this name is already open; check it with \"ekanos names status <slug>\".';\n case 'SLUG_RESERVED':\n return 'This name is reserved. Pick another, or publish restricted under your Source namespace.';\n case 'SLUG_TAKEN':\n return 'Another package already holds this name. Pick another, or publish restricted under your Source namespace.';\n default:\n return 'Check the claim with \"ekanos names status <slug>\".';\n }\n}\n\nfunction headers(accessToken: string): Record<string, string> {\n return { accept: 'application/json', authorization: `Bearer ${accessToken}` };\n}\n\nfunction isUnauthorized(status: number): boolean {\n return status === 401 || (status >= 300 && status < 400);\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type Kinded = {
|
|
2
|
+
kind: 'feature' | 'experience' | 'integration';
|
|
3
|
+
};
|
|
4
|
+
/**
|
|
5
|
+
* Whether a list came from a host that predates the Features rename
|
|
6
|
+
* (2026-10-28), where a built-in capability was called an `experience`. Such a
|
|
7
|
+
* host never says `feature`, while every current host has built-in Features, so
|
|
8
|
+
* a list with none is from an older one.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hostPredatesFeatures(products: readonly Kinded[]): boolean;
|
|
11
|
+
/** The display group of a product: Features, Experiences or Integrations. */
|
|
12
|
+
export declare function productGroup(product: Kinded, legacyHost: boolean): 'feature' | 'experience' | 'integration';
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a list came from a host that predates the Features rename
|
|
3
|
+
* (2026-10-28), where a built-in capability was called an `experience`. Such a
|
|
4
|
+
* host never says `feature`, while every current host has built-in Features, so
|
|
5
|
+
* a list with none is from an older one.
|
|
6
|
+
*/
|
|
7
|
+
export function hostPredatesFeatures(products) {
|
|
8
|
+
return (products.some((product) => product.kind === 'experience') &&
|
|
9
|
+
!products.some((product) => product.kind === 'feature'));
|
|
10
|
+
}
|
|
11
|
+
/** The display group of a product: Features, Experiences or Integrations. */
|
|
12
|
+
export function productGroup(product, legacyHost) {
|
|
13
|
+
if (product.kind === 'experience' && legacyHost)
|
|
14
|
+
return 'feature';
|
|
15
|
+
return product.kind;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=product-kinds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"product-kinds.js","sourceRoot":"","sources":["../src/product-kinds.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,QAA2B;IAC9D,OAAO,CACL,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,YAAY,CAAC;QACzD,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC,CACxD,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,MAAM,UAAU,YAAY,CAC1B,OAAe,EACf,UAAmB;IAEnB,IAAI,OAAO,CAAC,IAAI,KAAK,YAAY,IAAI,UAAU;QAAE,OAAO,SAAS,CAAC;IAClE,OAAO,OAAO,CAAC,IAAI,CAAC;AACtB,CAAC","sourcesContent":["type Kinded = { kind: 'feature' | 'experience' | 'integration' };\n\n/**\n * Whether a list came from a host that predates the Features rename\n * (2026-10-28), where a built-in capability was called an `experience`. Such a\n * host never says `feature`, while every current host has built-in Features, so\n * a list with none is from an older one.\n */\nexport function hostPredatesFeatures(products: readonly Kinded[]): boolean {\n return (\n products.some((product) => product.kind === 'experience') &&\n !products.some((product) => product.kind === 'feature')\n );\n}\n\n/** The display group of a product: Features, Experiences or Integrations. */\nexport function productGroup(\n product: Kinded,\n legacyHost: boolean,\n): 'feature' | 'experience' | 'integration' {\n if (product.kind === 'experience' && legacyHost) return 'feature';\n return product.kind;\n}\n"]}
|
package/dist/publish-api.d.ts
CHANGED
|
@@ -12,12 +12,22 @@ import type { AuthEnvironment } from './auth/session.js';
|
|
|
12
12
|
* POST /api/partner/submissions [Authorization: Bearer <access_token>]
|
|
13
13
|
* ← multipart/form-data:
|
|
14
14
|
* source — Ekanos source slug
|
|
15
|
-
* slug — integration slug (kebab-case)
|
|
15
|
+
* slug — integration (or experience) slug (kebab-case; bare, never namespaced)
|
|
16
16
|
* version — semver
|
|
17
|
-
*
|
|
17
|
+
* kind — "experience" for a composed experience; omitted for an
|
|
18
|
+
* integration (an older host never sees the field). Sent
|
|
19
|
+
* only to a host that advertises
|
|
20
|
+
* `capabilities.experienceSubmissions` on the toolchain
|
|
21
|
+
* route.
|
|
22
|
+
* manifest — JSON string (the parsed ekanos.json + declaration
|
|
23
|
+
* summary; for an experience, its built manifest.json)
|
|
18
24
|
* archive — gzipped tarball, ≤ 4 MiB
|
|
25
|
+
* availability — "global" | "restricted"; sent only when ekanos.json
|
|
26
|
+
* declares it
|
|
19
27
|
* → 201 { ok: true, data: { submissionId, slug, version, state,
|
|
20
|
-
* archiveSha256
|
|
28
|
+
* archiveSha256,
|
|
29
|
+
* // newer hosts only:
|
|
30
|
+
* namespace?: string|null, displayName? } }
|
|
21
31
|
* → 400 { error, code: "INVALID_REQUEST" | "INVALID_ARCHIVE" }
|
|
22
32
|
* → 401 { error, code: "AUTH_REQUIRED" } token absent or expired —
|
|
23
33
|
* refresh once and retry
|
|
@@ -25,6 +35,12 @@ import type { AuthEnvironment } from './auth/session.js';
|
|
|
25
35
|
* → 404 { error, code: "SOURCE_NOT_FOUND" }
|
|
26
36
|
* → 409 { error, code: "VERSION_EXISTS" } this (slug, version) is
|
|
27
37
|
* already submitted
|
|
38
|
+
* → 409 { error, code: "KIND_MISMATCH" } the slug is registered as
|
|
39
|
+
* the other kind
|
|
40
|
+
* → 409 { error, code: "GLOBAL_NAME_UNAPPROVED", hint? }
|
|
41
|
+
* availability "global" without
|
|
42
|
+
* an approved global name
|
|
43
|
+
* → 409 { error, code: "NAMESPACE_REQUIRED" } the source has no namespace
|
|
28
44
|
* → 413 { error, code: "ARCHIVE_TOO_LARGE" }
|
|
29
45
|
* → 3xx treated as 401 (an
|
|
30
46
|
* auth-gated Ekanos page
|
|
@@ -33,7 +49,7 @@ import type { AuthEnvironment } from './auth/session.js';
|
|
|
33
49
|
* GET /api/partner/submissions?source=<slug|id>[&detail=findings]
|
|
34
50
|
* [Authorization: Bearer]
|
|
35
51
|
* → 200 { ok: true, data: { source, integrations: [{ id, slug, name,
|
|
36
|
-
* createdAt, versions: [{ id, version, state, archiveSha256,
|
|
52
|
+
* createdAt, namespace?: string|null, displayName?, versions: [{ id, version, state, archiveSha256,
|
|
37
53
|
* submittedAt, gatedAt,
|
|
38
54
|
* // present once a gate has run (gate_report is non-null):
|
|
39
55
|
* errorCount?, warnCount?,
|
|
@@ -46,7 +62,8 @@ import type { AuthEnvironment } from './auth/session.js';
|
|
|
46
62
|
* → 404 { error, code: "SOURCE_NOT_FOUND" }
|
|
47
63
|
* → 3xx treated as 401
|
|
48
64
|
*
|
|
49
|
-
* `state` walks submitted -> gated|rejected -> dev -> prod -> revoked
|
|
65
|
+
* `state` walks submitted -> gated|rejected -> dev -> prod -> revoked (a
|
|
66
|
+
* `dev` version is marked `superseded` once a newer version reaches dev),
|
|
50
67
|
* enforced by a database trigger on the host's side (the full transition
|
|
51
68
|
* matrix lives there). `gated`/`dev`/`rejected` are the
|
|
52
69
|
* terminal-for-waiting states `ekanos status --watch` polls for.
|
|
@@ -61,10 +78,14 @@ export declare const MAX_MANIFEST_BYTES: number;
|
|
|
61
78
|
export interface SubmissionPayload {
|
|
62
79
|
source: string;
|
|
63
80
|
slug: string;
|
|
81
|
+
/** Omitted for an integration. */
|
|
82
|
+
kind?: 'experience';
|
|
64
83
|
version: string;
|
|
65
84
|
/** JSON string — already serialized by the caller. */
|
|
66
85
|
manifest: string;
|
|
67
86
|
archive: Uint8Array;
|
|
87
|
+
/** Sent only when ekanos.json declares an availability mode. */
|
|
88
|
+
availability?: 'global' | 'restricted';
|
|
68
89
|
}
|
|
69
90
|
export interface SubmissionReceipt {
|
|
70
91
|
submissionId: string;
|
|
@@ -72,6 +93,10 @@ export interface SubmissionReceipt {
|
|
|
72
93
|
version: string;
|
|
73
94
|
state: string;
|
|
74
95
|
archiveSha256: string;
|
|
96
|
+
/** The Source namespace the package is named under; absent on an older host. */
|
|
97
|
+
namespace?: string | null;
|
|
98
|
+
/** The full name the host uses (`acme/google-drive`); absent on an older host. */
|
|
99
|
+
displayName?: string;
|
|
75
100
|
}
|
|
76
101
|
export type SubmitResult = {
|
|
77
102
|
status: 'ok';
|
|
@@ -82,6 +107,11 @@ export type SubmitResult = {
|
|
|
82
107
|
status: 'unauthorized';
|
|
83
108
|
};
|
|
84
109
|
export declare function submitArchive(host: string, accessToken: string, payload: SubmissionPayload): Promise<SubmitResult>;
|
|
110
|
+
/**
|
|
111
|
+
* The refusal for a `global` package whose name no super-admin has approved.
|
|
112
|
+
* Shared by the pre-flight (`publish.ts`) and the 409 the host answers with.
|
|
113
|
+
*/
|
|
114
|
+
export declare function globalNameUnapprovedError(slug: string): import("./errors.js").CliError;
|
|
85
115
|
/**
|
|
86
116
|
* One gate finding, mirroring `@ekanos/integration-schema`'s `Finding` shape
|
|
87
117
|
* (the same one `validate`/`publish` carry in `data.findings`). Declared
|
|
@@ -135,6 +165,9 @@ export interface SubmissionListing {
|
|
|
135
165
|
slug: string;
|
|
136
166
|
name: string | null;
|
|
137
167
|
createdAt: string | null;
|
|
168
|
+
/** Absent on an older host. */
|
|
169
|
+
namespace?: string | null;
|
|
170
|
+
displayName?: string;
|
|
138
171
|
versions: SubmissionVersion[];
|
|
139
172
|
}
|
|
140
173
|
export type ListResult = {
|
package/dist/publish-api.js
CHANGED
|
@@ -14,12 +14,22 @@ import { authRequiredError, forbiddenError, invalidStateError, networkError, not
|
|
|
14
14
|
* POST /api/partner/submissions [Authorization: Bearer <access_token>]
|
|
15
15
|
* ← multipart/form-data:
|
|
16
16
|
* source — Ekanos source slug
|
|
17
|
-
* slug — integration slug (kebab-case)
|
|
17
|
+
* slug — integration (or experience) slug (kebab-case; bare, never namespaced)
|
|
18
18
|
* version — semver
|
|
19
|
-
*
|
|
19
|
+
* kind — "experience" for a composed experience; omitted for an
|
|
20
|
+
* integration (an older host never sees the field). Sent
|
|
21
|
+
* only to a host that advertises
|
|
22
|
+
* `capabilities.experienceSubmissions` on the toolchain
|
|
23
|
+
* route.
|
|
24
|
+
* manifest — JSON string (the parsed ekanos.json + declaration
|
|
25
|
+
* summary; for an experience, its built manifest.json)
|
|
20
26
|
* archive — gzipped tarball, ≤ 4 MiB
|
|
27
|
+
* availability — "global" | "restricted"; sent only when ekanos.json
|
|
28
|
+
* declares it
|
|
21
29
|
* → 201 { ok: true, data: { submissionId, slug, version, state,
|
|
22
|
-
* archiveSha256
|
|
30
|
+
* archiveSha256,
|
|
31
|
+
* // newer hosts only:
|
|
32
|
+
* namespace?: string|null, displayName? } }
|
|
23
33
|
* → 400 { error, code: "INVALID_REQUEST" | "INVALID_ARCHIVE" }
|
|
24
34
|
* → 401 { error, code: "AUTH_REQUIRED" } token absent or expired —
|
|
25
35
|
* refresh once and retry
|
|
@@ -27,6 +37,12 @@ import { authRequiredError, forbiddenError, invalidStateError, networkError, not
|
|
|
27
37
|
* → 404 { error, code: "SOURCE_NOT_FOUND" }
|
|
28
38
|
* → 409 { error, code: "VERSION_EXISTS" } this (slug, version) is
|
|
29
39
|
* already submitted
|
|
40
|
+
* → 409 { error, code: "KIND_MISMATCH" } the slug is registered as
|
|
41
|
+
* the other kind
|
|
42
|
+
* → 409 { error, code: "GLOBAL_NAME_UNAPPROVED", hint? }
|
|
43
|
+
* availability "global" without
|
|
44
|
+
* an approved global name
|
|
45
|
+
* → 409 { error, code: "NAMESPACE_REQUIRED" } the source has no namespace
|
|
30
46
|
* → 413 { error, code: "ARCHIVE_TOO_LARGE" }
|
|
31
47
|
* → 3xx treated as 401 (an
|
|
32
48
|
* auth-gated Ekanos page
|
|
@@ -35,7 +51,7 @@ import { authRequiredError, forbiddenError, invalidStateError, networkError, not
|
|
|
35
51
|
* GET /api/partner/submissions?source=<slug|id>[&detail=findings]
|
|
36
52
|
* [Authorization: Bearer]
|
|
37
53
|
* → 200 { ok: true, data: { source, integrations: [{ id, slug, name,
|
|
38
|
-
* createdAt, versions: [{ id, version, state, archiveSha256,
|
|
54
|
+
* createdAt, namespace?: string|null, displayName?, versions: [{ id, version, state, archiveSha256,
|
|
39
55
|
* submittedAt, gatedAt,
|
|
40
56
|
* // present once a gate has run (gate_report is non-null):
|
|
41
57
|
* errorCount?, warnCount?,
|
|
@@ -48,7 +64,8 @@ import { authRequiredError, forbiddenError, invalidStateError, networkError, not
|
|
|
48
64
|
* → 404 { error, code: "SOURCE_NOT_FOUND" }
|
|
49
65
|
* → 3xx treated as 401
|
|
50
66
|
*
|
|
51
|
-
* `state` walks submitted -> gated|rejected -> dev -> prod -> revoked
|
|
67
|
+
* `state` walks submitted -> gated|rejected -> dev -> prod -> revoked (a
|
|
68
|
+
* `dev` version is marked `superseded` once a newer version reaches dev),
|
|
52
69
|
* enforced by a database trigger on the host's side (the full transition
|
|
53
70
|
* matrix lives there). `gated`/`dev`/`rejected` are the
|
|
54
71
|
* terminal-for-waiting states `ekanos status --watch` polls for.
|
|
@@ -61,12 +78,16 @@ import { authRequiredError, forbiddenError, invalidStateError, networkError, not
|
|
|
61
78
|
*/
|
|
62
79
|
export const MAX_MANIFEST_BYTES = 64 * 1024;
|
|
63
80
|
export async function submitArchive(host, accessToken, payload) {
|
|
64
|
-
var _a;
|
|
81
|
+
var _a, _b;
|
|
65
82
|
const form = new FormData();
|
|
66
83
|
form.append('source', payload.source);
|
|
67
84
|
form.append('slug', payload.slug);
|
|
68
85
|
form.append('version', payload.version);
|
|
86
|
+
if (payload.kind)
|
|
87
|
+
form.append('kind', payload.kind);
|
|
69
88
|
form.append('manifest', payload.manifest);
|
|
89
|
+
if (payload.availability)
|
|
90
|
+
form.append('availability', payload.availability);
|
|
70
91
|
form.append('archive',
|
|
71
92
|
// Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,
|
|
72
93
|
// which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).
|
|
@@ -95,6 +116,17 @@ export async function submitArchive(host, accessToken, payload) {
|
|
|
95
116
|
throw notFoundError(serverMessage !== null && serverMessage !== void 0 ? serverMessage : `${host} has no source with slug "${payload.source}".`, `Check the --source value (or the "source" field in ekanos.json) ` +
|
|
96
117
|
`against the slug your Ekanos operator gave you.`);
|
|
97
118
|
}
|
|
119
|
+
if (response.status === 409 && str(body.code) === 'KIND_MISMATCH') {
|
|
120
|
+
throw invalidStateError(serverMessage !== null && serverMessage !== void 0 ? serverMessage : `"${payload.slug}" is registered as the other kind of package.`, `A slug names one kind for its whole life. Pick a different slug, or ` +
|
|
121
|
+
`submit it as the kind it was first registered as.`);
|
|
122
|
+
}
|
|
123
|
+
if (response.status === 409 && body.code === 'GLOBAL_NAME_UNAPPROVED') {
|
|
124
|
+
throw globalNameUnapprovedError(payload.slug);
|
|
125
|
+
}
|
|
126
|
+
if (response.status === 409 && body.code === 'NAMESPACE_REQUIRED') {
|
|
127
|
+
throw invalidStateError(serverMessage !== null && serverMessage !== void 0 ? serverMessage : `Source "${payload.source}" has no namespace to name "${payload.slug}" under.`, (_a = str(body.hint)) !== null && _a !== void 0 ? _a : `Ask an Ekanos super-admin to give "${payload.source}" a namespace, ` +
|
|
128
|
+
`then retry.`);
|
|
129
|
+
}
|
|
98
130
|
if (response.status === 409) {
|
|
99
131
|
throw invalidStateError(serverMessage !== null && serverMessage !== void 0 ? serverMessage : `Version ${payload.version} of "${payload.slug}" is already submitted.`, `Submitted versions are immutable. Bump the "version" field in ` +
|
|
100
132
|
`package.json and publish again.`);
|
|
@@ -110,7 +142,7 @@ export async function submitArchive(host, accessToken, payload) {
|
|
|
110
142
|
: `Confirm the host is an Ekanos deployment running a build that ` +
|
|
111
143
|
`includes the partner submissions route.`);
|
|
112
144
|
}
|
|
113
|
-
const data = ((
|
|
145
|
+
const data = ((_b = body.data) !== null && _b !== void 0 ? _b : {});
|
|
114
146
|
const receipt = {
|
|
115
147
|
submissionId: str(data.submissionId),
|
|
116
148
|
slug: str(data.slug),
|
|
@@ -118,12 +150,27 @@ export async function submitArchive(host, accessToken, payload) {
|
|
|
118
150
|
state: str(data.state),
|
|
119
151
|
archiveSha256: str(data.archiveSha256),
|
|
120
152
|
};
|
|
153
|
+
const displayName = str(data.displayName);
|
|
121
154
|
if (Object.values(receipt).some((value) => value === null)) {
|
|
122
155
|
throw networkError(`${host} accepted the submission but returned an incomplete receipt.`, `Confirm the deployment is running an Ekanos build that includes the ` +
|
|
123
156
|
`partner submissions route, then check the submission's state with ` +
|
|
124
157
|
`your operator before re-publishing.`);
|
|
125
158
|
}
|
|
126
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
status: 'ok',
|
|
161
|
+
receipt: Object.assign(Object.assign(Object.assign({}, receipt), (data.namespace === null || typeof data.namespace === 'string'
|
|
162
|
+
? { namespace: data.namespace }
|
|
163
|
+
: {})), (displayName ? { displayName } : {})),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The refusal for a `global` package whose name no super-admin has approved.
|
|
168
|
+
* Shared by the pre-flight (`publish.ts`) and the 409 the host answers with.
|
|
169
|
+
*/
|
|
170
|
+
export function globalNameUnapprovedError(slug) {
|
|
171
|
+
return invalidStateError(`The global name "${slug}" is not approved.`, `Run "ekanos names claim ${slug}" to request it, or publish restricted ` +
|
|
172
|
+
`(remove availability.mode "global" from ekanos.json, or set it to ` +
|
|
173
|
+
`"restricted").`);
|
|
127
174
|
}
|
|
128
175
|
/**
|
|
129
176
|
* List the caller's visible submissions for a source. Same authorization model
|
|
@@ -173,19 +220,15 @@ export async function listSubmissions(host, accessToken, source, options = {}) {
|
|
|
173
220
|
const rows = Array.isArray(data.integrations) ? data.integrations : [];
|
|
174
221
|
const integrations = rows
|
|
175
222
|
.filter((row) => typeof row === 'object' && row !== null)
|
|
176
|
-
.map((row) => { var _a, _b; return ({
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
name: str(row.name),
|
|
180
|
-
createdAt: str(row.createdAt),
|
|
181
|
-
versions: (Array.isArray(row.versions) ? row.versions : [])
|
|
223
|
+
.map((row) => { var _a, _b; return (Object.assign(Object.assign(Object.assign({ id: (_a = str(row.id)) !== null && _a !== void 0 ? _a : '', slug: (_b = str(row.slug)) !== null && _b !== void 0 ? _b : '', name: str(row.name), createdAt: str(row.createdAt) }, (row.namespace === null || typeof row.namespace === 'string'
|
|
224
|
+
? { namespace: row.namespace }
|
|
225
|
+
: {})), (str(row.displayName) ? { displayName: str(row.displayName) } : {})), { versions: (Array.isArray(row.versions) ? row.versions : [])
|
|
182
226
|
.filter((version) => typeof version === 'object' && version !== null)
|
|
183
227
|
.map((version) => { var _a, _b, _c; return (Object.assign(Object.assign({ id: (_a = str(version.id)) !== null && _a !== void 0 ? _a : '', version: (_b = str(version.version)) !== null && _b !== void 0 ? _b : '', state: (_c = str(version.state)) !== null && _c !== void 0 ? _c : '', archiveSha256: str(version.archiveSha256), submittedAt: str(version.submittedAt), gatedAt: str(version.gatedAt), errorCount: num(version.errorCount), warnCount: num(version.warnCount) }, (Array.isArray(version.findings)
|
|
184
228
|
? { findings: version.findings }
|
|
185
229
|
: {})), (Array.isArray(version.stages)
|
|
186
230
|
? { stages: version.stages }
|
|
187
|
-
: {}))); })
|
|
188
|
-
}); });
|
|
231
|
+
: {}))); }) })); });
|
|
189
232
|
return { status: 'ok', integrations };
|
|
190
233
|
}
|
|
191
234
|
/** Guard for the "still unauthorized after one refresh" terminal case. */
|