@ekanos/cli 0.1.7 → 0.1.9

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.
@@ -29,15 +29,26 @@
29
29
  * auth-gated Fusion page
30
30
  * answers 307)
31
31
  *
32
- * GET /api/partner/submissions?source=<slug|id> [Authorization: Bearer]
32
+ * GET /api/partner/submissions?source=<slug|id>[&detail=findings]
33
+ * [Authorization: Bearer]
33
34
  * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,
34
35
  * createdAt, versions: [{ id, version, state, archiveSha256,
35
- * submittedAt }] }] } }
36
+ * submittedAt, gatedAt,
37
+ * // present once a gate has run (gate_report is non-null):
38
+ * errorCount?, warnCount?,
39
+ * // present ONLY with ?detail=findings, alongside the above:
40
+ * findings?, stages?
41
+ * }] }] } }
36
42
  * → 400 { error, code: "INVALID_REQUEST" }
37
43
  * → 401 { error, code: "AUTH_REQUIRED" } refresh once and retry
38
44
  * → 403 { error, code: "MFA_REQUIRED" }
39
45
  * → 404 { error, code: "SOURCE_NOT_FOUND" }
40
46
  * → 3xx treated as 401
47
+ *
48
+ * `state` walks submitted -> gated|rejected -> dev -> prod -> revoked (see
49
+ * the trigger in apps/web/supabase/schemas/110-partner-submissions.sql for
50
+ * the full transition matrix). `gated`/`dev`/`rejected` are the
51
+ * terminal-for-waiting states `ekanos status --watch` polls for.
41
52
  * ---------------------------------------------------------------------------
42
53
  */
43
54
  /**
@@ -71,12 +82,39 @@ export type SubmitResult = {
71
82
  status: 'unauthorized';
72
83
  };
73
84
  export declare function submitArchive(host: string, accessToken: string, payload: SubmissionPayload): Promise<SubmitResult>;
85
+ /**
86
+ * One gate finding, mirroring `@ekanos/integration-schema`'s `Finding` shape
87
+ * (the same one `validate`/`publish` carry in `data.findings`). Declared
88
+ * locally rather than imported: the gate's own report type lives in
89
+ * `@ekanos/checks`, a server-side/toolchain package this partner-facing CLI
90
+ * has no business depending on — the wire shape is the only contract.
91
+ */
92
+ export interface GateFinding {
93
+ check: string;
94
+ severity: 'error' | 'warn' | 'info';
95
+ file?: string;
96
+ line?: number;
97
+ message: string;
98
+ hint: string;
99
+ }
100
+ export interface GateStageResult {
101
+ id: string;
102
+ status: 'pass' | 'fail' | 'skipped';
103
+ durationMs: number;
104
+ }
74
105
  export interface SubmissionVersion {
75
106
  id: string;
76
107
  version: string;
77
108
  state: string;
78
109
  archiveSha256: string | null;
79
110
  submittedAt: string | null;
111
+ gatedAt: string | null;
112
+ /** Present once a gate has run. */
113
+ errorCount: number | null;
114
+ warnCount: number | null;
115
+ /** Present only when the request passed `?detail=findings`. */
116
+ findings?: GateFinding[];
117
+ stages?: GateStageResult[];
80
118
  }
81
119
  export interface SubmissionListing {
82
120
  id: string;
@@ -97,7 +135,13 @@ export type ListResult = {
97
135
  * List the caller's visible submissions for a source. Same authorization model
98
136
  * as `submitArchive`: the seat SELECT policies filter server-side, so a caller
99
137
  * with no seat sees an empty list rather than an error.
138
+ *
139
+ * `detail: 'findings'` requests the full per-version findings/stages arrays
140
+ * (used by `status --findings` and `status --watch`); omitted, the server
141
+ * answers with counts only — see the wire contract above.
100
142
  */
101
- export declare function listSubmissions(host: string, accessToken: string, source: string): Promise<ListResult>;
143
+ export declare function listSubmissions(host: string, accessToken: string, source: string, options?: {
144
+ detail?: 'findings';
145
+ }): Promise<ListResult>;
102
146
  /** Guard for the "still unauthorized after one refresh" terminal case. */
103
147
  export declare function stillUnauthorized(host: string): never;
@@ -31,15 +31,26 @@ import { authRequiredError, forbiddenError, invalidStateError, networkError, not
31
31
  * auth-gated Fusion page
32
32
  * answers 307)
33
33
  *
34
- * GET /api/partner/submissions?source=<slug|id> [Authorization: Bearer]
34
+ * GET /api/partner/submissions?source=<slug|id>[&detail=findings]
35
+ * [Authorization: Bearer]
35
36
  * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,
36
37
  * createdAt, versions: [{ id, version, state, archiveSha256,
37
- * submittedAt }] }] } }
38
+ * submittedAt, gatedAt,
39
+ * // present once a gate has run (gate_report is non-null):
40
+ * errorCount?, warnCount?,
41
+ * // present ONLY with ?detail=findings, alongside the above:
42
+ * findings?, stages?
43
+ * }] }] } }
38
44
  * → 400 { error, code: "INVALID_REQUEST" }
39
45
  * → 401 { error, code: "AUTH_REQUIRED" } refresh once and retry
40
46
  * → 403 { error, code: "MFA_REQUIRED" }
41
47
  * → 404 { error, code: "SOURCE_NOT_FOUND" }
42
48
  * → 3xx treated as 401
49
+ *
50
+ * `state` walks submitted -> gated|rejected -> dev -> prod -> revoked (see
51
+ * the trigger in apps/web/supabase/schemas/110-partner-submissions.sql for
52
+ * the full transition matrix). `gated`/`dev`/`rejected` are the
53
+ * terminal-for-waiting states `ekanos status --watch` polls for.
43
54
  * ---------------------------------------------------------------------------
44
55
  */
45
56
  /**
@@ -118,10 +129,17 @@ export async function submitArchive(host, accessToken, payload) {
118
129
  * List the caller's visible submissions for a source. Same authorization model
119
130
  * as `submitArchive`: the seat SELECT policies filter server-side, so a caller
120
131
  * with no seat sees an empty list rather than an error.
132
+ *
133
+ * `detail: 'findings'` requests the full per-version findings/stages arrays
134
+ * (used by `status --findings` and `status --watch`); omitted, the server
135
+ * answers with counts only — see the wire contract above.
121
136
  */
122
- export async function listSubmissions(host, accessToken, source) {
137
+ export async function listSubmissions(host, accessToken, source, options = {}) {
123
138
  var _a;
124
- const response = await request(`${host}/api/partner/submissions?source=${encodeURIComponent(source)}`, {
139
+ const query = new URLSearchParams({ source });
140
+ if (options.detail)
141
+ query.set('detail', options.detail);
142
+ const response = await request(`${host}/api/partner/submissions?${query.toString()}`, {
125
143
  method: 'GET',
126
144
  headers: {
127
145
  accept: 'application/json',
@@ -166,13 +184,11 @@ export async function listSubmissions(host, accessToken, source) {
166
184
  .filter((version) => typeof version === 'object' && version !== null)
167
185
  .map((version) => {
168
186
  var _a, _b, _c;
169
- return ({
170
- id: (_a = str(version.id)) !== null && _a !== void 0 ? _a : '',
171
- version: (_b = str(version.version)) !== null && _b !== void 0 ? _b : '',
172
- state: (_c = str(version.state)) !== null && _c !== void 0 ? _c : '',
173
- archiveSha256: str(version.archiveSha256),
174
- submittedAt: str(version.submittedAt),
175
- });
187
+ 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)
188
+ ? { findings: version.findings }
189
+ : {})), (Array.isArray(version.stages)
190
+ ? { stages: version.stages }
191
+ : {})));
176
192
  }),
177
193
  });
178
194
  });
@@ -199,4 +215,7 @@ function isRedirect(status) {
199
215
  function str(value) {
200
216
  return typeof value === 'string' && value.length > 0 ? value : null;
201
217
  }
218
+ function num(value) {
219
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
220
+ }
202
221
  //# sourceMappingURL=publish-api.js.map
@@ -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/fusion-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 the Fusion submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/submissions/route.ts`.\n * As with the auth routes (see `auth/fusion-api.ts`), the two halves are\n * coupled 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 — Fusion 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 Fusion page\n * answers 307)\n *\n * GET /api/partner/submissions?source=<slug|id> [Authorization: Bearer]\n * → 200 { ok: true, data: { source, integrations: [{ id, slug, name,\n * createdAt, versions: [{ id, version, state, archiveSha256,\n * submittedAt }] }] } }\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 */\n\n/**\n * The server's manifest cap — 64 KiB of UTF-8, enforced in\n * `apps/web/app/api/partner/submissions/_lib/submission-validation.ts` 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 Fusion 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 Fusion 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 Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion 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 a Fusion 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\nexport interface SubmissionVersion {\n id: string;\n version: string;\n state: string;\n archiveSha256: string | null;\n submittedAt: string | null;\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 */\nexport async function listSubmissions(\n host: string,\n accessToken: string,\n source: string,\n): Promise<ListResult> {\n const response = await request(\n `${host}/api/partner/submissions?source=${encodeURIComponent(source)}`,\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 --host ${host}\" 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 Fusion 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 Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion 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 })),\n }));\n\n return { status: 'ok', integrations };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorized(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the submission.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\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"]}
1
+ {"version":3,"file":"publish-api.js","sourceRoot":"","sources":["../src/publish-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { request } from './auth/fusion-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 the Fusion submission intake route.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the seam between this package and the Fusion web app.\n *\n * The server side lives in `apps/web/app/api/partner/submissions/route.ts`.\n * As with the auth routes (see `auth/fusion-api.ts`), the two halves are\n * coupled 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 — Fusion 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 Fusion 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 (see\n * the trigger in apps/web/supabase/schemas/110-partner-submissions.sql for\n * the full transition matrix). `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 in\n * `apps/web/app/api/partner/submissions/_lib/submission-validation.ts` 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 Fusion 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 Fusion 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 Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion 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 a Fusion 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\n * `@ekanos/checks`, a server-side/toolchain package this partner-facing CLI\n * has no business depending on — the wire shape is the only contract.\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}\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 --host ${host}\" 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 Fusion 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 Fusion deployment returned a server error. Retry shortly; if ` +\n `it persists, report it with the status code.`\n : `Confirm the host is a Fusion 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(host: string): never {\n throw authRequiredError(\n `The session for ${host} could not authorize the submission.`,\n `Run \"ekanos login --host ${host}\" and retry.`,\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,5 +1,6 @@
1
1
  import { collectCollisionFindings, collectDefinitionFindings, } from '@ekanos/integration-schema';
2
2
  import { preconditionError } from './errors.js';
3
+ import { collectIconFindings } from './icon-findings.js';
3
4
  import { loadDefinition } from './load-definition.js';
4
5
  import { collectDocsFindings, collectProjectFindings } from './project-checks.js';
5
6
  /**
@@ -56,6 +57,7 @@ export async function collectAllFindings(loaded) {
56
57
  findings.push(...collectCollisionFindings(definitions));
57
58
  findings.push(...collectProjectFindings(loaded.projectDir));
58
59
  findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));
60
+ findings.push(...collectIconFindings(loaded.projectDir));
59
61
  return findings;
60
62
  }
61
63
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,wBAAwB,EACxB,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAAE,mBAAmB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAE/E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,MAAqB;IAErB,MAAM,QAAQ,GAAc,EAAE,CAAC;IAC/B,MAAM,WAAW,GAA+B,EAAE,CAAC;IAEnD,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QAC9C,MAAM,MAAM,GAAG,MAAM,cAAc,CACjC,WAAW,CAAC,SAAS,EACrB,MAAM,CAAC,UAAU,CAClB,CAAC;QAEF,IAAI,MAAM,CAAC,EAAE,EAAE,CAAC;YACd,QAAQ,CAAC,IAAI,CACX,GAAG,yBAAyB,CAAC,MAAM,CAAC,UAAU,EAAE;gBAC9C,IAAI,EAAE,WAAW,CAAC,SAAS;aAC5B,CAAC,CACH,CAAC;YACF,QAAQ,CAAC,IAAI,CACX,GAAG,4BAA4B,CAC7B,WAAW,EACX,MAAM,CAAC,UAAgC,CACxC,CACF,CAAC;YACF,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,UAAsC,CAAC,CAAC;YAChE,SAAS;QACX,CAAC;QAED,IAAI,MAAM,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;YAC/B,yEAAyE;YACzE,iEAAiE;YACjE,QAAQ,CAAC,IAAI,CAAC;gBACZ,KAAK,EAAE,iBAAiB;gBACxB,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,WAAW,CAAC,SAAS;gBAC3B,OAAO,EAAE,MAAM,CAAC,OAAO;gBACvB,IAAI,EACF,oEAAoE;oBACpE,uBAAuB;aAC1B,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,kEAAkE;QAClE,MAAM,iBAAiB,CACrB,kDAAkD,WAAW,CAAC,IAAI,KAAK;YACrE,MAAM,CAAC,OAAO,EAChB,oEAAoE;YAClE,6BAA6B,CAChC,CAAC;IACJ,CAAC;IAED,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,CAAC,GAAG,wBAAwB,CAAC,WAAW,CAAC,CAAC,CAAC;IACxD,QAAQ,CAAC,IAAI,CAAC,GAAG,sBAAsB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;IAC5D,QAAQ,CAAC,IAAI,CAAC,GAAG,mBAAmB,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC;IAEzE,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,4BAA4B,CACnC,WAA+D,EAC/D,UAA8B;IAE9B,MAAM,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC;IACjC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,WAAW,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAE7E,OAAO;QACL;YACE,KAAK,EAAE,wBAAwB;YAC/B,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,WAAW,CAAC,SAAS;YAC3B,OAAO,EACL,8BAA8B,WAAW,CAAC,IAAI,wBAAwB;gBACtE,wBAAwB,QAAQ,4BAA4B;gBAC5D,sEAAsE;gBACtE,qCAAqC;YACvC,IAAI,EACF,sDAAsD,QAAQ,OAAO;gBACrE,+BAA+B,WAAW,CAAC,IAAI,OAAO;gBACtD,sBAAsB;SACzB;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\n\nimport { preconditionError } from './errors';\nimport { loadDefinition } from './load-definition';\nimport type { LoadedProject } from './project';\nimport { collectDocsFindings, collectProjectFindings } from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n *\n * NOTE on schema skew: an earlier version of this function rewrote the\n * unrecognized-key hint when the project's `@ekanos/integration-schema` was\n * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —\n * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`\n * or `publish` ever calls this function in that state, so a project-newer\n * finding can never reach here to be rewritten, and the precondition error\n * already names both versions and points at the fix. It was removed rather\n * than kept \"just in case\": if a future caller ever makes validate proceed\n * under skew instead of failing fast, THAT is when a rewrite here becomes\n * necessary again, and it should be reintroduced with a real caller driving\n * it, not tests that fake their own reachability.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n"]}
1
+ {"version":3,"file":"validate-findings.js","sourceRoot":"","sources":["../src/validate-findings.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n DefinitionCollisionInput,\n Finding,\n} from '@ekanos/integration-schema';\nimport {\n collectCollisionFindings,\n collectDefinitionFindings,\n} from '@ekanos/integration-schema';\n\nimport { preconditionError } from './errors';\nimport { collectIconFindings } from './icon-findings';\nimport { loadDefinition } from './load-definition';\nimport type { LoadedProject } from './project';\nimport { collectDocsFindings, collectProjectFindings } from './project-checks';\n\n/**\n * The full findings pass shared by `validate` (which reports them) and\n * `publish` (which refuses to submit on any error-severity finding). One\n * implementation so the publish gate can never drift from what `validate`\n * checks — a project that validates clean is, by construction, a project that\n * publish will accept locally.\n *\n * NOTE on schema skew: an earlier version of this function rewrote the\n * unrecognized-key hint when the project's `@ekanos/integration-schema` was\n * newer than the CLI's (see `schema-skew.ts`). That rewrite was dead code —\n * `assertSchemaNotSkewed` throws PRECONDITION_FAILED before either `validate`\n * or `publish` ever calls this function in that state, so a project-newer\n * finding can never reach here to be rewritten, and the precondition error\n * already names both versions and points at the fix. It was removed rather\n * than kept \"just in case\": if a future caller ever makes validate proceed\n * under skew instead of failing fast, THAT is when a rewrite here becomes\n * necessary again, and it should be reintroduced with a real caller driving\n * it, not tests that fake their own reachability.\n */\nexport async function collectAllFindings(\n loaded: LoadedProject,\n): Promise<Finding[]> {\n const findings: Finding[] = [];\n const definitions: DefinitionCollisionInput[] = [];\n\n for (const integration of loaded.integrations) {\n const result = await loadDefinition(\n integration.entryPath,\n loaded.projectDir,\n );\n\n if (result.ok) {\n findings.push(\n ...collectDefinitionFindings(result.definition, {\n file: integration.entryPath,\n }),\n );\n findings.push(\n ...collectSlugAgreementFindings(\n integration,\n result.definition as { slug?: unknown },\n ),\n );\n definitions.push(result.definition as DefinitionCollisionInput);\n continue;\n }\n\n if (result.kind === 'rejected') {\n // The module loaded but the SDK/schema rejected the definition at import\n // time — surface it as a validation finding rather than a crash.\n findings.push({\n check: 'definition.load',\n severity: 'error',\n file: integration.entryPath,\n message: result.message,\n hint:\n 'Fix the integration definition so defineIntegration() accepts it, ' +\n 'then re-run validate.',\n });\n continue;\n }\n\n // A genuine module-load failure is a precondition, not a finding.\n throw preconditionError(\n `Could not load the integration definition for \"${integration.slug}\": ` +\n result.message,\n 'Ensure the entry module and its installed dependencies load under ' +\n 'Node, then re-run validate.',\n );\n }\n\n // Cross-checks only mean something with more than one definition in hand.\n findings.push(...collectCollisionFindings(definitions));\n findings.push(...collectProjectFindings(loaded.projectDir));\n findings.push(...collectDocsFindings(loaded.project, loaded.projectDir));\n findings.push(...collectIconFindings(loaded.projectDir));\n\n return findings;\n}\n\n/**\n * `ekanos.json` and the definition each carry a slug, and nothing compared\n * them: a project could declare `something-else` while the definition said\n * `repo-activity` and validate would report `{ ok: true, findings: [] }`.\n *\n * That is the identifier every surface addresses the integration by — the\n * harness route, the product slug, the widget id prefix, the MCP server — so\n * two sources of truth disagreeing is not a style question. It is an error,\n * not a warning, for the same reason a silent default is worse than a loud\n * one: the failure it causes shows up somewhere else entirely.\n */\nfunction collectSlugAgreementFindings(\n integration: { slug: string; entry: string; entryPath: string },\n definition: { slug?: unknown },\n): Finding[] {\n const declared = definition.slug;\n if (typeof declared !== 'string' || declared === integration.slug) return [];\n\n return [\n {\n check: 'project.slug-agreement',\n severity: 'error',\n file: integration.entryPath,\n message:\n `ekanos.json declares slug \"${integration.slug}\" for this entry, but ` +\n `the definition says \"${declared}\". The slug addresses the ` +\n 'integration everywhere — its harness route, its product record, its ' +\n 'widget ids — so the two must agree.',\n hint:\n `Change one to match the other: either set \"slug\": \"${declared}\" in ` +\n `ekanos.json, or pass slug: '${integration.slug}' to ` +\n 'defineIntegration().',\n },\n ];\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekanos/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "The Ekanos partner toolchain CLI: scaffold, validate, and test a Fusion integration against the published @ekanos packages. Agent-native — every verb speaks JSON with a stable exit-code taxonomy.",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@ekanos/integration-schema": "0.1.4",
35
+ "@ekanos/integration-schema": "0.1.5",
36
36
  "esbuild": "0.28.1"
37
37
  },
38
38
  "peerDependencies": {
@@ -50,8 +50,10 @@
50
50
  "prettier": "@kit/prettier-config",
51
51
  "scripts": {
52
52
  "build": "tsc -p tsconfig.build.json && node scripts/rewrite-esm-specifiers.mjs",
53
+ "check:fa-free-names": "node scripts/generate-fa-free-names.mjs",
53
54
  "clean": "git clean -xdf .turbo node_modules dist",
54
55
  "format": "prettier --check \"**/*.{ts,tsx}\"",
56
+ "generate:fa-free-names": "node scripts/generate-fa-free-names.mjs --write",
55
57
  "lint": "eslint .",
56
58
  "typecheck": "tsc --noEmit",
57
59
  "test": "vitest run --config vitest.config.ts"
@@ -14,7 +14,9 @@
14
14
  },
15
15
  "devDependencies": {
16
16
  "@ekanos/cli": "^0.1.0",
17
+ "@ekanos/harness": "^0.1.0",
17
18
  "@hookform/resolvers": "^5.2.2",
19
+ "@tailwindcss/postcss": "^4.3.3",
18
20
  "@tanstack/react-query": "^5.101.4",
19
21
  "@types/react": "^19.2.0",
20
22
  "@types/react-dom": "^19.2.0",
@@ -22,6 +24,7 @@
22
24
  "react": "^19.2.8",
23
25
  "react-dom": "^19.2.8",
24
26
  "react-hook-form": "^7.68.0",
27
+ "tailwindcss": "^4.3.3",
25
28
  "typescript": "^5.9.3",
26
29
  "vitest": "4.1.10",
27
30
  "zod": "^3.25.76"
@@ -74,6 +74,11 @@ Every widget: `'use client'`, `Widget.DataState` wrapping `Widget.Card` →
74
74
  fetching on `ctx?.state.state === 'active'` from `WidgetContext`. Copy the
75
75
  canonical example from the SDK README rather than improvising the tree.
76
76
 
77
+ `Widget.Active` itself renders flush (no padding) so full-bleed charts and
78
+ tables can run edge-to-edge. For ordinary content — text, rows, a KPI
79
+ readout — wrap it in `Widget.Body` (the standard padded body, `px-5 pt-4
80
+ pb-5`) instead of a hand-rolled `px-4`/`px-5` wrapper div.
81
+
77
82
  ## Four states, no exceptions
78
83
 
79
84
  - **Loading**: `Widget.Loading` (or `skeleton` blocks shaped like the real
@@ -6,6 +6,10 @@ names — verify against the Free set). Everything sits inside the standard
6
6
  `Widget.Active` body from the SDK's compound API; these are the bodies, not
7
7
  the wrapper.
8
8
 
9
+ Where a pattern needs a plain padded body (not a full-bleed chart or table),
10
+ prefer `Widget.Body` over a hand-rolled `px-4`/`px-5` wrapper — see the SDK
11
+ README §`components.widgets`.
12
+
9
13
  ## 1. Flat list row → row with a first read
10
14
 
11
15
  **Failure:** every field the same size, so nothing answers "what needs my