@ekanos/cli 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -8
- package/dist/agents-api.d.ts +11 -2
- package/dist/agents-api.js +39 -3
- package/dist/agents-api.js.map +1 -1
- package/dist/apps-api.d.ts +3 -2
- package/dist/apps-api.js +3 -7
- package/dist/apps-api.js.map +1 -1
- package/dist/auth/ekanos-api.d.ts +16 -2
- package/dist/auth/ekanos-api.js +101 -15
- package/dist/auth/ekanos-api.js.map +1 -1
- package/dist/auth/host-trust.d.ts +46 -0
- package/dist/auth/host-trust.js +79 -0
- package/dist/auth/host-trust.js.map +1 -0
- package/dist/auth/session.d.ts +10 -1
- package/dist/auth/session.js +28 -28
- package/dist/auth/session.js.map +1 -1
- package/dist/commands/agents-edit.d.ts +6 -2
- package/dist/commands/agents-edit.js +57 -26
- package/dist/commands/agents-edit.js.map +1 -1
- package/dist/commands/agents.d.ts +13 -2
- package/dist/commands/agents.js +148 -36
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/apps.d.ts +15 -4
- package/dist/commands/apps.js +133 -51
- package/dist/commands/apps.js.map +1 -1
- package/dist/commands/catalog.js +7 -6
- package/dist/commands/catalog.js.map +1 -1
- package/dist/commands/knowledge.d.ts +56 -0
- package/dist/commands/knowledge.js +456 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +30 -2
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/logout.d.ts +9 -1
- package/dist/commands/logout.js +56 -7
- package/dist/commands/logout.js.map +1 -1
- package/dist/commands/publish.js +3 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/reviewers.d.ts +2 -0
- package/dist/commands/reviewers.js +7 -3
- package/dist/commands/reviewers.js.map +1 -1
- package/dist/commands/routes.d.ts +2 -0
- package/dist/commands/routes.js +42 -9
- package/dist/commands/routes.js.map +1 -1
- package/dist/commands/runs.d.ts +13 -2
- package/dist/commands/runs.js +96 -15
- package/dist/commands/runs.js.map +1 -1
- package/dist/commands/scope-notice.d.ts +21 -0
- package/dist/commands/scope-notice.js +35 -0
- package/dist/commands/scope-notice.js.map +1 -0
- package/dist/commands/skills.d.ts +7 -1
- package/dist/commands/skills.js +80 -24
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/sources.d.ts +4 -0
- package/dist/commands/sources.js +233 -5
- package/dist/commands/sources.js.map +1 -1
- package/dist/commands/status.js +5 -4
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/team-agent-readiness.d.ts +59 -0
- package/dist/commands/team-agent-readiness.js +341 -0
- package/dist/commands/team-agent-readiness.js.map +1 -0
- package/dist/commands/team-agents-agents.d.ts +24 -3
- package/dist/commands/team-agents-agents.js +336 -35
- package/dist/commands/team-agents-agents.js.map +1 -1
- package/dist/commands/team-agents-mcp.js +32 -3
- package/dist/commands/team-agents-mcp.js.map +1 -1
- package/dist/commands/team-agents-triggers.js +230 -54
- package/dist/commands/team-agents-triggers.js.map +1 -1
- package/dist/commands/team-agents.d.ts +3 -1
- package/dist/commands/team-agents.js +27 -7
- package/dist/commands/team-agents.js.map +1 -1
- package/dist/commands/teams-channel-install.d.ts +24 -0
- package/dist/commands/teams-channel-install.js +94 -0
- package/dist/commands/teams-channel-install.js.map +1 -0
- package/dist/commands/teams-connect.d.ts +66 -0
- package/dist/commands/teams-connect.js +397 -0
- package/dist/commands/teams-connect.js.map +1 -0
- package/dist/commands/teams-events.d.ts +12 -0
- package/dist/commands/teams-events.js +138 -0
- package/dist/commands/teams-events.js.map +1 -0
- package/dist/commands/teams-onboard.d.ts +16 -5
- package/dist/commands/teams-onboard.js +61 -36
- package/dist/commands/teams-onboard.js.map +1 -1
- package/dist/commands/teams-overview.d.ts +11 -0
- package/dist/commands/teams-overview.js +160 -0
- package/dist/commands/teams-overview.js.map +1 -0
- package/dist/commands/teams-scope.d.ts +10 -0
- package/dist/commands/teams-scope.js +109 -0
- package/dist/commands/teams-scope.js.map +1 -0
- package/dist/commands/teams.d.ts +64 -7
- package/dist/commands/teams.js +254 -60
- package/dist/commands/teams.js.map +1 -1
- package/dist/commands/upgrade.js +12 -2
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/commands/use.js +2 -2
- package/dist/commands/use.js.map +1 -1
- package/dist/commands/whoami.js +1 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/connections-api.js +1 -1
- package/dist/connections-api.js.map +1 -1
- package/dist/delegate.js +11 -4
- package/dist/delegate.js.map +1 -1
- package/dist/harness-scaffold.js +12 -0
- package/dist/harness-scaffold.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +724 -152
- package/dist/index.js.map +1 -1
- package/dist/knowledge-api.d.ts +175 -0
- package/dist/knowledge-api.js +169 -0
- package/dist/knowledge-api.js.map +1 -0
- package/dist/open-url.d.ts +16 -0
- package/dist/open-url.js +73 -0
- package/dist/open-url.js.map +1 -0
- package/dist/parse-argv.d.ts +2 -0
- package/dist/parse-argv.js.map +1 -1
- package/dist/publish-api.d.ts +2 -1
- package/dist/publish-api.js +4 -3
- package/dist/publish-api.js.map +1 -1
- package/dist/pull-write.d.ts +27 -0
- package/dist/pull-write.js +60 -0
- package/dist/pull-write.js.map +1 -0
- package/dist/routes-api.d.ts +3 -0
- package/dist/routes-api.js +9 -4
- package/dist/routes-api.js.map +1 -1
- package/dist/runs-api.d.ts +7 -1
- package/dist/runs-api.js +21 -6
- package/dist/runs-api.js.map +1 -1
- package/dist/safe-fs.d.ts +2 -0
- package/dist/safe-fs.js +12 -6
- package/dist/safe-fs.js.map +1 -1
- package/dist/secret-file.d.ts +17 -0
- package/dist/secret-file.js +69 -0
- package/dist/secret-file.js.map +1 -0
- package/dist/skills-api.d.ts +10 -2
- package/dist/skills-api.js +37 -2
- package/dist/skills-api.js.map +1 -1
- package/dist/source-products-api.d.ts +63 -0
- package/dist/source-products-api.js +123 -0
- package/dist/source-products-api.js.map +1 -0
- package/dist/source-target.js +2 -1
- package/dist/source-target.js.map +1 -1
- package/dist/sources-api.d.ts +2 -1
- package/dist/sources-api.js +3 -2
- package/dist/sources-api.js.map +1 -1
- package/dist/team-connect-api.d.ts +286 -0
- package/dist/team-connect-api.js +107 -0
- package/dist/team-connect-api.js.map +1 -0
- package/dist/teams-api.d.ts +42 -1
- package/dist/teams-api.js +98 -20
- package/dist/teams-api.js.map +1 -1
- package/dist/toolchain-api.d.ts +9 -0
- package/dist/toolchain-api.js +17 -2
- package/dist/toolchain-api.js.map +1 -1
- package/dist/toolchain-resolve.d.ts +3 -0
- package/dist/toolchain-resolve.js +15 -1
- package/dist/toolchain-resolve.js.map +1 -1
- package/dist/toolchain.d.ts +6 -0
- package/dist/toolchain.js +19 -0
- package/dist/toolchain.js.map +1 -1
- package/dist/update-notice.js +2 -1
- package/dist/update-notice.js.map +1 -1
- package/package.json +2 -2
- package/templates/AGENTS.md.tmpl +6 -1
- package/templates/claude-skill.md.tmpl +5 -0
- package/templates/package.json.tmpl +4 -4
- package/templates/skills/build-agent/SKILL.md +115 -16
- package/templates/skills/build-agent/references/agent-json.md +26 -8
- package/templates/skills/build-agent/references/app-setup.md +2 -1
- package/templates/skills/build-agent/references/rollout.md +27 -4
- package/templates/skills/build-agent/references/set-up-an-integration.md +74 -15
- package/templates/skills/build-agent/references/skills.md +20 -1
- package/templates/skills/build-agent/references/triggers.md +84 -20
- package/templates/skills/build-integration/references/widget-design.md +3 -3
- package/templates/skills/ekanos-platform/SKILL.md +70 -17
- package/templates/skills/polish-widgets/references/patterns.md +2 -2
package/dist/skills-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills-api.js","sourceRoot":"","sources":["../src/skills-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,qBAAqB,EAGrB,2BAA2B,EAE3B,qBAAqB,GACtB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EACL,iBAAiB,EACjB,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,UAAU,EACV,eAAe,GAChB,MAAM,UAAU,CAAC;AAyBlB,MAAM,KAAK,GAAG,qBAAqB,CAAC;AAEpC,SAAS,KAAK,CAAC,MAAc,EAAE,IAAwB;IACrD,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,IAAY,EACZ,WAAmB,EACnB,MAAc,EACd,IAAa;IAEb,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,qBAAqB,EAAE;QACpD,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,GAAG,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE;QACvC,OAAO,EAAE,IAAI,aAAJ,IAAI,cAAJ,IAAI,GAAI,MAAM;KACxB,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,IAAY,EACZ,WAAmB,EACnB,MAAc,EACd,IAAY,EACZ,IAAa;IAEb,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,qBAAqB,EAAE;QACpD,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,GAAG,KAAK,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE;QACnE,OAAO,EAAE,IAAI;KACd,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,IAAY,EACZ,WAAmB,EACnB,IAA4B;IAE5B,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,2BAA2B,EAAE;QAC1D,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,KAAK;QACX,IAAI;QACJ,OAAO,EAAE,IAAI,CAAC,IAAI;KACnB,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,IAAI,CACjB,IAAY,EACZ,WAAmB,EACnB,MAAoB,EACpB,IAKC;IAED,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,EAAE;QACpD,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,OAAO,kBACL,MAAM,EAAE,kBAAkB,EAC1B,aAAa,EAAE,UAAU,WAAW,EAAE,IACnC,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS;YACzB,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAC5C;QACD,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;KACtE,CAAC,CAAC;IAEH,IACE,QAAQ,CAAC,MAAM,KAAK,GAAG;QACvB,CAAC,QAAQ,CAAC,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG,CAAC,EACjD,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAEhC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,KAAK,eAAe,EAAE,CAAC;QACxD,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC;YACvC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAc,EAAE,EAAE;gBACrC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;oBAAE,OAAO,EAAE,CAAC;gBAC3D,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,KAAgC,CAAC;gBACjE,OAAO,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAChD,CAAC,CAAC;YACJ,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,eAAe,CACnB,CAAC,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,6BAA6B,EAAE,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAChE,sCAAsC,CACvC,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACvD,MAAM,UAAU,CACd,OAAO,aAAP,OAAO,cAAP,OAAO,GACL,CAAC,QAAQ,CAAC,MAAM,KAAK,GAAG;YACtB,CAAC,CAAC,wCAAwC;YAC1C,CAAC,CAAC,+BAA+B,CAAC,EACtC,8DAA8D,CAC/D,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;QACvD,MAAM,iBAAiB,CACrB,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,GAAG,IAAI,wCAAwC,EAC1D,6DAA6D,CAC9D,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,cAAc,CAClB,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,+BAA+B,EAC1C,IAAI,KAAK,uBAAuB;YAC9B,CAAC,CAAC,4EAA4E;YAC9E,CAAC,CAAC,8DAA8D,CACnE,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,aAAa,CACjB,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,IAAI,IAAI,CAAC,OAAO,kBAAkB,EAC7C,IAAI,KAAK,kBAAkB;YACzB,CAAC,CAAC,8DAA8D;YAChE,CAAC,CAAC,IAAI,KAAK,gBAAgB;gBACzB,CAAC,CAAC,qDAAqD;gBACvD,CAAC,CAAC,6CAA6C,CACpD,CAAC;IACJ,CAAC;IAED,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,CACrB,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,IAAI,IAAI,CAAC,OAAO,qBAAqB,EAChD,oDAAoD,CACrD,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,YAAY,CAChB,GAAG,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,QAAQ,CAAC,MAAM,GAAG,EACjE,QAAQ,CAAC,MAAM,IAAI,GAAG;YACpB,CAAC,CAAC,+DAA+D;YACjE,CAAC,CAAC,gEAAgE,CACrE,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,YAAY,CAChB,GAAG,IAAI,GAAG,KAAK,8CAA8C,EAC7D,8EAA8E,CAC/E,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;AAC7C,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,QAAkB;IAElB,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAC9C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAClD,CAAC,CAAE,MAAkC;YACrC,CAAC,CAAC,EAAE,CAAC;IACT,CAAC;eAAO,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC","sourcesContent":["import {\n type SourceSkillList,\n SourceSkillListSchema,\n type SourceSkillPushRequest,\n type SourceSkillPushResult,\n SourceSkillPushResultSchema,\n type SourceSkillShow,\n SourceSkillShowSchema,\n} from '@ekanos/integration-schema';\nimport type { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's skills.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the `SourceSkill*` schemas in `@ekanos/integration-schema`,\n * which the host's `/api/partner/skills` routes build and this parses.\n *\n * GET /api/partner/skills?source=<s>[&team=<t>] → { ok, data: SourceSkillList }\n * GET /api/partner/skills/<slug>?source=<s>[&team=<t>] → { ok, data: SourceSkillShow }\n * POST /api/partner/skills SourceSkillPushRequest → { ok, data: SourceSkillPushResult }\n *\n * → 401 / 3xx refresh once and retry\n * → 400 { code: \"INVALID_SKILL\", issues: { path, message }[] }\n * → 403 { code: \"MFA_REQUIRED\" | \"FORBIDDEN\" | \"SOURCE_ADMIN_REQUIRED\" }\n * → 404 { code: \"SOURCE_NOT_FOUND\" | \"TEAM_NOT_FOUND\" | \"SKILL_NOT_FOUND\" }\n * → 409 { code: \"SKILL_NOT_PROMPT\" }\n * ---------------------------------------------------------------------------\n */\n\nexport type SkillsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nconst ROUTE = '/api/partner/skills';\n\nfunction query(source: string, team: string | undefined) {\n return new URLSearchParams(team ? { source, team } : { source });\n}\n\nexport function listSkills(\n host: string,\n accessToken: string,\n source: string,\n team?: string,\n): Promise<SkillsApiResult<SourceSkillList>> {\n return call(host, accessToken, SourceSkillListSchema, {\n method: 'GET',\n path: `${ROUTE}?${query(source, team)}`,\n subject: team ?? source,\n });\n}\n\nexport function showSkill(\n host: string,\n accessToken: string,\n source: string,\n slug: string,\n team?: string,\n): Promise<SkillsApiResult<SourceSkillShow>> {\n return call(host, accessToken, SourceSkillShowSchema, {\n method: 'GET',\n path: `${ROUTE}/${encodeURIComponent(slug)}?${query(source, team)}`,\n subject: slug,\n });\n}\n\nexport function pushSkill(\n host: string,\n accessToken: string,\n body: SourceSkillPushRequest,\n): Promise<SkillsApiResult<SourceSkillPushResult>> {\n return call(host, accessToken, SourceSkillPushResultSchema, {\n method: 'POST',\n path: ROUTE,\n body,\n subject: body.slug,\n });\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n body?: unknown;\n subject: string;\n },\n): Promise<SkillsApiResult<T>> {\n const response = await request(`${host}${init.path}`, {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n if (\n response.status === 401 ||\n (response.status >= 300 && response.status < 400)\n ) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = str(body.code);\n const message = str(body.error);\n\n if (response.status === 400 && code === 'INVALID_SKILL') {\n const issues = Array.isArray(body.issues)\n ? body.issues.flatMap((issue: unknown) => {\n if (typeof issue !== 'object' || issue === null) return [];\n const { path, message: text } = issue as Record<string, unknown>;\n return [` ${String(path)}: ${String(text)}`];\n })\n : [];\n throw validationError(\n [message ?? 'The host refused the skill.', ...issues].join('\\n'),\n 'Fix the skill folder and push again.',\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw usageError(\n message ??\n (response.status === 413\n ? 'The skill folder is too large to send.'\n : 'The host refused the request.'),\n 'See \"ekanos skills --help\" for each subcommand\\'s arguments.',\n );\n }\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n 'Complete MFA in the browser, then run \"ekanos login\" again.',\n );\n }\n\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n code === 'SOURCE_ADMIN_REQUIRED'\n ? 'Only a Source admin pushes skills or reads team skills. Ask one to run it.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n message ?? `\"${init.subject}\" was not found.`,\n code === 'SOURCE_NOT_FOUND'\n ? 'Run \"ekanos sources\" to list the sources you hold a seat on.'\n : code === 'TEAM_NOT_FOUND'\n ? 'Run \"ekanos teams list\" to see the Source\\'s teams.'\n : 'Run \"ekanos skills list\" to see the skills.',\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n message ?? `\"${init.subject}\" cannot be pushed.`,\n 'Rename the skill folder (its slug) and push again.',\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}${init.path.split('?')[0]} responded ${response.status}.`,\n response.status >= 500\n ? 'The Ekanos deployment returned a server error. Retry shortly.'\n : 'Confirm the host runs a build that includes the skills routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${ROUTE} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the skills contract.',\n );\n }\n\n return { status: 'ok', data: parsed.data };\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"skills-api.js","sourceRoot":"","sources":["../src/skills-api.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n type SourceSkillArchiveResult,\n SourceSkillArchiveResultSchema,\n SourceSkillInUseSchema,\n type SourceSkillList,\n SourceSkillListSchema,\n type SourceSkillPushRequest,\n type SourceSkillPushResult,\n SourceSkillPushResultSchema,\n type SourceSkillShow,\n SourceSkillShowSchema,\n} from '@ekanos/integration-schema';\nimport type { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n validationError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's skills.\n *\n * ---------------------------------------------------------------------------\n * WIRE CONTRACT — the `SourceSkill*` schemas in `@ekanos/integration-schema`,\n * which the host's `/api/partner/skills` routes build and this parses.\n *\n * GET /api/partner/skills?source=<s>[&team=<t>] → { ok, data: SourceSkillList }\n * GET /api/partner/skills/<slug>?source=<s>[&team=<t>] → { ok, data: SourceSkillShow }\n * POST /api/partner/skills SourceSkillPushRequest → { ok, data: SourceSkillPushResult }\n * POST /api/partner/skills/<slug>/archive | unarchive { source, team? }\n * → { ok, data: SourceSkillArchiveResult }\n *\n * → 401 / 3xx refresh once and retry\n * → 400 { code: \"INVALID_SKILL\", issues: { path, message }[] }\n * → 403 { code: \"MFA_REQUIRED\" | \"FORBIDDEN\" | \"SOURCE_ADMIN_REQUIRED\" }\n * → 404 { code: \"SOURCE_NOT_FOUND\" | \"TEAM_NOT_FOUND\" | \"SKILL_NOT_FOUND\" }\n * → 409 { code: \"SKILL_NOT_PROMPT\" | \"SKILL_ARCHIVED\" }\n * → 409 { code: \"SKILL_IN_USE\", inUse: SourceSkillInUse } archive\n * ---------------------------------------------------------------------------\n */\n\nexport type SkillsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nconst ROUTE = '/api/partner/skills';\n\nfunction query(source: string, team: string | undefined) {\n return new URLSearchParams(team ? { source, team } : { source });\n}\n\nexport function listSkills(\n host: string,\n accessToken: string,\n source: string,\n team?: string,\n): Promise<SkillsApiResult<SourceSkillList>> {\n return call(host, accessToken, SourceSkillListSchema, {\n method: 'GET',\n path: `${ROUTE}?${query(source, team)}`,\n subject: team ?? source,\n });\n}\n\nexport function showSkill(\n host: string,\n accessToken: string,\n source: string,\n slug: string,\n team?: string,\n): Promise<SkillsApiResult<SourceSkillShow>> {\n return call(host, accessToken, SourceSkillShowSchema, {\n method: 'GET',\n path: `${ROUTE}/${encodeURIComponent(slug)}?${query(source, team)}`,\n subject: slug,\n });\n}\n\nexport function pushSkill(\n host: string,\n accessToken: string,\n body: SourceSkillPushRequest,\n): Promise<SkillsApiResult<SourceSkillPushResult>> {\n return call(host, accessToken, SourceSkillPushResultSchema, {\n method: 'POST',\n path: ROUTE,\n body,\n subject: body.slug,\n });\n}\n\nexport function archiveSkill(\n host: string,\n accessToken: string,\n action: 'archive' | 'unarchive',\n params: { source: string; slug: string; team?: string },\n): Promise<SkillsApiResult<SourceSkillArchiveResult>> {\n return call(host, accessToken, SourceSkillArchiveResultSchema, {\n method: 'POST',\n path: `${ROUTE}/${encodeURIComponent(params.slug)}/${action}`,\n body: {\n source: params.source,\n ...(params.team ? { team: params.team } : {}),\n },\n subject: params.slug,\n source: params.source,\n });\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: {\n method: 'GET' | 'POST';\n path: string;\n body?: unknown;\n subject: string;\n /** For the commands a refusal suggests. */\n source?: string;\n },\n): Promise<SkillsApiResult<T>> {\n const response = await request(`${host}${init.path}`, {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n ...(init.body === undefined\n ? {}\n : { 'content-type': 'application/json' }),\n },\n body: init.body === undefined ? undefined : JSON.stringify(init.body),\n });\n\n if (\n response.status === 401 ||\n (response.status >= 300 && response.status < 400)\n ) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = str(body.code);\n const message = str(body.error);\n\n if (response.status === 400 && code === 'INVALID_SKILL') {\n const issues = Array.isArray(body.issues)\n ? body.issues.flatMap((issue: unknown) => {\n if (typeof issue !== 'object' || issue === null) return [];\n const { path, message: text } = issue as Record<string, unknown>;\n return [` ${String(path)}: ${String(text)}`];\n })\n : [];\n throw validationError(\n [message ?? 'The host refused the skill.', ...issues].join('\\n'),\n 'Fix the skill folder and push again.',\n );\n }\n\n if (response.status === 400 || response.status === 413) {\n throw usageError(\n message ??\n (response.status === 413\n ? 'The skill folder is too large to send.'\n : 'The host refused the request.'),\n 'See \"ekanos skills --help\" for each subcommand\\'s arguments.',\n );\n }\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n 'Complete MFA in the browser, then run \"ekanos login\" again.',\n );\n }\n\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n code === 'SOURCE_ADMIN_REQUIRED'\n ? 'Only a Source admin pushes or archives skills, or reads team skills. Ask one to run it.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n\n if (response.status === 404) {\n throw notFoundError(\n message ?? `\"${init.subject}\" was not found.`,\n code === 'SOURCE_NOT_FOUND'\n ? 'Run \"ekanos sources\" to list the sources you hold a seat on.'\n : code === 'TEAM_NOT_FOUND'\n ? 'Run \"ekanos teams list\" to see the Source\\'s teams.'\n : 'Run \"ekanos skills list\" to see the skills.',\n );\n }\n\n if (response.status === 409 && code === 'SKILL_ARCHIVED') {\n throw invalidStateError(\n message ?? `\"${init.subject}\" is archived.`,\n `Run \"ekanos skills unarchive ${init.subject}\" first, or push it under another slug.`,\n );\n }\n\n if (response.status === 409 && code === 'SKILL_IN_USE') {\n throw skillInUseError(\n message ?? `\"${init.subject}\" is still in use.`,\n body.inUse,\n init.subject,\n init.source,\n );\n }\n\n if (response.status === 409) {\n throw invalidStateError(\n message ?? `\"${init.subject}\" cannot be pushed.`,\n 'Rename the skill folder (its slug) and push again.',\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}${init.path.split('?')[0]} responded ${response.status}.`,\n response.status >= 500\n ? 'The Ekanos deployment returned a server error. Retry shortly.'\n : 'Confirm the host runs a build that includes the skills routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}${ROUTE} answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the skills contract.',\n );\n }\n\n return { status: 'ok', data: parsed.data };\n}\n\n/** What still uses the skill, and the command that stops each use. */\nfunction skillInUseError(\n message: string,\n value: unknown,\n slug: string,\n source: string | undefined,\n) {\n const parsed = SourceSkillInUseSchema.safeParse(value);\n const inUse = parsed.success ? parsed.data : { templates: [], agents: [] };\n const on = source ? ` --source ${source}` : '';\n return invalidStateError(\n [\n message,\n ...inUse.templates.map(\n (template) => ` agent ${template.key} (${template.name}) names it`,\n ),\n ...inUse.agents.map(\n (agent) =>\n ` ${agent.team}: agent ${agent.agent} has it attached` +\n (agent.fromTemplate\n ? ' (from an older template version it is pinned to)'\n : ''),\n ),\n ].join('\\n'),\n [\n 'Stop each use, then archive again:',\n ...inUse.templates.map(\n (template) =>\n ` ekanos agents pull ${template.key}${on}, drop \"${slug}\" from its skills, then agents apply and agents publish (or archive the agent: ekanos agents archive ${template.key}${on})`,\n ),\n ...inUse.agents.map((agent) =>\n agent.fromTemplate\n ? ` ekanos teams agents unpin ${agent.team} ${agent.agent}${on}`\n : ` ekanos teams agents skills ${agent.team} ${agent.agent} remove ${slug}${on}`,\n ),\n ].join('\\n'),\n );\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\nfunction str(value: unknown): string | null {\n return typeof value === 'string' && value.length > 0 ? value : null;\n}\n"]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SourceProductSchema: z.ZodObject<{
|
|
3
|
+
slug: z.ZodString;
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
kind: z.ZodEnum<{
|
|
6
|
+
experience: "experience";
|
|
7
|
+
integration: "integration";
|
|
8
|
+
}>;
|
|
9
|
+
enabled: z.ZodBoolean;
|
|
10
|
+
lever: z.ZodEnum<{
|
|
11
|
+
entitlement: "entitlement";
|
|
12
|
+
flag: "flag";
|
|
13
|
+
plan: "plan";
|
|
14
|
+
}>;
|
|
15
|
+
registered: z.ZodBoolean;
|
|
16
|
+
owned: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type SourceProduct = z.infer<typeof SourceProductSchema>;
|
|
19
|
+
export type SourceProductsApiResult<T> = {
|
|
20
|
+
status: 'ok';
|
|
21
|
+
data: T;
|
|
22
|
+
} | {
|
|
23
|
+
status: 'unauthorized';
|
|
24
|
+
};
|
|
25
|
+
export declare function listSourceProducts(host: string, token: string, source: string): Promise<SourceProductsApiResult<{
|
|
26
|
+
source: {
|
|
27
|
+
id: string;
|
|
28
|
+
slug: string;
|
|
29
|
+
name: string | null;
|
|
30
|
+
};
|
|
31
|
+
products: {
|
|
32
|
+
slug: string;
|
|
33
|
+
name: string;
|
|
34
|
+
kind: "experience" | "integration";
|
|
35
|
+
enabled: boolean;
|
|
36
|
+
lever: "entitlement" | "flag" | "plan";
|
|
37
|
+
registered: boolean;
|
|
38
|
+
owned: boolean;
|
|
39
|
+
}[];
|
|
40
|
+
canEnable: boolean;
|
|
41
|
+
canEnableOwned: boolean;
|
|
42
|
+
}>>;
|
|
43
|
+
export declare function setSourceProduct(host: string, token: string, input: {
|
|
44
|
+
source: string;
|
|
45
|
+
product: string;
|
|
46
|
+
enabled: boolean;
|
|
47
|
+
}): Promise<SourceProductsApiResult<{
|
|
48
|
+
source: {
|
|
49
|
+
id: string;
|
|
50
|
+
slug: string;
|
|
51
|
+
name: string | null;
|
|
52
|
+
};
|
|
53
|
+
product: {
|
|
54
|
+
slug: string;
|
|
55
|
+
name: string;
|
|
56
|
+
kind: "experience" | "integration";
|
|
57
|
+
enabled: boolean;
|
|
58
|
+
lever: "entitlement" | "flag" | "plan";
|
|
59
|
+
registered: boolean;
|
|
60
|
+
owned: boolean;
|
|
61
|
+
};
|
|
62
|
+
changed: boolean;
|
|
63
|
+
}>>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { request } from './auth/ekanos-api.js';
|
|
3
|
+
import { authRequiredError, forbiddenError, invalidStateError, networkError, notFoundError, usageError, } from './errors.js';
|
|
4
|
+
/**
|
|
5
|
+
* The CLI's client for a Source's products (`ekanos sources products`).
|
|
6
|
+
*
|
|
7
|
+
* GET /api/partner/sources/products?source=<slug|id>
|
|
8
|
+
* → { source, products: [{ slug, name, kind, enabled, lever, registered, owned }],
|
|
9
|
+
* canEnable, canEnableOwned }
|
|
10
|
+
* PUT /api/partner/sources/products/<product>?source=<slug|id> enable
|
|
11
|
+
* DELETE /api/partner/sources/products/<product>?source=<slug|id> disable
|
|
12
|
+
* → { source, product, changed }
|
|
13
|
+
*
|
|
14
|
+
* → 401 / 3xx refresh once and retry
|
|
15
|
+
* → 403 SOURCE_ADMIN_REQUIRED no Source admin seat on the Source
|
|
16
|
+
* → 403 SUPER_ADMIN_REQUIRED not an integration the Source built: a
|
|
17
|
+
* super-admin grant, which an `ekanos login`
|
|
18
|
+
* session never carries
|
|
19
|
+
* → 409 REFUSED a dependency or demo-only guard refused
|
|
20
|
+
*
|
|
21
|
+
* The shapes are read here rather than from `@ekanos/integration-schema`, so
|
|
22
|
+
* this CLI works against a host without a schema release.
|
|
23
|
+
*/
|
|
24
|
+
const SourceRefSchema = z.object({
|
|
25
|
+
id: z.string(),
|
|
26
|
+
slug: z.string(),
|
|
27
|
+
name: z.string().nullable(),
|
|
28
|
+
});
|
|
29
|
+
export const SourceProductSchema = z.object({
|
|
30
|
+
slug: z.string(),
|
|
31
|
+
name: z.string(),
|
|
32
|
+
kind: z.enum(['experience', 'integration']),
|
|
33
|
+
enabled: z.boolean(),
|
|
34
|
+
lever: z.enum(['flag', 'plan', 'entitlement']),
|
|
35
|
+
registered: z.boolean(),
|
|
36
|
+
/** The Source built it, so its Source admin may switch it. Absent on an
|
|
37
|
+
* older host, which allowed no Source admin to. */
|
|
38
|
+
owned: z.boolean().optional().default(false),
|
|
39
|
+
});
|
|
40
|
+
const ListSchema = z.object({
|
|
41
|
+
source: SourceRefSchema,
|
|
42
|
+
products: z.array(SourceProductSchema),
|
|
43
|
+
canEnable: z.boolean(),
|
|
44
|
+
canEnableOwned: z.boolean().optional().default(false),
|
|
45
|
+
});
|
|
46
|
+
const ChangeSchema = z.object({
|
|
47
|
+
source: SourceRefSchema,
|
|
48
|
+
product: SourceProductSchema,
|
|
49
|
+
changed: z.boolean(),
|
|
50
|
+
});
|
|
51
|
+
export function listSourceProducts(host, token, source) {
|
|
52
|
+
return call(host, token, ListSchema, {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
path: `/api/partner/sources/products?${new URLSearchParams({ source })}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
export function setSourceProduct(host, token, input) {
|
|
58
|
+
return call(host, token, ChangeSchema, {
|
|
59
|
+
method: input.enabled ? 'PUT' : 'DELETE',
|
|
60
|
+
path: `/api/partner/sources/products/${encodeURIComponent(input.product)}?${new URLSearchParams({ source: input.source })}`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function call(host, accessToken, schema, init) {
|
|
64
|
+
const response = await request(`${host}${init.path}`, {
|
|
65
|
+
method: init.method,
|
|
66
|
+
headers: {
|
|
67
|
+
accept: 'application/json',
|
|
68
|
+
authorization: `Bearer ${accessToken}`,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
if (response.status === 401 ||
|
|
72
|
+
(response.status >= 300 && response.status < 400)) {
|
|
73
|
+
return { status: 'unauthorized' };
|
|
74
|
+
}
|
|
75
|
+
const body = await readOptionalJson(response);
|
|
76
|
+
const code = typeof body.code === 'string' ? body.code : null;
|
|
77
|
+
const message = typeof body.error === 'string' && body.error ? body.error : null;
|
|
78
|
+
if (response.status === 403 && code === 'MFA_REQUIRED') {
|
|
79
|
+
throw authRequiredError(message !== null && message !== void 0 ? message : `${host} requires multi-factor authentication.`, 'Complete MFA in the browser, then run "ekanos login" again.');
|
|
80
|
+
}
|
|
81
|
+
if (response.status === 403 && code === 'SUPER_ADMIN_REQUIRED') {
|
|
82
|
+
throw forbiddenError(message !== null && message !== void 0 ? message : 'Only a super-admin enables this product for a Source.', 'A Source admin switches only the integrations their Source built. Anything else a super-admin does in the browser: /admin/sources → the Source → Products (Experiences are switched on the same page).');
|
|
83
|
+
}
|
|
84
|
+
if (response.status === 403 && code === 'SOURCE_ADMIN_REQUIRED') {
|
|
85
|
+
throw forbiddenError(message !== null && message !== void 0 ? message : 'Switching a product needs a Source admin seat on the Source.', 'Run "ekanos sources" to see your seats; a Source admin grants the seat.');
|
|
86
|
+
}
|
|
87
|
+
if (response.status === 403) {
|
|
88
|
+
throw forbiddenError(message !== null && message !== void 0 ? message : 'Not permitted on this Source.', 'Run "ekanos sources" to list the sources you hold a seat on.');
|
|
89
|
+
}
|
|
90
|
+
if (response.status === 404) {
|
|
91
|
+
throw notFoundError(message !== null && message !== void 0 ? message : 'Not found.', code === 'PRODUCT_NOT_FOUND'
|
|
92
|
+
? '"ekanos sources products" lists the catalog.'
|
|
93
|
+
: 'Run "ekanos sources" to list the sources you hold a seat on.');
|
|
94
|
+
}
|
|
95
|
+
if (response.status === 409) {
|
|
96
|
+
throw invalidStateError(message !== null && message !== void 0 ? message : 'The host refused the change.', '"ekanos sources products" shows what this Source has.');
|
|
97
|
+
}
|
|
98
|
+
if (response.status === 400) {
|
|
99
|
+
throw usageError(message !== null && message !== void 0 ? message : 'The host refused the request.', 'See "ekanos sources --help".');
|
|
100
|
+
}
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw networkError(`${host}${init.path.split('?')[0]} responded ${response.status}.`, response.status >= 500
|
|
103
|
+
? 'The Ekanos deployment returned a server error. Retry shortly.'
|
|
104
|
+
: 'Confirm the host runs a build that includes the source products routes.');
|
|
105
|
+
}
|
|
106
|
+
const parsed = schema.safeParse(body.data);
|
|
107
|
+
if (!parsed.success) {
|
|
108
|
+
throw networkError(`${host}/api/partner/sources/products answered in a shape this CLI does not read.`, 'Run "ekanos upgrade" — the CLI and the host disagree on the source products contract.');
|
|
109
|
+
}
|
|
110
|
+
return { status: 'ok', data: parsed.data };
|
|
111
|
+
}
|
|
112
|
+
async function readOptionalJson(response) {
|
|
113
|
+
try {
|
|
114
|
+
const parsed = await response.json();
|
|
115
|
+
return typeof parsed === 'object' && parsed !== null
|
|
116
|
+
? parsed
|
|
117
|
+
: {};
|
|
118
|
+
}
|
|
119
|
+
catch (_a) {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=source-products-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source-products-api.js","sourceRoot":"","sources":["../src/source-products-api.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from 'zod';\n\nimport { request } from './auth/ekanos-api';\nimport {\n authRequiredError,\n forbiddenError,\n invalidStateError,\n networkError,\n notFoundError,\n usageError,\n} from './errors';\n\n/**\n * The CLI's client for a Source's products (`ekanos sources products`).\n *\n * GET /api/partner/sources/products?source=<slug|id>\n * → { source, products: [{ slug, name, kind, enabled, lever, registered, owned }],\n * canEnable, canEnableOwned }\n * PUT /api/partner/sources/products/<product>?source=<slug|id> enable\n * DELETE /api/partner/sources/products/<product>?source=<slug|id> disable\n * → { source, product, changed }\n *\n * → 401 / 3xx refresh once and retry\n * → 403 SOURCE_ADMIN_REQUIRED no Source admin seat on the Source\n * → 403 SUPER_ADMIN_REQUIRED not an integration the Source built: a\n * super-admin grant, which an `ekanos login`\n * session never carries\n * → 409 REFUSED a dependency or demo-only guard refused\n *\n * The shapes are read here rather than from `@ekanos/integration-schema`, so\n * this CLI works against a host without a schema release.\n */\n\nconst SourceRefSchema = z.object({\n id: z.string(),\n slug: z.string(),\n name: z.string().nullable(),\n});\n\nexport const SourceProductSchema = z.object({\n slug: z.string(),\n name: z.string(),\n kind: z.enum(['experience', 'integration']),\n enabled: z.boolean(),\n lever: z.enum(['flag', 'plan', 'entitlement']),\n registered: z.boolean(),\n /** The Source built it, so its Source admin may switch it. Absent on an\n * older host, which allowed no Source admin to. */\n owned: z.boolean().optional().default(false),\n});\nexport type SourceProduct = z.infer<typeof SourceProductSchema>;\n\nconst ListSchema = z.object({\n source: SourceRefSchema,\n products: z.array(SourceProductSchema),\n canEnable: z.boolean(),\n canEnableOwned: z.boolean().optional().default(false),\n});\nconst ChangeSchema = z.object({\n source: SourceRefSchema,\n product: SourceProductSchema,\n changed: z.boolean(),\n});\n\nexport type SourceProductsApiResult<T> =\n | { status: 'ok'; data: T }\n | { status: 'unauthorized' };\n\nexport function listSourceProducts(\n host: string,\n token: string,\n source: string,\n) {\n return call(host, token, ListSchema, {\n method: 'GET',\n path: `/api/partner/sources/products?${new URLSearchParams({ source })}`,\n });\n}\n\nexport function setSourceProduct(\n host: string,\n token: string,\n input: { source: string; product: string; enabled: boolean },\n) {\n return call(host, token, ChangeSchema, {\n method: input.enabled ? 'PUT' : 'DELETE',\n path: `/api/partner/sources/products/${encodeURIComponent(input.product)}?${new URLSearchParams({ source: input.source })}`,\n });\n}\n\nasync function call<T>(\n host: string,\n accessToken: string,\n schema: z.ZodType<T>,\n init: { method: 'GET' | 'PUT' | 'DELETE'; path: string },\n): Promise<SourceProductsApiResult<T>> {\n const response = await request(`${host}${init.path}`, {\n method: init.method,\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (\n response.status === 401 ||\n (response.status >= 300 && response.status < 400)\n ) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n const code = typeof body.code === 'string' ? body.code : null;\n const message =\n typeof body.error === 'string' && body.error ? body.error : null;\n\n if (response.status === 403 && code === 'MFA_REQUIRED') {\n throw authRequiredError(\n message ?? `${host} requires multi-factor authentication.`,\n 'Complete MFA in the browser, then run \"ekanos login\" again.',\n );\n }\n if (response.status === 403 && code === 'SUPER_ADMIN_REQUIRED') {\n throw forbiddenError(\n message ?? 'Only a super-admin enables this product for a Source.',\n 'A Source admin switches only the integrations their Source built. Anything else a super-admin does in the browser: /admin/sources → the Source → Products (Experiences are switched on the same page).',\n );\n }\n if (response.status === 403 && code === 'SOURCE_ADMIN_REQUIRED') {\n throw forbiddenError(\n message ?? 'Switching a product needs a Source admin seat on the Source.',\n 'Run \"ekanos sources\" to see your seats; a Source admin grants the seat.',\n );\n }\n if (response.status === 403) {\n throw forbiddenError(\n message ?? 'Not permitted on this Source.',\n 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 404) {\n throw notFoundError(\n message ?? 'Not found.',\n code === 'PRODUCT_NOT_FOUND'\n ? '\"ekanos sources products\" lists the catalog.'\n : 'Run \"ekanos sources\" to list the sources you hold a seat on.',\n );\n }\n if (response.status === 409) {\n throw invalidStateError(\n message ?? 'The host refused the change.',\n '\"ekanos sources products\" shows what this Source has.',\n );\n }\n if (response.status === 400) {\n throw usageError(\n message ?? 'The host refused the request.',\n 'See \"ekanos sources --help\".',\n );\n }\n if (!response.ok) {\n throw networkError(\n `${host}${init.path.split('?')[0]} responded ${response.status}.`,\n response.status >= 500\n ? 'The Ekanos deployment returned a server error. Retry shortly.'\n : 'Confirm the host runs a build that includes the source products routes.',\n );\n }\n\n const parsed = schema.safeParse(body.data);\n if (!parsed.success) {\n throw networkError(\n `${host}/api/partner/sources/products answered in a shape this CLI does not read.`,\n 'Run \"ekanos upgrade\" — the CLI and the host disagree on the source products contract.',\n );\n }\n return { status: 'ok', data: parsed.data };\n}\n\nasync function readOptionalJson(\n response: Response,\n): Promise<Record<string, unknown>> {\n try {\n const parsed: unknown = await response.json();\n return typeof parsed === 'object' && parsed !== null\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n"]}
|
package/dist/source-target.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { loginHint } from './auth/host-trust.js';
|
|
1
2
|
import { readActiveSource } from './commands/sources.js';
|
|
2
3
|
import { authRequiredError, validationError } from './errors.js';
|
|
3
4
|
import { fetchSeatsWithOneRefresh } from './seats.js';
|
|
@@ -14,7 +15,7 @@ export async function resolveSourceTarget(env, session, args) {
|
|
|
14
15
|
return { source: named, session };
|
|
15
16
|
const seats = await fetchSeatsWithOneRefresh(env, session);
|
|
16
17
|
if (seats.status !== 'ok') {
|
|
17
|
-
throw authRequiredError(`The session for ${env.host} could not list your sources.`,
|
|
18
|
+
throw authRequiredError(`The session for ${env.host} could not list your sources.`, loginHint(env));
|
|
18
19
|
}
|
|
19
20
|
if (seats.sources.length === 1) {
|
|
20
21
|
return { source: seats.sources[0].slug, session: seats.session };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-target.js","sourceRoot":"","sources":["../src/source-target.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"source-target.js","sourceRoot":"","sources":["../src/source-target.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,GAAoB,EACpB,OAAsB,EACtB,IAAsC;;IAEtC,MAAM,KAAK,GAAG,CAAA,MAAA,IAAI,CAAC,MAAM,0CAAE,IAAI,EAAE,KAAI,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAChE,IAAI,KAAK;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IAE7C,MAAM,KAAK,GAAG,MAAM,wBAAwB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;QAC1B,MAAM,iBAAiB,CACrB,mBAAmB,GAAG,CAAC,IAAI,+BAA+B,EAC1D,SAAS,CAAC,GAAG,CAAC,CACf,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;IACpE,CAAC;IAED,MAAM,eAAe,CACnB,6CAA6C;QAC3C,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC;YACzB,CAAC,CAAC,yBAAyB;YAC3B,CAAC,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,EAC9D,yDAAyD,CAC1D,CAAC;AACJ,CAAC","sourcesContent":["import type { StoredSession } from './auth/credential-store';\nimport { loginHint } from './auth/host-trust';\nimport type { AuthEnvironment } from './auth/session';\nimport { readActiveSource } from './commands/sources';\nimport { authRequiredError, validationError } from './errors';\nimport { fetchSeatsWithOneRefresh } from './seats';\n\n/**\n * Which Source a Source-scoped read or write (`catalog`, `agents`) is about:\n * `--source`, else `source` in ekanos.json, else the caller's only seat.\n * Works outside a project. With several seats and nothing named, it refuses\n * rather than guessing.\n */\nexport async function resolveSourceTarget(\n env: AuthEnvironment,\n session: StoredSession,\n args: { source?: string; dir: string },\n): Promise<{ source: string; session: StoredSession }> {\n const named = args.source?.trim() || readActiveSource(args.dir);\n if (named) return { source: named, session };\n\n const seats = await fetchSeatsWithOneRefresh(env, session);\n if (seats.status !== 'ok') {\n throw authRequiredError(\n `The session for ${env.host} could not list your sources.`,\n loginHint(env),\n );\n }\n if (seats.sources.length === 1) {\n return { source: seats.sources[0]!.slug, session: seats.session };\n }\n\n throw validationError(\n 'Which Source? This account holds a seat on ' +\n (seats.sources.length === 0\n ? 'no source on this host.'\n : `${seats.sources.map((seat) => seat.slug).join(', ')}.`),\n 'Pass \"--source <slug>\", or set \"source\" in ekanos.json.',\n );\n}\n"]}
|
package/dist/sources-api.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { AuthEnvironment } from './auth/session.js';
|
|
1
2
|
/**
|
|
2
3
|
* The CLI's client for the Ekanos source-seat visibility route.
|
|
3
4
|
*
|
|
@@ -41,4 +42,4 @@ export type FetchSourceSeatsResult = {
|
|
|
41
42
|
*/
|
|
42
43
|
export declare function fetchSourceSeats(host: string, accessToken: string): Promise<FetchSourceSeatsResult>;
|
|
43
44
|
/** Guard for the "still unauthorized after one refresh" terminal case. */
|
|
44
|
-
export declare function stillUnauthorizedSourceSeats(
|
|
45
|
+
export declare function stillUnauthorizedSourceSeats(env: AuthEnvironment): never;
|
package/dist/sources-api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { request } from './auth/ekanos-api.js';
|
|
2
|
+
import { loginHint } from './auth/host-trust.js';
|
|
2
3
|
import { authRequiredError, networkError } from './errors.js';
|
|
3
4
|
/**
|
|
4
5
|
* List the sources the caller holds a seat on (any role). Authorization is
|
|
@@ -43,8 +44,8 @@ export async function fetchSourceSeats(host, accessToken) {
|
|
|
43
44
|
return { status: 'ok', sources };
|
|
44
45
|
}
|
|
45
46
|
/** Guard for the "still unauthorized after one refresh" terminal case. */
|
|
46
|
-
export function stillUnauthorizedSourceSeats(
|
|
47
|
-
throw authRequiredError(`The session for ${host} could not authorize the source seat lookup.`,
|
|
47
|
+
export function stillUnauthorizedSourceSeats(env) {
|
|
48
|
+
throw authRequiredError(`The session for ${env.host} could not authorize the source seat lookup.`, loginHint(env));
|
|
48
49
|
}
|
|
49
50
|
async function readOptionalJson(response) {
|
|
50
51
|
try {
|
package/dist/sources-api.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAqC3D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAY,EACZ,WAAmB;;IAEnB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,sBAAsB,EAAE;QAC5D,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,aAAa,EAAE,UAAU,WAAW,EAAE;SACvC;KACF,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3D,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAE9C,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,OACrB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,mCACb,GAAG,IAAI,yDAAyD,EAClE,6DAA6D,CAC9D,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,YAAY,CAChB,GAAG,IAAI,kCAAkC,QAAQ,CAAC,MAAM,GAAG,EAC3D,QAAQ,CAAC,MAAM,IAAI,GAAG;YACpB,CAAC,CAAC,mEAAmE;gBACjE,8CAA8C;YAClD,CAAC,CAAC,gEAAgE;gBAC9D,qCAAqC,CAC5C,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,OAAC,IAAI,CAAC,IAAI,mCAAI,EAAE,CAA4B,CAAC;IAC1D,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAE7D,MAAM,OAAO,GAAG,IAAI;SACjB,MAAM,CACL,CAAC,GAAG,EAAkC,EAAE,CACtC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAC1C;SACA,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,sBAAC,CAAC;QACb,EAAE,QAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,mCAAI,EAAE;QACrB,IAAI,QAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,EAAE;QACzB,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;QACnB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YAC7B,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC;YACtE,CAAC,CAAC,EAAE;KACP,CAAC,GAAA,CAAC,CAAC;IAEN,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AACnC,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,4BAA4B,CAAC,
|
|
1
|
+
{"version":3,"file":"sources-api.js","sourceRoot":"","sources":["../src/sources-api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAqC3D;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,IAAY,EACZ,WAAmB;;IAEnB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,IAAI,sBAAsB,EAAE;QAC5D,MAAM,EAAE,KAAK;QACb,OAAO,EAAE;YACP,MAAM,EAAE,kBAAkB;YAC1B,aAAa,EAAE,UAAU,WAAW,EAAE;SACvC;KACF,CAAC,CAAC;IAEH,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3D,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpC,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAE9C,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QAC5B,MAAM,iBAAiB,OACrB,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,mCACb,GAAG,IAAI,yDAAyD,EAClE,6DAA6D,CAC9D,CAAC;IACJ,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,YAAY,CAChB,GAAG,IAAI,kCAAkC,QAAQ,CAAC,MAAM,GAAG,EAC3D,QAAQ,CAAC,MAAM,IAAI,GAAG;YACpB,CAAC,CAAC,mEAAmE;gBACjE,8CAA8C;YAClD,CAAC,CAAC,gEAAgE;gBAC9D,qCAAqC,CAC5C,CAAC;IACJ,CAAC;IAED,MAAM,IAAI,GAAG,OAAC,IAAI,CAAC,IAAI,mCAAI,EAAE,CAA4B,CAAC;IAC1D,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAE7D,MAAM,OAAO,GAAG,IAAI;SACjB,MAAM,CACL,CAAC,GAAG,EAAkC,EAAE,CACtC,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,CAC1C;SACA,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,sBAAC,CAAC;QACb,EAAE,QAAE,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,mCAAI,EAAE;QACrB,IAAI,QAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAI,EAAE;QACzB,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;QACnB,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC;YAC7B,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC;YACtE,CAAC,CAAC,EAAE;KACP,CAAC,GAAA,CAAC,CAAC;IAEN,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AACnC,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,4BAA4B,CAAC,GAAoB;IAC/D,MAAM,iBAAiB,CACrB,mBAAmB,GAAG,CAAC,IAAI,8CAA8C,EACzE,SAAS,CAAC,GAAG,CAAC,CACf,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,gBAAgB,CAC7B,QAAkB;IAElB,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;QAE9C,OAAO,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAClD,CAAC,CAAE,MAAkC;YACrC,CAAC,CAAC,EAAE,CAAC;IACT,CAAC;eAAO,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,MAAc;IAChC,OAAO,MAAM,IAAI,GAAG,IAAI,MAAM,GAAG,GAAG,CAAC;AACvC,CAAC;AAED,SAAS,GAAG,CAAC,KAAc;IACzB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC","sourcesContent":["import { request } from './auth/ekanos-api';\nimport { loginHint } from './auth/host-trust';\nimport type { AuthEnvironment } from './auth/session';\nimport { authRequiredError, networkError } from './errors';\n\n/**\n * The CLI's client for the Ekanos source-seat visibility 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/sources` route. As with the\n * auth routes and `publish-api.ts`, the two halves are coupled only by\n * HTTP; tests stub `fetch`, never the route.\n *\n * GET /api/partner/sources [Authorization: Bearer <access_token>]\n * → 200 { ok: true, data: { sources: [{ id, slug, name, roles }] } }\n * roles is a non-empty array of \"developer\" | \"source_admin\", sorted; sources\n * is sorted by slug. Empty array means no seats anywhere.\n * → 401 { error, code: \"AUTH_REQUIRED\" } token absent or expired —\n * refresh once and retry\n * → 403 { error, code: \"MFA_REQUIRED\" }\n * → 3xx treated as 401 (an\n * auth-gated Ekanos page\n * answers 307)\n * ---------------------------------------------------------------------------\n */\n\nexport interface SourceSeat {\n id: string;\n slug: string;\n name: string | null;\n roles: string[];\n}\n\nexport type FetchSourceSeatsResult =\n | { status: 'ok'; sources: SourceSeat[] }\n /** The bearer token did not authenticate — refresh and retry, once. */\n | { status: 'unauthorized' };\n\n/**\n * List the sources the caller holds a seat on (any role). Authorization is\n * entirely server-side (RLS on `source_memberships`): there is no client-side\n * filter to get wrong here.\n */\nexport async function fetchSourceSeats(\n host: string,\n accessToken: string,\n): Promise<FetchSourceSeatsResult> {\n const response = await request(`${host}/api/partner/sources`, {\n method: 'GET',\n headers: {\n accept: 'application/json',\n authorization: `Bearer ${accessToken}`,\n },\n });\n\n if (response.status === 401 || isRedirect(response.status)) {\n return { status: 'unauthorized' };\n }\n\n const body = await readOptionalJson(response);\n\n if (response.status === 403) {\n throw authRequiredError(\n str(body.error) ??\n `${host} requires multi-factor authentication for this account.`,\n `Complete MFA in the browser, then run \"ekanos login\" again.`,\n );\n }\n\n if (!response.ok) {\n throw networkError(\n `${host}/api/partner/sources 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 sources route.`,\n );\n }\n\n const data = (body.data ?? {}) as Record<string, unknown>;\n const rows = Array.isArray(data.sources) ? data.sources : [];\n\n const sources = 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 roles: Array.isArray(row.roles)\n ? row.roles.filter((role): role is string => typeof role === 'string')\n : [],\n }));\n\n return { status: 'ok', sources };\n}\n\n/** Guard for the \"still unauthorized after one refresh\" terminal case. */\nexport function stillUnauthorizedSourceSeats(env: AuthEnvironment): never {\n throw authRequiredError(\n `The session for ${env.host} could not authorize the source seat lookup.`,\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"]}
|