@sitecoreai-labs/sitecoreai-cli 0.1.2 → 0.2.1
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/dist/agents/tasks/agent.js +2 -2
- package/dist/agents/tasks/resources.js +2 -2
- package/dist/agents/tasks/space.js +1 -1
- package/dist/brand/api/auth.d.ts +16 -9
- package/dist/brand/api/auth.js +29 -19
- package/dist/brand/credential.d.ts +71 -1
- package/dist/brand/credential.js +119 -2
- package/dist/brand/recipe/diff.js +7 -2
- package/dist/brand/recipe/kind.js +0 -0
- package/dist/brand/recipe/schema.d.ts +113 -7
- package/dist/brand/recipe/schema.js +137 -8
- package/dist/brand/seed.d.ts +9 -5
- package/dist/brand/seed.js +30 -5
- package/dist/brief/api/briefs.d.ts +8 -0
- package/dist/brief/api/briefs.js +49 -11
- package/dist/brief/index.d.ts +1 -1
- package/dist/brief/index.js +2 -1
- package/dist/brief/recipe/index.d.ts +11 -2
- package/dist/brief/recipe/index.js +17 -3
- package/dist/brief/recipe/instance-diff.d.ts +4 -0
- package/dist/brief/recipe/instance-diff.js +77 -0
- package/dist/brief/recipe/instance-kind.d.ts +4 -0
- package/dist/brief/recipe/instance-kind.js +190 -0
- package/dist/brief/recipe/instance-schema.d.ts +61 -0
- package/dist/brief/recipe/instance-schema.js +68 -0
- package/dist/brief/recipe/schema.js +4 -1
- package/dist/brief/tasks/index.d.ts +35 -0
- package/dist/brief/tasks/index.js +62 -1
- package/dist/campaigns/recipe/schema.d.ts +39 -8
- package/dist/campaigns/recipe/schema.js +40 -10
- package/dist/commands/agents/sync.js +2 -2
- package/dist/commands/brand/seed.js +1 -1
- package/dist/commands/brand/sync.js +2 -2
- package/dist/commands/brief/create.d.ts +2 -0
- package/dist/commands/brief/create.js +56 -0
- package/dist/commands/brief/index.d.ts +3 -1
- package/dist/commands/brief/index.js +11 -1
- package/dist/commands/brief/sync.d.ts +9 -6
- package/dist/commands/brief/sync.js +54 -22
- package/dist/commands/brief/update.d.ts +2 -0
- package/dist/commands/brief/update.js +84 -0
- package/dist/commands/campaign/sync.js +2 -2
- package/dist/mcp/descriptions.js +3 -3
- package/dist/mcp/tools/brief-recipe.js +67 -23
- package/dist/mcp/tools/brief.js +83 -6
- package/dist/recipe/compile/design-parameters-template.js +5 -3
- package/dist/recipe/compile/enumeration.js +6 -11
- package/dist/recipe/compile/shared.js +12 -12
- package/dist/recipe/compile.js +4 -4
- package/dist/recipe/io.d.ts +8 -3
- package/dist/recipe/io.js +11 -81
- package/dist/recipe/items/read-current.js +31 -24
- package/dist/recipe/schema/recipe.d.ts +167 -84
- package/dist/recipe/schema/recipe.js +130 -46
- package/dist/recipe/schema/source-fields.d.ts +20 -0
- package/dist/recipe/schema/source-fields.js +25 -1
- package/dist/recipe/validate.d.ts +3 -3
- package/dist/recipe/validate.js +20 -10
- package/dist/sync/aggregate-kinds.js +1 -0
- package/dist/sync/aggregate.js +2 -2
- package/dist/sync/io.d.ts +13 -3
- package/dist/sync/io.js +43 -17
- package/dist/sync/typescript-recipe.d.ts +30 -0
- package/dist/sync/typescript-recipe.js +112 -0
- package/package.json +1 -1
|
@@ -38,7 +38,7 @@ const runAgentGet = async (options) => {
|
|
|
38
38
|
exports.runAgentGet = runAgentGet;
|
|
39
39
|
const runAgentCreate = async (options) => {
|
|
40
40
|
const { logger, session } = await (0, shared_1.prepare)(options);
|
|
41
|
-
const recipe = (0, sync_1.loadRecipe)(options.file, agent_schema_1.AgentRecipeSchema);
|
|
41
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file, agent_schema_1.AgentRecipeSchema);
|
|
42
42
|
if (findByName(await (0, agents_1.listAgents)(session), recipe.name)) {
|
|
43
43
|
throw (0, errors_1.createScaiError)(`Agent "${recipe.name}" already exists.`, "INPUT_INVALID", {
|
|
44
44
|
hint: "Use `scai agents agent update` to change it, or pick a different name.",
|
|
@@ -67,7 +67,7 @@ const runAgentCreate = async (options) => {
|
|
|
67
67
|
exports.runAgentCreate = runAgentCreate;
|
|
68
68
|
const runAgentUpdate = async (options) => {
|
|
69
69
|
const { logger, session } = await (0, shared_1.prepare)(options);
|
|
70
|
-
const recipe = (0, sync_1.loadRecipe)(options.file, agent_schema_1.AgentRecipeSchema);
|
|
70
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file, agent_schema_1.AgentRecipeSchema);
|
|
71
71
|
const existing = await (0, agents_1.getAgent)(session, options.idOrSlug);
|
|
72
72
|
if (!existing) {
|
|
73
73
|
throw (0, errors_1.createScaiError)(`Agent "${options.idOrSlug}" not found.`, "INPUT_INVALID", {
|
|
@@ -32,7 +32,7 @@ const makeResourceTasks = (config) => {
|
|
|
32
32
|
};
|
|
33
33
|
const runCreate = async (options) => {
|
|
34
34
|
const { logger, session } = await (0, shared_1.prepare)(options);
|
|
35
|
-
const recipe = (0, sync_1.loadRecipe)(options.file, config.schema);
|
|
35
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file, config.schema);
|
|
36
36
|
if (await config.find(session, recipe.name)) {
|
|
37
37
|
throw (0, errors_1.createScaiError)(`A ${config.resource} named "${recipe.name}" already exists.`, "INPUT_INVALID", { hint: `Pick a different name, or use \`scai agents ${config.resource} update\`.` });
|
|
38
38
|
}
|
|
@@ -54,7 +54,7 @@ const makeResourceTasks = (config) => {
|
|
|
54
54
|
(0, shared_1.requireUnverified)(options.unverified, config.resource, "update");
|
|
55
55
|
}
|
|
56
56
|
const { logger, session } = await (0, shared_1.prepare)(options);
|
|
57
|
-
const recipe = (0, sync_1.loadRecipe)(options.file, config.schema);
|
|
57
|
+
const recipe = await (0, sync_1.loadRecipe)(options.file, config.schema);
|
|
58
58
|
let id;
|
|
59
59
|
if (config.resolveById) {
|
|
60
60
|
id = options.idOrName;
|
|
@@ -35,7 +35,7 @@ const runSpaceArtifacts = async (options) => {
|
|
|
35
35
|
exports.runSpaceArtifacts = runSpaceArtifacts;
|
|
36
36
|
const runSpaceUpdate = async (options) => {
|
|
37
37
|
const { logger, session } = await (0, shared_1.prepare)(options);
|
|
38
|
-
const patch = (0, sync_1.loadRecipe)(options.file, SpaceConfigPatchSchema);
|
|
38
|
+
const patch = await (0, sync_1.loadRecipe)(options.file, SpaceConfigPatchSchema);
|
|
39
39
|
const current = await (0, spaces_1.getSpaceConfig)(session, options.spaceId);
|
|
40
40
|
const merged = { ...current, ...patch };
|
|
41
41
|
if (options.whatIf) {
|
package/dist/brand/api/auth.d.ts
CHANGED
|
@@ -32,7 +32,12 @@ import type { BrandCredential } from "../../config/types";
|
|
|
32
32
|
export declare const BRAND_REQUIRED_SCOPES: readonly ["ai.org.br:gen"];
|
|
33
33
|
export interface AcquireBrandTokenOptions {
|
|
34
34
|
orgId: string;
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* The `brand[orgId]` config block, if present. Optional: serverless
|
|
37
|
+
* callers (showcase orchestrator) may have no config file at all and
|
|
38
|
+
* supply credentials via the `SITECOREAI_BRAND_*` env vars instead.
|
|
39
|
+
*/
|
|
40
|
+
credential?: BrandCredential;
|
|
36
41
|
}
|
|
37
42
|
export declare const extractScopes: (token: string) => string[];
|
|
38
43
|
export declare const hasBrandScopes: (token: string) => boolean;
|
|
@@ -45,14 +50,16 @@ export declare const hasBrandScopes: (token: string) => boolean;
|
|
|
45
50
|
* by a previous mint via this function. Reused while it still
|
|
46
51
|
* carries the required scopes; cleared on next 401 by callers.
|
|
47
52
|
* 2. Fresh M2M mint against the `auth.sitecorecloud.io/oauth/token`
|
|
48
|
-
* endpoint with `audience=https://api.sitecorecloud.io
|
|
49
|
-
*
|
|
50
|
-
*
|
|
53
|
+
* endpoint with `audience=https://api.sitecorecloud.io`. The
|
|
54
|
+
* `clientId` + secret + authority + audience are resolved by
|
|
55
|
+
* `resolveBrandSecrets`, which walks: (a) the
|
|
56
|
+
* `SITECOREAI_BRAND_*` env vars (serverless override), then
|
|
57
|
+
* (b) the `brand[orgId]` config block + OS keychain. Successful
|
|
58
|
+
* mints are cached.
|
|
51
59
|
*
|
|
52
|
-
* Refuses with `AUTH_BRAND_REQUIRED` when
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* just re-login.
|
|
60
|
+
* Refuses with `AUTH_BRAND_REQUIRED` when neither tier supplies a
|
|
61
|
+
* credential. The error message points operators at both the OS
|
|
62
|
+
* keychain login flow AND the serverless env-var path so the right
|
|
63
|
+
* fix is one re-read away.
|
|
57
64
|
*/
|
|
58
65
|
export declare const acquireBrandToken: (options: AcquireBrandTokenOptions) => Promise<string>;
|
package/dist/brand/api/auth.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.acquireBrandToken = exports.hasBrandScopes = exports.extractScopes = exp
|
|
|
4
4
|
const auth_1 = require("../../serialization/api/auth");
|
|
5
5
|
const errors_1 = require("../../shared/errors");
|
|
6
6
|
const keychain_1 = require("../../shared/keychain");
|
|
7
|
+
const credential_1 = require("../credential");
|
|
7
8
|
/**
|
|
8
9
|
* OAuth scopes scai's *currently shipped* Brand operations require.
|
|
9
10
|
*
|
|
@@ -35,7 +36,7 @@ const keychain_1 = require("../../shared/keychain");
|
|
|
35
36
|
* if their specific scope isn't present.
|
|
36
37
|
*/
|
|
37
38
|
exports.BRAND_REQUIRED_SCOPES = ["ai.org.br:gen"];
|
|
38
|
-
const NO_CREDENTIAL_HINT = "Run `scai setup login brand --env <env>` to provision the credential, or paste an existing AI APIs key into `brand.<orgId>` in sitecoreai.cli.json (clientId only; secret goes through the keychain via the login flow). Create the credential in Cloud Portal → Stream → Admin → AI APIs keys.";
|
|
39
|
+
const NO_CREDENTIAL_HINT = "Run `scai setup login brand --env <env>` to provision the credential, or paste an existing AI APIs key into `brand.<orgId>` in sitecoreai.cli.json (clientId only; secret goes through the keychain via the login flow). Create the credential in Cloud Portal → Stream → Admin → AI APIs keys. In serverless contexts (Vercel functions, CI runners) without an OS keychain, set the SITECOREAI_BRAND_CLIENT_ID + SITECOREAI_BRAND_CLIENT_SECRET env vars instead — they take precedence over the config+keychain pair.";
|
|
39
40
|
const decodeJwtPayload = (token) => {
|
|
40
41
|
const parts = token.split(".");
|
|
41
42
|
if (parts.length !== 3) {
|
|
@@ -67,7 +68,6 @@ const hasBrandScopes = (token) => {
|
|
|
67
68
|
return exports.BRAND_REQUIRED_SCOPES.every((s) => granted.has(s));
|
|
68
69
|
};
|
|
69
70
|
exports.hasBrandScopes = hasBrandScopes;
|
|
70
|
-
const DEFAULT_AUTHORITY = "https://auth.sitecorecloud.io";
|
|
71
71
|
/**
|
|
72
72
|
* Returns a Bearer JWT for the Sitecore Brand APIs.
|
|
73
73
|
*
|
|
@@ -77,15 +77,17 @@ const DEFAULT_AUTHORITY = "https://auth.sitecorecloud.io";
|
|
|
77
77
|
* by a previous mint via this function. Reused while it still
|
|
78
78
|
* carries the required scopes; cleared on next 401 by callers.
|
|
79
79
|
* 2. Fresh M2M mint against the `auth.sitecorecloud.io/oauth/token`
|
|
80
|
-
* endpoint with `audience=https://api.sitecorecloud.io
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
* endpoint with `audience=https://api.sitecorecloud.io`. The
|
|
81
|
+
* `clientId` + secret + authority + audience are resolved by
|
|
82
|
+
* `resolveBrandSecrets`, which walks: (a) the
|
|
83
|
+
* `SITECOREAI_BRAND_*` env vars (serverless override), then
|
|
84
|
+
* (b) the `brand[orgId]` config block + OS keychain. Successful
|
|
85
|
+
* mints are cached.
|
|
83
86
|
*
|
|
84
|
-
* Refuses with `AUTH_BRAND_REQUIRED` when
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
* just re-login.
|
|
87
|
+
* Refuses with `AUTH_BRAND_REQUIRED` when neither tier supplies a
|
|
88
|
+
* credential. The error message points operators at both the OS
|
|
89
|
+
* keychain login flow AND the serverless env-var path so the right
|
|
90
|
+
* fix is one re-read away.
|
|
89
91
|
*/
|
|
90
92
|
const acquireBrandToken = async (options) => {
|
|
91
93
|
const { orgId, credential } = options;
|
|
@@ -93,25 +95,33 @@ const acquireBrandToken = async (options) => {
|
|
|
93
95
|
// server-side scope enforcement happens on the API call itself,
|
|
94
96
|
// and gating here would skip a perfectly usable token whenever
|
|
95
97
|
// the operation it's used for doesn't need the validated scope.
|
|
98
|
+
//
|
|
99
|
+
// NB: in a serverless context (no keychain) the cache lookup
|
|
100
|
+
// silently returns undefined via the keychain wrapper's
|
|
101
|
+
// fail-closed behaviour, so we drop straight to the mint path —
|
|
102
|
+
// every invocation re-mints, which is correct: there is no
|
|
103
|
+
// persistent process to hold a cached token across invocations.
|
|
96
104
|
const cached = await (0, keychain_1.getBrandToken)(orgId);
|
|
97
105
|
if (cached) {
|
|
98
106
|
return cached;
|
|
99
107
|
}
|
|
100
|
-
// 2. Fresh M2M mint via the AI APIs key.
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
// 2. Fresh M2M mint via the AI APIs key. `resolveBrandSecrets`
|
|
109
|
+
// picks env-var vs config+keychain; partial-env states throw
|
|
110
|
+
// inside the resolver (so the operator sees the malformed-env
|
|
111
|
+
// hint, not a generic missing-credential one), an empty result
|
|
112
|
+
// here means neither tier had anything to offer.
|
|
113
|
+
const secrets = await (0, credential_1.resolveBrandSecrets)({ orgId, credential });
|
|
114
|
+
if (!secrets) {
|
|
103
115
|
throw (0, errors_1.createScaiError)(`No Brand credential is configured for org '${orgId}'.`, "AUTH_BRAND_REQUIRED", { hint: NO_CREDENTIAL_HINT });
|
|
104
116
|
}
|
|
105
|
-
const authority = credential.authority ?? DEFAULT_AUTHORITY;
|
|
106
|
-
const audience = credential.audience ?? auth_1.DEFAULT_SITECORE_API_AUDIENCE;
|
|
107
117
|
// Reuse the shared client-credentials helper. It accepts a
|
|
108
118
|
// `SitecoreApiClientOptions`-shaped argument; we only need the
|
|
109
119
|
// auth-relevant fields.
|
|
110
120
|
const mintEnv = {
|
|
111
|
-
authority,
|
|
112
|
-
clientId:
|
|
113
|
-
clientSecret,
|
|
114
|
-
audience,
|
|
121
|
+
authority: secrets.authority,
|
|
122
|
+
clientId: secrets.clientId,
|
|
123
|
+
clientSecret: secrets.clientSecret,
|
|
124
|
+
audience: secrets.audience,
|
|
115
125
|
};
|
|
116
126
|
let result;
|
|
117
127
|
try {
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
* Brand credential resolution shared by every `scai brand` surface and
|
|
3
3
|
* by `scai setup login brand`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Three layers:
|
|
6
6
|
* - `resolveBrandOrgId` — the pure orgId-resolution rule.
|
|
7
7
|
* - `resolveBrandClient` — reads the config, resolves the orgId, looks
|
|
8
8
|
* up the `brand[orgId]` credential, and returns the API client
|
|
9
9
|
* options a brand operation needs.
|
|
10
|
+
* - `resolveBrandSecrets` — pulls the `{ clientId, clientSecret,
|
|
11
|
+
* authority, audience }` quartet a token mint needs. Env-var
|
|
12
|
+
* overrides take precedence over the config-and-keychain pair so
|
|
13
|
+
* serverless callers (the showcase orchestrator) can drive brand
|
|
14
|
+
* ops without an OS keychain.
|
|
10
15
|
*
|
|
11
16
|
* (`src/brand/recipe/client.ts` has a separate `resolveBrandClient` that
|
|
12
17
|
* resolves from a sync `SyncContext` rather than CLI options — the sync
|
|
@@ -14,6 +19,7 @@
|
|
|
14
19
|
* the fallbacks here.)
|
|
15
20
|
*/
|
|
16
21
|
import type { BrandApiClientOptions } from "./api/client";
|
|
22
|
+
import type { BrandCredential } from "../config/types";
|
|
17
23
|
/**
|
|
18
24
|
* Resolve the Sitecore `organizationId` for a Brand credential.
|
|
19
25
|
* Resolution order:
|
|
@@ -53,3 +59,67 @@ export interface BrandClientResolveOptions {
|
|
|
53
59
|
* first.
|
|
54
60
|
*/
|
|
55
61
|
export declare const resolveBrandClient: (options: BrandClientResolveOptions) => BrandApiClientOptions;
|
|
62
|
+
/**
|
|
63
|
+
* The `{ clientId, clientSecret, authority, audience }` a Brand-API
|
|
64
|
+
* client-credentials mint needs, plus the tier that supplied each
|
|
65
|
+
* field for diagnostics.
|
|
66
|
+
*/
|
|
67
|
+
export interface ResolvedBrandSecrets {
|
|
68
|
+
clientId: string;
|
|
69
|
+
clientSecret: string;
|
|
70
|
+
authority: string;
|
|
71
|
+
audience: string;
|
|
72
|
+
/**
|
|
73
|
+
* Which tier the **secret** came from. `"env"` means the
|
|
74
|
+
* orchestrator-style override; `"keychain"` is the developer-laptop
|
|
75
|
+
* default. The `clientId` / `authority` / `audience` may still come
|
|
76
|
+
* from the config when the secret is from the env (one-shot override
|
|
77
|
+
* of just the secret is supported and useful for short-lived CI
|
|
78
|
+
* secrets paired with a config-pinned client).
|
|
79
|
+
*/
|
|
80
|
+
source: "env" | "keychain";
|
|
81
|
+
}
|
|
82
|
+
export interface ResolveBrandSecretsOptions {
|
|
83
|
+
/** Sitecore organization id — keys the `getBrandClientSecret` slot. */
|
|
84
|
+
orgId: string;
|
|
85
|
+
/** `brand[orgId]` block from the root config — may be absent in serverless. */
|
|
86
|
+
credential?: BrandCredential;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the secrets a Brand-API M2M mint needs, walking two tiers:
|
|
90
|
+
*
|
|
91
|
+
* 1. **Environment variables** (`SITECOREAI_BRAND_CLIENT_ID` +
|
|
92
|
+
* `SITECOREAI_BRAND_CLIENT_SECRET`). Both must be present for this
|
|
93
|
+
* tier to apply. `SITECOREAI_BRAND_AUTHORITY` and
|
|
94
|
+
* `SITECOREAI_BRAND_AUDIENCE` are optional overrides that compose
|
|
95
|
+
* with either tier.
|
|
96
|
+
* 2. **Config + OS keychain** — the developer-laptop default: client
|
|
97
|
+
* id from the `brand[orgId]` config block, secret from the keychain
|
|
98
|
+
* (`getBrandClientSecret`).
|
|
99
|
+
*
|
|
100
|
+
* **Precedence:** env wins. Rationale — the env-var path is the
|
|
101
|
+
* explicit, per-invocation override a serverless host (Vercel function,
|
|
102
|
+
* CI runner) sets right before calling scai; treating it as the
|
|
103
|
+
* higher-priority lookup means a host can swap credentials between
|
|
104
|
+
* invocations without touching disk or the keychain. The keychain path
|
|
105
|
+
* is the long-lived default for human developers.
|
|
106
|
+
*
|
|
107
|
+
* Error contract — `AUTH_BRAND_REQUIRED` in three flavors so operators
|
|
108
|
+
* know which knob to turn:
|
|
109
|
+
*
|
|
110
|
+
* - **Partial env**: one of `SITECOREAI_BRAND_CLIENT_ID` /
|
|
111
|
+
* `SITECOREAI_BRAND_CLIENT_SECRET` is set but the other isn't.
|
|
112
|
+
* We treat that as malformed (operator intended env-tier but
|
|
113
|
+
* missed a var) rather than silently falling through to the
|
|
114
|
+
* keychain — silent fallthrough would mask a typo and hand the
|
|
115
|
+
* wrong credential to the API.
|
|
116
|
+
* - **No credential anywhere**: neither env nor keychain has the
|
|
117
|
+
* secret, and the config has no `clientId` for the org.
|
|
118
|
+
* - **Keychain only, no secret**: the config has a `clientId` but
|
|
119
|
+
* the keychain lookup came back empty (re-login needed).
|
|
120
|
+
*
|
|
121
|
+
* Returns `undefined` only as a sentinel for the "no credential
|
|
122
|
+
* anywhere" case so the caller can build the right hint with the
|
|
123
|
+
* orgId in scope. Other malformed states throw immediately.
|
|
124
|
+
*/
|
|
125
|
+
export declare const resolveBrandSecrets: (options: ResolveBrandSecretsOptions) => Promise<ResolvedBrandSecrets | undefined>;
|
package/dist/brand/credential.js
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
* Brand credential resolution shared by every `scai brand` surface and
|
|
4
4
|
* by `scai setup login brand`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
6
|
+
* Three layers:
|
|
7
7
|
* - `resolveBrandOrgId` — the pure orgId-resolution rule.
|
|
8
8
|
* - `resolveBrandClient` — reads the config, resolves the orgId, looks
|
|
9
9
|
* up the `brand[orgId]` credential, and returns the API client
|
|
10
10
|
* options a brand operation needs.
|
|
11
|
+
* - `resolveBrandSecrets` — pulls the `{ clientId, clientSecret,
|
|
12
|
+
* authority, audience }` quartet a token mint needs. Env-var
|
|
13
|
+
* overrides take precedence over the config-and-keychain pair so
|
|
14
|
+
* serverless callers (the showcase orchestrator) can drive brand
|
|
15
|
+
* ops without an OS keychain.
|
|
11
16
|
*
|
|
12
17
|
* (`src/brand/recipe/client.ts` has a separate `resolveBrandClient` that
|
|
13
18
|
* resolves from a sync `SyncContext` rather than CLI options — the sync
|
|
@@ -15,10 +20,11 @@
|
|
|
15
20
|
* the fallbacks here.)
|
|
16
21
|
*/
|
|
17
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.resolveBrandClient = exports.resolveBrandOrgId = void 0;
|
|
23
|
+
exports.resolveBrandSecrets = exports.resolveBrandClient = exports.resolveBrandOrgId = void 0;
|
|
19
24
|
const root_config_1 = require("../config/root-config");
|
|
20
25
|
const errors_1 = require("../shared/errors");
|
|
21
26
|
const cli_tasks_1 = require("../shared/cli-tasks");
|
|
27
|
+
const keychain_1 = require("../shared/keychain");
|
|
22
28
|
/**
|
|
23
29
|
* Resolve the Sitecore `organizationId` for a Brand credential.
|
|
24
30
|
* Resolution order:
|
|
@@ -85,3 +91,114 @@ const resolveBrandClient = (options) => {
|
|
|
85
91
|
return { orgId, credential };
|
|
86
92
|
};
|
|
87
93
|
exports.resolveBrandClient = resolveBrandClient;
|
|
94
|
+
/**
|
|
95
|
+
* Default Auth0 authority for Brand credentials. Kept aligned with the
|
|
96
|
+
* one `acquireBrandToken` previously hard-coded so callers that drop in
|
|
97
|
+
* `resolveBrandSecrets` see no behavioural change.
|
|
98
|
+
*/
|
|
99
|
+
const DEFAULT_BRAND_AUTHORITY = "https://auth.sitecorecloud.io";
|
|
100
|
+
const DEFAULT_BRAND_AUDIENCE = "https://api.sitecorecloud.io";
|
|
101
|
+
/**
|
|
102
|
+
* Env-var names the serverless fallback honors. The serverless caller —
|
|
103
|
+
* today the showcase orchestrator's `brandkit_deploy` handler — sets
|
|
104
|
+
* these per invocation: there is no shared OS keychain in a Vercel
|
|
105
|
+
* function, so the credential MUST come from the environment.
|
|
106
|
+
*
|
|
107
|
+
* Generic names (no per-org / per-env normalization) on purpose: a
|
|
108
|
+
* single function invocation is always scoped to one org, and the host
|
|
109
|
+
* sets the env vars right before calling scai. If a future caller needs
|
|
110
|
+
* multi-org concurrency in one process, this will need a key-normalized
|
|
111
|
+
* variant — but that day hasn't come.
|
|
112
|
+
*/
|
|
113
|
+
const BRAND_ENV_CLIENT_ID = "SITECOREAI_BRAND_CLIENT_ID";
|
|
114
|
+
const BRAND_ENV_CLIENT_SECRET = "SITECOREAI_BRAND_CLIENT_SECRET";
|
|
115
|
+
const BRAND_ENV_AUTHORITY = "SITECOREAI_BRAND_AUTHORITY";
|
|
116
|
+
const BRAND_ENV_AUDIENCE = "SITECOREAI_BRAND_AUDIENCE";
|
|
117
|
+
const trimmedEnv = (key) => {
|
|
118
|
+
const raw = process.env[key];
|
|
119
|
+
if (typeof raw !== "string")
|
|
120
|
+
return undefined;
|
|
121
|
+
const trimmed = raw.trim();
|
|
122
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the secrets a Brand-API M2M mint needs, walking two tiers:
|
|
126
|
+
*
|
|
127
|
+
* 1. **Environment variables** (`SITECOREAI_BRAND_CLIENT_ID` +
|
|
128
|
+
* `SITECOREAI_BRAND_CLIENT_SECRET`). Both must be present for this
|
|
129
|
+
* tier to apply. `SITECOREAI_BRAND_AUTHORITY` and
|
|
130
|
+
* `SITECOREAI_BRAND_AUDIENCE` are optional overrides that compose
|
|
131
|
+
* with either tier.
|
|
132
|
+
* 2. **Config + OS keychain** — the developer-laptop default: client
|
|
133
|
+
* id from the `brand[orgId]` config block, secret from the keychain
|
|
134
|
+
* (`getBrandClientSecret`).
|
|
135
|
+
*
|
|
136
|
+
* **Precedence:** env wins. Rationale — the env-var path is the
|
|
137
|
+
* explicit, per-invocation override a serverless host (Vercel function,
|
|
138
|
+
* CI runner) sets right before calling scai; treating it as the
|
|
139
|
+
* higher-priority lookup means a host can swap credentials between
|
|
140
|
+
* invocations without touching disk or the keychain. The keychain path
|
|
141
|
+
* is the long-lived default for human developers.
|
|
142
|
+
*
|
|
143
|
+
* Error contract — `AUTH_BRAND_REQUIRED` in three flavors so operators
|
|
144
|
+
* know which knob to turn:
|
|
145
|
+
*
|
|
146
|
+
* - **Partial env**: one of `SITECOREAI_BRAND_CLIENT_ID` /
|
|
147
|
+
* `SITECOREAI_BRAND_CLIENT_SECRET` is set but the other isn't.
|
|
148
|
+
* We treat that as malformed (operator intended env-tier but
|
|
149
|
+
* missed a var) rather than silently falling through to the
|
|
150
|
+
* keychain — silent fallthrough would mask a typo and hand the
|
|
151
|
+
* wrong credential to the API.
|
|
152
|
+
* - **No credential anywhere**: neither env nor keychain has the
|
|
153
|
+
* secret, and the config has no `clientId` for the org.
|
|
154
|
+
* - **Keychain only, no secret**: the config has a `clientId` but
|
|
155
|
+
* the keychain lookup came back empty (re-login needed).
|
|
156
|
+
*
|
|
157
|
+
* Returns `undefined` only as a sentinel for the "no credential
|
|
158
|
+
* anywhere" case so the caller can build the right hint with the
|
|
159
|
+
* orgId in scope. Other malformed states throw immediately.
|
|
160
|
+
*/
|
|
161
|
+
const resolveBrandSecrets = async (options) => {
|
|
162
|
+
const envClientId = trimmedEnv(BRAND_ENV_CLIENT_ID);
|
|
163
|
+
const envClientSecret = trimmedEnv(BRAND_ENV_CLIENT_SECRET);
|
|
164
|
+
const envAuthority = trimmedEnv(BRAND_ENV_AUTHORITY);
|
|
165
|
+
const envAudience = trimmedEnv(BRAND_ENV_AUDIENCE);
|
|
166
|
+
// Tier 1: env vars. Require BOTH the id and the secret — a partial
|
|
167
|
+
// pair is almost always a misconfiguration (forgot to set the other
|
|
168
|
+
// half), and silently falling through to a different credential
|
|
169
|
+
// tier would surface as a confusing 401 from Sitecore rather than
|
|
170
|
+
// the real "your env is malformed" message.
|
|
171
|
+
if (envClientId && envClientSecret) {
|
|
172
|
+
return {
|
|
173
|
+
clientId: envClientId,
|
|
174
|
+
clientSecret: envClientSecret,
|
|
175
|
+
authority: envAuthority ?? options.credential?.authority ?? DEFAULT_BRAND_AUTHORITY,
|
|
176
|
+
audience: envAudience ?? options.credential?.audience ?? DEFAULT_BRAND_AUDIENCE,
|
|
177
|
+
source: "env",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (envClientId || envClientSecret) {
|
|
181
|
+
const missing = envClientId ? BRAND_ENV_CLIENT_SECRET : BRAND_ENV_CLIENT_ID;
|
|
182
|
+
throw (0, errors_1.createScaiError)(`Brand credential env vars are partially set: ${missing} is missing.`, "AUTH_BRAND_REQUIRED", {
|
|
183
|
+
hint: `Set both ${BRAND_ENV_CLIENT_ID} and ${BRAND_ENV_CLIENT_SECRET}, or unset both to fall back to the OS keychain.`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Tier 2: config + OS keychain. The config supplies the non-secret
|
|
187
|
+
// metadata (clientId / authority / audience); the keychain supplies
|
|
188
|
+
// the secret.
|
|
189
|
+
if (!options.credential?.clientId) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
const keychainSecret = await (0, keychain_1.getBrandClientSecret)(options.orgId);
|
|
193
|
+
if (!keychainSecret) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
clientId: options.credential.clientId,
|
|
198
|
+
clientSecret: keychainSecret,
|
|
199
|
+
authority: envAuthority ?? options.credential.authority ?? DEFAULT_BRAND_AUTHORITY,
|
|
200
|
+
audience: envAudience ?? options.credential.audience ?? DEFAULT_BRAND_AUDIENCE,
|
|
201
|
+
source: "keychain",
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
exports.resolveBrandSecrets = resolveBrandSecrets;
|
|
@@ -31,11 +31,16 @@ const diffBrandKit = (desired, current) => {
|
|
|
31
31
|
meta: { stage: "kit", description: desired.description, industry: desired.industry },
|
|
32
32
|
});
|
|
33
33
|
desired.documents.forEach((document, index) => {
|
|
34
|
+
// The discriminated union has two shapes — `url` carries a URL,
|
|
35
|
+
// `registry-file` carries a `path`. The diff describes the
|
|
36
|
+
// change in human terms only; the apply step is where the
|
|
37
|
+
// registry-file → URL contract is enforced.
|
|
38
|
+
const label = document.kind === "url" ? document.url : `registry-file:${document.path}`;
|
|
34
39
|
changes.push({
|
|
35
40
|
kind: "create",
|
|
36
41
|
path: `documents[${index}]`,
|
|
37
|
-
summary: `Upload + ingest ${
|
|
38
|
-
after:
|
|
42
|
+
summary: `Upload + ingest ${label}`,
|
|
43
|
+
after: label,
|
|
39
44
|
meta: { stage: "document", document },
|
|
40
45
|
});
|
|
41
46
|
});
|
|
Binary file
|
|
@@ -5,7 +5,23 @@
|
|
|
5
5
|
* This schema is the single source of truth for the `brand-kit` recipe
|
|
6
6
|
* kind: it validates recipe files, drives the `sync` CLI, and becomes
|
|
7
7
|
* the MCP tool input schema. Keep the `.describe()` text accurate — the
|
|
8
|
-
* model reads it.
|
|
8
|
+
* model reads it.
|
|
9
|
+
*
|
|
10
|
+
* Two authoring shapes share this schema:
|
|
11
|
+
*
|
|
12
|
+
* - **scai-native** — flat object with `name`, `documents`, `sections`.
|
|
13
|
+
* No `kind` / `schemaVersion` / `handle`. Used by hand-authored
|
|
14
|
+
* `.brandkit.yaml` files and the `scai brand sync pull` capture
|
|
15
|
+
* path. Stays valid: the discriminator fields are optional here.
|
|
16
|
+
* - **registry-superset** — the richer shape the `@registry`
|
|
17
|
+
* `sitecore-recipes.ts` exports use: adds `kind: "brandkit"`,
|
|
18
|
+
* `schemaVersion: "1"`, `handle`, `displayName`, and a
|
|
19
|
+
* discriminated `documents[]` shape (`url` | `registry-file`)
|
|
20
|
+
* with `tags` / `sections` hints per document. The orchestrator
|
|
21
|
+
* passes recipes through unchanged from the registry to scai;
|
|
22
|
+
* scai's parser accepts the extra fields without stripping them.
|
|
23
|
+
*
|
|
24
|
+
* See docs/recipe-sync-architecture.md.
|
|
9
25
|
*/
|
|
10
26
|
import { z } from "zod";
|
|
11
27
|
/** A `richArray`-field entry: text plus optional tags and a constraint. */
|
|
@@ -24,31 +40,121 @@ export declare const BrandFieldValueSchema: z.ZodUnion<readonly [z.ZodString, z.
|
|
|
24
40
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
25
41
|
restrictions: z.ZodOptional<z.ZodString>;
|
|
26
42
|
}, z.core.$strip>>]>;
|
|
27
|
-
/**
|
|
28
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Canonical Sitecore AI brand-kit section names — the seven buckets
|
|
45
|
+
* the EnrichSections pipeline produces. Mirrors
|
|
46
|
+
* `BRAND_KIT_CANONICAL_SECTIONS` in the registry's recipe definitions.
|
|
47
|
+
* Exported so callers building recipes have a stable list to bias
|
|
48
|
+
* `documents[].sections` against.
|
|
49
|
+
*/
|
|
50
|
+
export declare const BRAND_KIT_CANONICAL_SECTIONS: readonly ["Brand Context", "Global Goals", "Tone of Voice", "Glossary and Localization", "Do's and Don'ts", "Grammar Checklists", "Visual Guidelines"];
|
|
51
|
+
export type BrandKitCanonicalSection = (typeof BRAND_KIT_CANONICAL_SECTIONS)[number];
|
|
52
|
+
/**
|
|
53
|
+
* A brand document referenced by URL. Sitecore's Documents API fetches
|
|
54
|
+
* the URL server-side and copies the bytes into MMS. The default
|
|
55
|
+
* variant — what `scai brand sync pull` emits when capturing a live
|
|
56
|
+
* kit.
|
|
57
|
+
*/
|
|
58
|
+
export declare const BrandUrlDocumentSchema: z.ZodObject<{
|
|
59
|
+
title: z.ZodOptional<z.ZodString>;
|
|
60
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
61
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
62
|
+
sections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
63
|
+
kind: z.ZodLiteral<"url">;
|
|
29
64
|
url: z.ZodString;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
/**
|
|
67
|
+
* A brand document stored alongside the recipe in the registry repo.
|
|
68
|
+
* The `path` is relative to the recipe file's directory. scai itself
|
|
69
|
+
* does **not** upload these — the Sitecore Documents API has no
|
|
70
|
+
* working bytes-upload path (see
|
|
71
|
+
* `src/brand/documents/upload.ts::LOCAL_UPLOAD_UNSUPPORTED_MESSAGE`),
|
|
72
|
+
* so the orchestrator (or whoever owns the recipe before scai sees
|
|
73
|
+
* it) MUST translate `registry-file` entries to `url` entries
|
|
74
|
+
* pointing at an HTTP-reachable host before invoking `scai brand
|
|
75
|
+
* sync push`. The seed runner rejects unresolved `registry-file`
|
|
76
|
+
* documents with a clear hint; this stays in the schema as an
|
|
77
|
+
* authoring-time shape so registry-side recipes round-trip cleanly.
|
|
78
|
+
*/
|
|
79
|
+
export declare const BrandRegistryFileDocumentSchema: z.ZodObject<{
|
|
30
80
|
title: z.ZodOptional<z.ZodString>;
|
|
31
81
|
summary: z.ZodOptional<z.ZodString>;
|
|
82
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
83
|
+
sections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
84
|
+
kind: z.ZodLiteral<"registry-file">;
|
|
85
|
+
path: z.ZodString;
|
|
32
86
|
}, z.core.$strip>;
|
|
87
|
+
/**
|
|
88
|
+
* A brand document to ingest. Two variants — `{ kind: "url", url }` and
|
|
89
|
+
* `{ kind: "registry-file", path }` — sharing optional `title` /
|
|
90
|
+
* `summary` / `tags` / `sections` ingestion hints.
|
|
91
|
+
*
|
|
92
|
+
* Back-compat: scai-native recipes that pre-date the discriminator wrote
|
|
93
|
+
* documents as the flat `{ url, title?, summary? }` shape. The
|
|
94
|
+
* `z.preprocess` step defaults a missing `kind` to `"url"` whenever
|
|
95
|
+
* `url` is present, so legacy recipes keep parsing without a migration
|
|
96
|
+
* step. Zod 4's discriminated unions require the discriminator to be
|
|
97
|
+
* present BEFORE routing, so `.default("url")` inline on the literal
|
|
98
|
+
* doesn't work — preprocess is the correct seam.
|
|
99
|
+
*/
|
|
100
|
+
export declare const BrandDocumentSchema: z.ZodPreprocess<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
101
|
+
title: z.ZodOptional<z.ZodString>;
|
|
102
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
103
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
104
|
+
sections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
105
|
+
kind: z.ZodLiteral<"url">;
|
|
106
|
+
url: z.ZodString;
|
|
107
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
108
|
+
title: z.ZodOptional<z.ZodString>;
|
|
109
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
110
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
111
|
+
sections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
112
|
+
kind: z.ZodLiteral<"registry-file">;
|
|
113
|
+
path: z.ZodString;
|
|
114
|
+
}, z.core.$strip>], "kind">>;
|
|
33
115
|
/** The full brand-kit recipe. */
|
|
34
116
|
export declare const BrandKitRecipeSchema: z.ZodObject<{
|
|
117
|
+
kind: z.ZodOptional<z.ZodLiteral<"brandkit">>;
|
|
118
|
+
schemaVersion: z.ZodOptional<z.ZodLiteral<"1">>;
|
|
119
|
+
handle: z.ZodOptional<z.ZodString>;
|
|
35
120
|
name: z.ZodString;
|
|
121
|
+
displayName: z.ZodOptional<z.ZodString>;
|
|
36
122
|
description: z.ZodOptional<z.ZodString>;
|
|
37
123
|
industry: z.ZodOptional<z.ZodString>;
|
|
38
|
-
documents: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
124
|
+
documents: z.ZodDefault<z.ZodArray<z.ZodPreprocess<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
125
|
+
title: z.ZodOptional<z.ZodString>;
|
|
126
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
127
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
128
|
+
sections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
129
|
+
kind: z.ZodLiteral<"url">;
|
|
39
130
|
url: z.ZodString;
|
|
131
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
40
132
|
title: z.ZodOptional<z.ZodString>;
|
|
41
133
|
summary: z.ZodOptional<z.ZodString>;
|
|
42
|
-
|
|
134
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
135
|
+
sections: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
136
|
+
kind: z.ZodLiteral<"registry-file">;
|
|
137
|
+
path: z.ZodString;
|
|
138
|
+
}, z.core.$strip>], "kind">>>>;
|
|
43
139
|
sections: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>, z.ZodArray<z.ZodObject<{
|
|
44
140
|
name: z.ZodString;
|
|
45
141
|
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
46
142
|
restrictions: z.ZodOptional<z.ZodString>;
|
|
47
143
|
}, z.core.$strip>>]>>>>;
|
|
48
144
|
}, z.core.$strip>;
|
|
49
|
-
/** A validated brand-kit recipe. */
|
|
145
|
+
/** A validated brand-kit recipe (parsed form — defaults materialised). */
|
|
50
146
|
export type BrandKitRecipe = z.infer<typeof BrandKitRecipeSchema>;
|
|
147
|
+
/**
|
|
148
|
+
* Input alias — the shape an *author* writes. Same as
|
|
149
|
+
* `BrandKitRecipe` minus the `.default(...)` clauses; useful for
|
|
150
|
+
* loaders and TypeScript-authored recipe modules.
|
|
151
|
+
*/
|
|
152
|
+
export type BrandKitRecipeInput = z.input<typeof BrandKitRecipeSchema>;
|
|
51
153
|
/** A brand-kit field value (text / name list / rich entries). */
|
|
52
154
|
export type BrandFieldValue = z.infer<typeof BrandFieldValueSchema>;
|
|
53
|
-
/** A brand document reference within a recipe. */
|
|
155
|
+
/** A brand document reference within a recipe (URL or registry-file). */
|
|
54
156
|
export type BrandDocument = z.infer<typeof BrandDocumentSchema>;
|
|
157
|
+
/** A URL-form brand document (post-parse — `kind` is the literal). */
|
|
158
|
+
export type BrandUrlDocument = z.infer<typeof BrandUrlDocumentSchema>;
|
|
159
|
+
/** A registry-file-form brand document (post-parse — `kind` is the literal). */
|
|
160
|
+
export type BrandRegistryFileDocument = z.infer<typeof BrandRegistryFileDocumentSchema>;
|