@ekanos/cli 0.17.0 → 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.
Files changed (71) hide show
  1. package/README.md +1 -0
  2. package/dist/commands/catalog.js +11 -7
  3. package/dist/commands/catalog.js.map +1 -1
  4. package/dist/commands/names.d.ts +19 -0
  5. package/dist/commands/names.js +122 -0
  6. package/dist/commands/names.js.map +1 -0
  7. package/dist/commands/publish.d.ts +53 -3
  8. package/dist/commands/publish.js +58 -16
  9. package/dist/commands/publish.js.map +1 -1
  10. package/dist/commands/sources.js +15 -5
  11. package/dist/commands/sources.js.map +1 -1
  12. package/dist/commands/status.d.ts +2 -0
  13. package/dist/commands/status.js +12 -12
  14. package/dist/commands/status.js.map +1 -1
  15. package/dist/commands/teams-connect.js +1 -1
  16. package/dist/commands/teams-connect.js.map +1 -1
  17. package/dist/experience/boundary.d.ts +2 -2
  18. package/dist/experience/boundary.js +2 -11
  19. package/dist/experience/boundary.js.map +1 -1
  20. package/dist/experience/build.d.ts +3 -1
  21. package/dist/experience/build.js +15 -26
  22. package/dist/experience/build.js.map +1 -1
  23. package/dist/experience/clean-room.d.ts +40 -0
  24. package/dist/experience/clean-room.js +150 -0
  25. package/dist/experience/clean-room.js.map +1 -0
  26. package/dist/experience/definition-checks.d.ts +12 -1
  27. package/dist/experience/definition-checks.js +34 -11
  28. package/dist/experience/definition-checks.js.map +1 -1
  29. package/dist/experience/definition-shape.d.ts +20 -2
  30. package/dist/experience/definition-shape.js.map +1 -1
  31. package/dist/experience/publish.d.ts +42 -5
  32. package/dist/experience/publish.js +195 -11
  33. package/dist/experience/publish.js.map +1 -1
  34. package/dist/harness-scaffold.js +3 -1
  35. package/dist/harness-scaffold.js.map +1 -1
  36. package/dist/index.js +25 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/names-api.d.ts +90 -0
  39. package/dist/names-api.js +123 -0
  40. package/dist/names-api.js.map +1 -0
  41. package/dist/pack.js +5 -0
  42. package/dist/pack.js.map +1 -1
  43. package/dist/product-kinds.d.ts +13 -0
  44. package/dist/product-kinds.js +17 -0
  45. package/dist/product-kinds.js.map +1 -0
  46. package/dist/project-checks.d.ts +7 -0
  47. package/dist/project-checks.js +47 -0
  48. package/dist/project-checks.js.map +1 -1
  49. package/dist/publish-api.d.ts +38 -5
  50. package/dist/publish-api.js +59 -16
  51. package/dist/publish-api.js.map +1 -1
  52. package/dist/repo/publish.d.ts +6 -3
  53. package/dist/repo/publish.js +48 -14
  54. package/dist/repo/publish.js.map +1 -1
  55. package/dist/source-products-api.d.ts +3 -2
  56. package/dist/source-products-api.js +1 -1
  57. package/dist/source-products-api.js.map +1 -1
  58. package/dist/team-connect-api.d.ts +4 -0
  59. package/dist/team-connect-api.js +1 -1
  60. package/dist/team-connect-api.js.map +1 -1
  61. package/dist/toolchain-api.d.ts +15 -0
  62. package/dist/toolchain-api.js +12 -1
  63. package/dist/toolchain-api.js.map +1 -1
  64. package/dist/validate-findings.js +2 -1
  65. package/dist/validate-findings.js.map +1 -1
  66. package/package.json +2 -2
  67. package/templates/experience/package.json.tmpl +7 -4
  68. package/templates/harness/app/experience/layout/page.tsx.tmpl +1 -0
  69. package/templates/harness/app/experience/layout.tsx.tmpl +3 -0
  70. package/templates/harness/app/experience/pages/[id]/[...path]/page.tsx.tmpl +1 -0
  71. package/templates/package.json.tmpl +5 -4
@@ -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
- * manifest — JSON string (the parsed ekanos.json + declaration summary)
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 = {
@@ -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
- * manifest — JSON string (the parsed ekanos.json + declaration summary)
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 = ((_a = body.data) !== null && _a !== void 0 ? _a : {});
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 { status: 'ok', receipt: receipt };
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
- id: (_a = str(row.id)) !== null && _a !== void 0 ? _a : '',
178
- slug: (_b = str(row.slug)) !== null && _b !== void 0 ? _b : '',
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. */
@@ -1 +1 @@
1
- {"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/ekanos-api';\nimport { loginHint } from './auth/host-trust';\nimport type { AuthEnvironment } from './auth/session';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Ekanos submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Ekanos web app.\n *\n * The server side is the host's `/api/partner/submissions` route. As with\n * the auth routes (see `auth/ekanos-api.ts`), the two halves are coupled\n * only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Ekanos source slug\n * slug — integration slug (kebab-case)\n * version — semver\n * manifest — JSON string (the parsed ekanos.json + declaration summary)\n * archive — gzipped tarball, ≤ 4 MiB\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256 } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no developer seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Ekanos page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id>[&detail=findings]\n * [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, versions: [{ id, version, state, archiveSha256,\n * submittedAt, gatedAt,\n * // present once a gate has run (gate_report is non-null):\n * errorCount?, warnCount?,\n * // present ONLY with ?detail=findings, alongside the above:\n * findings?, stages?\n * }] }] } }\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 3xx treated as 401\n *\n * `state` walks submitted -> gated|rejected -> dev -> prod -> revoked,\n * enforced by a database trigger on the host's side (the full transition\n * matrix lives there). `gated`/`dev`/`rejected` are the\n * terminal-for-waiting states `ekanos status --watch` polls for.\n * ---------------------------------------------------------------------------\n */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced server-side against\n * `Buffer.byteLength`. Mirrored here so `publish` can refuse an oversized\n * manifest with a clear message instead of the server's generic 400.\n */\nexport const MAX_MANIFEST_BYTES = 64 * 1024;\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n form.append('manifest', payload.manifest);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Ekanos route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `developer seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Ekanos operator gave you.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return { status: 'ok', receipt: receipt as SubmissionReceipt };\n}\n\n/**\n * One gate finding, mirroring `@ekanos/integration-schema`'s `Finding` shape\n * (the same one `validate`/`publish` carry in `data.findings`). Declared\n * locally rather than imported: the gate's own report type lives in a\n * server-side toolchain package private to the platform monorepo, which this\n * partner-facing CLI has no business depending on — the wire shape is the\n * only contract.\n *\n * `origin`/`id`/`createdAt`/`resolvedAt` are the additive fields the host's\n * findings-merge endpoint puts on the wire once host-filed findings — CI\n * failures, host observations, a revocation reason — are merged in alongside\n * the gate's own verdict. A gate-only finding still omits them entirely\n * (this CLI's own `validate`/`publish` findings never set `origin`, which\n * the renderer reads as \"gate\"); they only appear on findings read back from\n * `GET /api/partner/submissions?detail=findings`.\n */\nexport interface GateFinding {\n check: string;\n severity: 'error' | 'warn' | 'info';\n file?: string;\n line?: number;\n message: string;\n hint: string;\n /** `'gate' | 'ci' | 'host' | 'revocation'` on the wire; kept as `string` — see the doc comment on `HostFindingRow` in `merge-findings.ts` for why the host stays defensive rather than a narrower literal union here. */\n origin?: string;\n id?: string;\n createdAt?: string;\n resolvedAt?: string | null;\n}\n\nexport interface GateStageResult {\n id: string;\n status: 'pass' | 'fail' | 'skipped';\n durationMs: number;\n}\n\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\n gatedAt: string | null;\n /** Present once a gate has run. */\n errorCount: number | null;\n warnCount: number | null;\n /** Present only when the request passed `?detail=findings`. */\n findings?: GateFinding[];\n stages?: GateStageResult[];\n}\n\nexport interface SubmissionListing {\n id: string;\n slug: string;\n name: string | null;\n createdAt: string | null;\n versions: SubmissionVersion[];\n}\n\nexport type ListResult =\n | { status: 'ok'; integrations: SubmissionListing[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the caller's visible submissions for a source. Same authorization model\n * as `submitArchive`: the seat SELECT policies filter server-side, so a caller\n * with no seat sees an empty list rather than an error.\n *\n * `detail: 'findings'` requests the full per-version findings/stages arrays\n * (used by `status --findings` and `status --watch`); omitted, the server\n * answers with counts only — see the wire contract above.\n */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n options: { detail?: 'findings' } = {},\n): Promise<ListResult> {\n const query = new URLSearchParams({ source });\n if (options.detail) query.set('detail', options.detail);\n\n const response = await request(\n `${host}/api/partner/submissions?${query.toString()}`,\n {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n },\n );\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ?? `${host} refused to list submissions for \"${source}\".`,\n `Complete MFA in the browser if the message names it, then run ` +\n `\"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${source}\".`,\n `Check the \"source\" field in ekanos.json (or --source) against the ` +\n `slug your Ekanos operator gave you.`,\n );\n }\n\n if (response.status === 400) {\n throw validationError(\n serverMessage ?? `${host} rejected the submissions query.`,\n `Fix the reported problem and re-run \"ekanos status\".`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.integrations) ? data.integrations : [];\n\n const integrations = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n createdAt: str(row.createdAt),\n versions: (Array.isArray(row.versions) ? row.versions : [])\n .filter(\n (version): version is Record<string, unknown> =>\n typeof version === 'object' && version !== null,\n )\n .map((version) => ({\n id: str(version.id) ?? '',\n version: str(version.version) ?? '',\n state: str(version.state) ?? '',\n archiveSha256: str(version.archiveSha256),\n submittedAt: str(version.submittedAt),\n gatedAt: str(version.gatedAt),\n errorCount: num(version.errorCount),\n warnCount: num(version.warnCount),\n ...(Array.isArray(version.findings)\n ? { findings: version.findings as GateFinding[] }\n : {}),\n ...(Array.isArray(version.stages)\n ? { stages: version.stages as GateStageResult[] }\n : {}),\n })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(env: AuthEnvironment): never {\n throw authRequiredError(\n `The session for ${env.host} could not authorize the submission.`,\n loginHint(env),\n );\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\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\nfunction num(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n"]}
1
+ {"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/ekanos-api';\nimport { loginHint } from './auth/host-trust';\nimport type { AuthEnvironment } from './auth/session';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for the Ekanos submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Ekanos web app.\n *\n * The server side is the host's `/api/partner/submissions` route. As with\n * the auth routes (see `auth/ekanos-api.ts`), the two halves are coupled\n * only by HTTP; tests stub `fetch`, never the route.\n *\n * POST /api/partner/submissions [Authorization: Bearer <access_token>]\n * ← multipart/form-data:\n * source — Ekanos source slug\n * slug — integration (or experience) slug (kebab-case; bare, never namespaced)\n * version — semver\n * kind — \"experience\" for a composed experience; omitted for an\n * integration (an older host never sees the field). Sent\n * only to a host that advertises\n * `capabilities.experienceSubmissions` on the toolchain\n * route.\n * manifest — JSON string (the parsed ekanos.json + declaration\n * summary; for an experience, its built manifest.json)\n * archive — gzipped tarball, ≤ 4 MiB\n * availability — \"global\" | \"restricted\"; sent only when ekanos.json\n * declares it\n * → 201 { ok: true, data: { submissionId, slug, version, state,\n * archiveSha256,\n * // newer hosts only:\n * namespace?: string|null, displayName? } }\n * → 400 { error, code: \"INVALID_REQUEST\" | \"INVALID_ARCHIVE\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"FORBIDDEN\" } no developer seat on the source\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 409 { error, code: \"VERSION_EXISTS\" } this (slug, version) is\n * already submitted\n * → 409 { error, code: \"KIND_MISMATCH\" } the slug is registered as\n * the other kind\n * → 409 { error, code: \"GLOBAL_NAME_UNAPPROVED\", hint? }\n * availability \"global\" without\n * an approved global name\n * → 409 { error, code: \"NAMESPACE_REQUIRED\" } the source has no namespace\n * → 413 { error, code: \"ARCHIVE_TOO_LARGE\" }\n * → 3xx treated as 401 (an\n * auth-gated Ekanos page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id>[&detail=findings]\n * [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, namespace?: string|null, displayName?, versions: [{ id, version, state, archiveSha256,\n * submittedAt, gatedAt,\n * // present once a gate has run (gate_report is non-null):\n * errorCount?, warnCount?,\n * // present ONLY with ?detail=findings, alongside the above:\n * findings?, stages?\n * }] }] } }\n * → 400 { error, code: \"INVALID_REQUEST\" }\n * → 401 { error, code: \"AUTH_REQUIRED\" } refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 404 { error, code: \"SOURCE_NOT_FOUND\" }\n * → 3xx treated as 401\n *\n * `state` walks submitted -> gated|rejected -> dev -> prod -> revoked (a\n * `dev` version is marked `superseded` once a newer version reaches dev),\n * enforced by a database trigger on the host's side (the full transition\n * matrix lives there). `gated`/`dev`/`rejected` are the\n * terminal-for-waiting states `ekanos status --watch` polls for.\n * ---------------------------------------------------------------------------\n */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced server-side against\n * `Buffer.byteLength`. Mirrored here so `publish` can refuse an oversized\n * manifest with a clear message instead of the server's generic 400.\n */\nexport const MAX_MANIFEST_BYTES = 64 * 1024;\n\nexport interface SubmissionPayload {\n source: string;\n slug: string;\n /** Omitted for an integration. */\n kind?: 'experience';\n version: string;\n /** JSON string — already serialized by the caller. */\n manifest: string;\n archive: Uint8Array;\n /** Sent only when ekanos.json declares an availability mode. */\n availability?: 'global' | 'restricted';\n}\n\nexport interface SubmissionReceipt {\n submissionId: string;\n slug: string;\n version: string;\n state: string;\n archiveSha256: string;\n /** The Source namespace the package is named under; absent on an older host. */\n namespace?: string | null;\n /** The full name the host uses (`acme/google-drive`); absent on an older host. */\n displayName?: string;\n}\n\nexport type SubmitResult =\n | { status: 'ok'; receipt: SubmissionReceipt }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\nexport async function submitArchive(\n host: string,\n accessToken: string,\n payload: SubmissionPayload,\n): Promise<SubmitResult> {\n const form = new FormData();\n form.append('source', payload.source);\n form.append('slug', payload.slug);\n form.append('version', payload.version);\n if (payload.kind) form.append('kind', payload.kind);\n form.append('manifest', payload.manifest);\n if (payload.availability) form.append('availability', payload.availability);\n form.append(\n 'archive',\n // Copy into a fresh Uint8Array: Buffer is Uint8Array<ArrayBufferLike>,\n // which BlobPart refuses (a SharedArrayBuffer view is not a BlobPart).\n new Blob([new Uint8Array(payload.archive)], { type: 'application/gzip' }),\n 'archive.tgz',\n );\n\n const response = await request(`${host}/api/partner/submissions`, {\n method: 'POST',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n body: form,\n });\n\n // 401, or the 307 an auth-gated Ekanos route answers with: the token did\n // not authenticate. The caller owns the refresh-and-retry decision.\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ??\n `${host} refused the submission: this account does not hold the ` +\n `developer seat on source \"${payload.source}\".`,\n `Ask the source operator to grant your account the developer seat for ` +\n `\"${payload.source}\", then retry.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${payload.source}\".`,\n `Check the --source value (or the \"source\" field in ekanos.json) ` +\n `against the slug your Ekanos operator gave you.`,\n );\n }\n\n if (response.status === 409 && str(body.code) === 'KIND_MISMATCH') {\n throw invalidStateError(\n serverMessage ??\n `\"${payload.slug}\" is registered as the other kind of package.`,\n `A slug names one kind for its whole life. Pick a different slug, or ` +\n `submit it as the kind it was first registered as.`,\n );\n }\n\n if (response.status === 409 && body.code === 'GLOBAL_NAME_UNAPPROVED') {\n throw globalNameUnapprovedError(payload.slug);\n }\n\n if (response.status === 409 && body.code === 'NAMESPACE_REQUIRED') {\n throw invalidStateError(\n serverMessage ??\n `Source \"${payload.source}\" has no namespace to name \"${payload.slug}\" under.`,\n str(body.hint) ??\n `Ask an Ekanos super-admin to give \"${payload.source}\" a namespace, ` +\n `then retry.`,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n serverMessage ??\n `Version ${payload.version} of \"${payload.slug}\" is already submitted.`,\n `Submitted versions are immutable. Bump the \"version\" field in ` +\n `package.json and publish again.`,\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw validationError(\n serverMessage ?? `${host} rejected the submission as invalid.`,\n `Fix the reported problem and re-run \"ekanos publish\". If the message ` +\n `names the archive, re-check the project layout (\"ekanos validate\").`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const receipt = {\n submissionId: str(data.submissionId),\n slug: str(data.slug),\n version: str(data.version),\n state: str(data.state),\n archiveSha256: str(data.archiveSha256),\n };\n const displayName = str(data.displayName);\n\n if (Object.values(receipt).some((value) => value === null)) {\n throw networkError(\n `${host} accepted the submission but returned an incomplete receipt.`,\n `Confirm the deployment is running an Ekanos build that includes the ` +\n `partner submissions route, then check the submission's state with ` +\n `your operator before re-publishing.`,\n );\n }\n\n return {\n status: 'ok',\n receipt: {\n ...(receipt as SubmissionReceipt),\n ...(data.namespace === null || typeof data.namespace === 'string'\n ? { namespace: data.namespace }\n : {}),\n ...(displayName ? { displayName } : {}),\n },\n };\n}\n\n/**\n * The refusal for a `global` package whose name no super-admin has approved.\n * Shared by the pre-flight (`publish.ts`) and the 409 the host answers with.\n */\nexport function globalNameUnapprovedError(slug: string) {\n return invalidStateError(\n `The global name \"${slug}\" is not approved.`,\n `Run \"ekanos names claim ${slug}\" to request it, or publish restricted ` +\n `(remove availability.mode \"global\" from ekanos.json, or set it to ` +\n `\"restricted\").`,\n );\n}\n\n/**\n * One gate finding, mirroring `@ekanos/integration-schema`'s `Finding` shape\n * (the same one `validate`/`publish` carry in `data.findings`). Declared\n * locally rather than imported: the gate's own report type lives in a\n * server-side toolchain package private to the platform monorepo, which this\n * partner-facing CLI has no business depending on — the wire shape is the\n * only contract.\n *\n * `origin`/`id`/`createdAt`/`resolvedAt` are the additive fields the host's\n * findings-merge endpoint puts on the wire once host-filed findings — CI\n * failures, host observations, a revocation reason — are merged in alongside\n * the gate's own verdict. A gate-only finding still omits them entirely\n * (this CLI's own `validate`/`publish` findings never set `origin`, which\n * the renderer reads as \"gate\"); they only appear on findings read back from\n * `GET /api/partner/submissions?detail=findings`.\n */\nexport interface GateFinding {\n check: string;\n severity: 'error' | 'warn' | 'info';\n file?: string;\n line?: number;\n message: string;\n hint: string;\n /** `'gate' | 'ci' | 'host' | 'revocation'` on the wire; kept as `string` — see the doc comment on `HostFindingRow` in `merge-findings.ts` for why the host stays defensive rather than a narrower literal union here. */\n origin?: string;\n id?: string;\n createdAt?: string;\n resolvedAt?: string | null;\n}\n\nexport interface GateStageResult {\n id: string;\n status: 'pass' | 'fail' | 'skipped';\n durationMs: number;\n}\n\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\n gatedAt: string | null;\n /** Present once a gate has run. */\n errorCount: number | null;\n warnCount: number | null;\n /** Present only when the request passed `?detail=findings`. */\n findings?: GateFinding[];\n stages?: GateStageResult[];\n}\n\nexport interface SubmissionListing {\n id: string;\n slug: string;\n name: string | null;\n createdAt: string | null;\n /** Absent on an older host. */\n namespace?: string | null;\n displayName?: string;\n versions: SubmissionVersion[];\n}\n\nexport type ListResult =\n | { status: 'ok'; integrations: SubmissionListing[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the caller's visible submissions for a source. Same authorization model\n * as `submitArchive`: the seat SELECT policies filter server-side, so a caller\n * with no seat sees an empty list rather than an error.\n *\n * `detail: 'findings'` requests the full per-version findings/stages arrays\n * (used by `status --findings` and `status --watch`); omitted, the server\n * answers with counts only — see the wire contract above.\n */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n options: { detail?: 'findings' } = {},\n): Promise<ListResult> {\n const query = new URLSearchParams({ source });\n if (options.detail) query.set('detail', options.detail);\n\n const response = await request(\n `${host}/api/partner/submissions?${query.toString()}`,\n {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n },\n );\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const serverMessage = str(body.error);\n\n if (response.status === 403) {\n throw forbiddenError(\n serverMessage ?? `${host} refused to list submissions for \"${source}\".`,\n `Complete MFA in the browser if the message names it, then run ` +\n `\"ekanos login\" again.`,\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n serverMessage ?? `${host} has no source with slug \"${source}\".`,\n `Check the \"source\" field in ekanos.json (or --source) against the ` +\n `slug your Ekanos operator gave you.`,\n );\n }\n\n if (response.status === 400) {\n throw validationError(\n serverMessage ?? `${host} rejected the submissions query.`,\n `Fix the reported problem and re-run \"ekanos status\".`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/submissions responded ${response.status}.`,\n response.status >= 500\n ? `The Ekanos deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is an Ekanos deployment running a build that ` +\n `includes the partner submissions route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.integrations) ? data.integrations : [];\n\n const integrations = rows\n .filter(\n (row): row is Record<string, unknown> =>\n typeof row === 'object' && row !== null,\n )\n .map((row) => ({\n id: str(row.id) ?? '',\n slug: str(row.slug) ?? '',\n name: str(row.name),\n createdAt: str(row.createdAt),\n ...(row.namespace === null || typeof row.namespace === 'string'\n ? { namespace: row.namespace }\n : {}),\n ...(str(row.displayName) ? { displayName: str(row.displayName)! } : {}),\n versions: (Array.isArray(row.versions) ? row.versions : [])\n .filter(\n (version): version is Record<string, unknown> =>\n typeof version === 'object' && version !== null,\n )\n .map((version) => ({\n id: str(version.id) ?? '',\n version: str(version.version) ?? '',\n state: str(version.state) ?? '',\n archiveSha256: str(version.archiveSha256),\n submittedAt: str(version.submittedAt),\n gatedAt: str(version.gatedAt),\n errorCount: num(version.errorCount),\n warnCount: num(version.warnCount),\n ...(Array.isArray(version.findings)\n ? { findings: version.findings as GateFinding[] }\n : {}),\n ...(Array.isArray(version.stages)\n ? { stages: version.stages as GateStageResult[] }\n : {}),\n })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(env: AuthEnvironment): never {\n throw authRequiredError(\n `The session for ${env.host} could not authorize the submission.`,\n loginHint(env),\n );\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\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n\nfunction num(value: unknown): number | null {\n return typeof value === 'number' && Number.isFinite(value) ? value : null;\n}\n"]}
@@ -27,6 +27,8 @@ export interface PackageRow {
27
27
  findings?: Finding[];
28
28
  /** Integration submissions: what the host answered. */
29
29
  receipt?: Record<string, unknown>;
30
+ /** The full name the host gave a submitted integration (`acme/google-drive`). */
31
+ displayName?: string;
30
32
  }
31
33
  /**
32
34
  * `ekanos publish [name...] | --all [--dry-run]` in a repo.
@@ -35,8 +37,9 @@ export interface PackageRow {
35
37
  * use them (a same-repo integration is not an override, so it is ordered
36
38
  * first, never refused). Every package passes its own gate before anything is
37
39
  * sent. `--dry-run` also builds each artifact under `.ekanos/publish/` and
38
- * touches no network. A real run submits the integrations in order through the
39
- * existing path; the host cannot admit an experience yet, so selected
40
- * experiences end the run with that refusal.
40
+ * touches no network. A real run submits the packages in order through the
41
+ * existing path; an experience goes to a host that advertises experience
42
+ * submissions, and against an older host that cannot admit one the run ends
43
+ * with that refusal after the rest were submitted.
41
44
  */
42
45
  export declare function runRepoPublish(ctx: CliContext, args: RepoPublishArgs): Promise<ExitCode>;
@@ -3,13 +3,19 @@ import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { buildManifest, escalateForPublish, prepareIntegrationSubmission, readProjectVersion, submitPrepared, } from '../commands/publish.js';
5
5
  import { CliError, gateFailedError, preconditionError, usageError, validationError, } from '../errors.js';
6
- import { EXPERIENCE_NOT_SUPPORTED, gateExperience, packExperience, } from '../experience/publish.js';
6
+ import { EXPERIENCE_NOT_SUPPORTED, gateExperience, packExperience, submitExperience, } from '../experience/publish.js';
7
7
  import { packProject } from '../pack.js';
8
8
  import { loadProject } from '../project.js';
9
9
  import { shortHost } from '../ui.js';
10
10
  import { collectAllFindings } from '../validate-findings.js';
11
11
  export const PUBLISH_ARTIFACT_DIR = path.join('.ekanos', 'publish');
12
- const AVAILABILITY_NOTE = 'availability is set by a super admin today; the host does not take it at submission';
12
+ /** What an integration's declared availability means at submission. */
13
+ function availabilityNote(pkg) {
14
+ var _a;
15
+ return ((_a = pkg.availability) === null || _a === void 0 ? void 0 : _a.mode) === 'global'
16
+ ? `needs an approved global name (run "ekanos names claim ${pkg.name}"); without one the publish is refused`
17
+ : 'publishes restricted, named under your Source namespace';
18
+ }
13
19
  function loadRepo(root) {
14
20
  try {
15
21
  return discoverRepo(root);
@@ -40,12 +46,12 @@ function artifactName(pkg) {
40
46
  return `${pkg.name}-${pkg.version}`;
41
47
  }
42
48
  function renderTable(ctx, rows) {
43
- return ctx.ui.table(['package', 'kind', 'version', 'availability', 'artifact', 'status'], rows.map((row) => { var _a; return [
44
- row.name,
49
+ return ctx.ui.table(['package', 'kind', 'version', 'availability', 'artifact', 'status'], rows.map((row) => { var _a, _b; return [
50
+ (_a = row.displayName) !== null && _a !== void 0 ? _a : row.name,
45
51
  row.kind,
46
52
  row.version,
47
53
  row.availability,
48
- (_a = row.artifact) !== null && _a !== void 0 ? _a : '-',
54
+ (_b = row.artifact) !== null && _b !== void 0 ? _b : '-',
49
55
  row.detail ? `${row.status}: ${row.detail}` : row.status,
50
56
  ]; }));
51
57
  }
@@ -129,9 +135,10 @@ async function checkExperience(pkg, repoRoot, build, env) {
129
135
  * use them (a same-repo integration is not an override, so it is ordered
130
136
  * first, never refused). Every package passes its own gate before anything is
131
137
  * sent. `--dry-run` also builds each artifact under `.ekanos/publish/` and
132
- * touches no network. A real run submits the integrations in order through the
133
- * existing path; the host cannot admit an experience yet, so selected
134
- * experiences end the run with that refusal.
138
+ * touches no network. A real run submits the packages in order through the
139
+ * existing path; an experience goes to a host that advertises experience
140
+ * submissions, and against an older host that cannot admit one the run ends
141
+ * with that refusal after the rest were submitted.
135
142
  */
136
143
  export async function runRepoPublish(ctx, args) {
137
144
  const repo = loadRepo(args.repoRoot);
@@ -150,7 +157,7 @@ export async function runRepoPublish(ctx, args) {
150
157
  const warnings = plan.unselectedDependencies.map((dep) => `${dep.experience} uses ${dep.integration}, which is in this repo but not in this publish: publish ${dep.integration} first, or make sure it is already published at a compatible version.`);
151
158
  const notes = plan.order.flatMap((pkg) => pkg.kind === 'integration' && pkg.availability
152
159
  ? [
153
- `${pkg.name}: ${describeAvailability(pkg.availability)} - ${AVAILABILITY_NOTE}.`,
160
+ `${pkg.name}: ${describeAvailability(pkg.availability)} - ${availabilityNote(pkg)}.`,
154
161
  ]
155
162
  : []);
156
163
  if (!ctx.jsonMode) {
@@ -183,14 +190,38 @@ export async function runRepoPublish(ctx, args) {
183
190
  let host = null;
184
191
  for (const [index, pkg] of plan.order.entries()) {
185
192
  const row = rows[index];
186
- if (pkg.kind === 'experience') {
187
- row.status = 'not supported by the host';
188
- continue;
189
- }
190
193
  if (stop) {
191
194
  row.status = 'not attempted';
192
195
  continue;
193
196
  }
197
+ if (pkg.kind === 'experience') {
198
+ try {
199
+ const single = Object.assign(Object.assign({}, args.publish), { dir: pkg.dir });
200
+ const gate = await gateExperience({
201
+ dir: pkg.dir,
202
+ dryRun: false,
203
+ env: args.publish.env,
204
+ });
205
+ const submission = await submitExperience(ctx, Object.assign(Object.assign({}, single), { dryRun: false }), gate);
206
+ if (submission.status === 'unsupported') {
207
+ host = submission.host;
208
+ row.status = 'not supported by the host';
209
+ continue;
210
+ }
211
+ row.status = 'submitted';
212
+ row.receipt = submission.data;
213
+ if (!ctx.jsonMode)
214
+ ctx.log(submission.human);
215
+ }
216
+ catch (error) {
217
+ if (!(error instanceof CliError))
218
+ throw error;
219
+ row.status = 'failed';
220
+ row.detail = error.message;
221
+ stop = error;
222
+ }
223
+ continue;
224
+ }
194
225
  try {
195
226
  const single = Object.assign(Object.assign({}, args.publish), { dir: pkg.dir });
196
227
  const prepared = await prepareIntegrationSubmission(ctx, single);
@@ -198,6 +229,9 @@ export async function runRepoPublish(ctx, args) {
198
229
  const submitted = await submitPrepared(ctx, single, prepared);
199
230
  row.status = 'submitted';
200
231
  row.receipt = submitted.data;
232
+ if (typeof submitted.data.displayName === 'string') {
233
+ row.displayName = submitted.data.displayName;
234
+ }
201
235
  if (!ctx.jsonMode)
202
236
  ctx.log(submitted.human);
203
237
  }
@@ -218,7 +252,7 @@ export async function runRepoPublish(ctx, args) {
218
252
  if (rows.some((row) => row.status === 'not supported by the host')) {
219
253
  if (!ctx.jsonMode)
220
254
  ctx.log(summary(`Submitted${hostNote}.`));
221
- return ctx.fail(gateFailedError(EXPERIENCE_NOT_SUPPORTED, 'The integrations in this publish were submitted; run "ekanos publish --dry-run" to build and pack the experiences. The host has no admission path for experiences until it ships one.'), data({ published: true }));
255
+ return ctx.fail(gateFailedError(EXPERIENCE_NOT_SUPPORTED, 'The other packages in this publish were submitted; run "ekanos publish --dry-run" to build and pack the experiences. This host has no admission path for experiences until it ships one.'), data({ published: true }));
222
256
  }
223
257
  return ctx.succeed(data({ published: true }), summary(`Submitted ${rows.length} package${rows.length === 1 ? '' : 's'}${hostNote}.`));
224
258
  }