@sequenceholdings/studio-cli 0.1.13 → 0.1.21
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 +258 -38
- package/dist/agents/apply-chunks.d.ts +13 -0
- package/dist/agents/apply-chunks.js +43 -0
- package/dist/agents/commands.d.ts +10 -0
- package/dist/agents/commands.js +218 -0
- package/dist/agents/scaffold.d.ts +2 -0
- package/dist/agents/scaffold.js +77 -0
- package/dist/agents/source.d.ts +18 -0
- package/dist/agents/source.js +121 -0
- package/dist/artifact/delegate.d.ts +2 -2
- package/dist/artifact/delegate.js +31 -73
- package/dist/atlas-client.js +52 -37
- package/dist/auth-cmds/commands.d.ts +1 -1
- package/dist/auth-cmds/commands.js +12 -7
- package/dist/auth.d.ts +104 -24
- package/dist/auth.js +456 -94
- package/dist/config.d.ts +3 -3
- package/dist/config.js +18 -13
- package/dist/env-catalog.js +13 -3
- package/dist/env-flags.d.ts +2 -0
- package/dist/env-flags.js +2 -0
- package/dist/env-registry.d.ts +27 -0
- package/dist/env-registry.js +204 -0
- package/dist/envs/commands.d.ts +1 -1
- package/dist/envs/commands.js +41 -3
- package/dist/file-lock.d.ts +5 -0
- package/dist/file-lock.js +187 -0
- package/dist/functions/commands.d.ts +10 -10
- package/dist/functions/commands.js +87 -53
- package/dist/functions/manifest.d.ts +1 -0
- package/dist/functions/manifest.js +36 -0
- package/dist/functions/source-selection.d.ts +24 -0
- package/dist/functions/source-selection.js +67 -0
- package/dist/login.d.ts +8 -3
- package/dist/login.js +46 -34
- package/dist/main.d.ts +3 -1
- package/dist/main.js +41 -12
- package/dist/orm/delegate.js +25 -7
- package/dist/pat-hints.js +2 -2
- package/dist/pipeline/commands.d.ts +58 -0
- package/dist/pipeline/commands.js +330 -0
- package/dist/pipeline/lifecycle.d.ts +58 -0
- package/dist/pipeline/lifecycle.js +348 -0
- package/dist/pipeline/pinning.d.ts +5 -0
- package/dist/pipeline/pinning.js +9 -0
- package/dist/pipeline/templates.d.ts +11 -0
- package/dist/pipeline/templates.js +166 -0
- package/dist/process/build.d.ts +4 -0
- package/dist/process/build.js +33 -2
- package/dist/process/codegen.js +19 -1
- package/dist/process/commands.js +97 -47
- package/dist/process/compiler-subprocess.d.ts +29 -0
- package/dist/process/compiler-subprocess.js +99 -0
- package/dist/process/compiler-worker.d.ts +1 -0
- package/dist/process/compiler-worker.js +38 -0
- package/dist/process/lint.d.ts +8 -0
- package/dist/process/lint.js +84 -29
- package/dist/process/repo-install.js +18 -2
- package/dist/repos/commands.d.ts +1 -1
- package/dist/repos/commands.js +17 -12
- package/dist/secrets/commands.d.ts +1 -1
- package/dist/secrets/commands.js +18 -18
- package/package.json +12 -5
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { type AuthMode } from '../auth.js';
|
|
1
2
|
import { type ResolvedEnv } from '../config.js';
|
|
2
3
|
import type { ParsedArgs } from '../process/commands.js';
|
|
3
4
|
import { type ManagedFunctionManifest } from './manifest.js';
|
|
4
|
-
|
|
5
|
+
export { parseFunctionsSourceSelection, parseFunctionsSourceSpec, resolveFunctionSourceDir } from './source-selection.js';
|
|
5
6
|
export declare const LOG = "[seq-studio]";
|
|
6
7
|
export interface FunctionSummary {
|
|
7
8
|
id: string;
|
|
@@ -32,9 +33,16 @@ export interface VersionSummary {
|
|
|
32
33
|
}
|
|
33
34
|
export declare function flagBool(flags: ParsedArgs['flags'], ...keys: string[]): boolean;
|
|
34
35
|
export interface CommandContext {
|
|
36
|
+
authMode?: AuthMode;
|
|
35
37
|
env: ResolvedEnv;
|
|
36
38
|
token: string;
|
|
37
39
|
}
|
|
40
|
+
export declare function requestedEnvironment(args: ParsedArgs): string | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Resolve env + token for Atlas network commands. Requires an explicit
|
|
43
|
+
* `-e/--env` so partners never hit the built-in `local` default and get an
|
|
44
|
+
* opaque `fetch failed` (same contract as `functions deploy`).
|
|
45
|
+
*/
|
|
38
46
|
export declare function buildContext(args: ParsedArgs): Promise<CommandContext>;
|
|
39
47
|
export declare function clientOptions(ctx: CommandContext): {
|
|
40
48
|
baseUrl: string;
|
|
@@ -42,14 +50,6 @@ export declare function clientOptions(ctx: CommandContext): {
|
|
|
42
50
|
};
|
|
43
51
|
export declare function readManifestOptional(dir: string): Promise<ManagedFunctionManifest | null>;
|
|
44
52
|
export declare function workDir(args: ParsedArgs): string;
|
|
45
|
-
/**
|
|
46
|
-
* Source selection for build/deploy: --dir (local, default '.'), a platform
|
|
47
|
-
* git-service repo (--repo <ns>/<name>), or any git URL (--git-url <url>);
|
|
48
|
-
* --ref picks a branch/tag/commit. Reuses the artifact-studio resolver —
|
|
49
|
-
* functions name their local dir with --dir rather than a positional, so map
|
|
50
|
-
* it onto the spec parser's positional slot.
|
|
51
|
-
*/
|
|
52
|
-
export declare function parseFunctionsSourceSpec(args: ParsedArgs): SourceSpec;
|
|
53
53
|
export declare function resolveOrRegisterFunction({ ctx, slug, title, description, }: {
|
|
54
54
|
ctx: CommandContext;
|
|
55
55
|
slug: string;
|
|
@@ -82,5 +82,5 @@ export declare function functionsRollbackCommand(args: ParsedArgs): Promise<numb
|
|
|
82
82
|
/** Minimal dotenv parser — KEY=VALUE lines, quotes stripped, comments skipped. */
|
|
83
83
|
export declare function parseDotenv(content: string): Record<string, string>;
|
|
84
84
|
export declare function functionsDeleteCommand(args: ParsedArgs): Promise<number>;
|
|
85
|
-
export declare const FUNCTIONS_USAGE = "usage:\n seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler\n seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)\n seq-studio functions deploy
|
|
85
|
+
export declare const FUNCTIONS_USAGE = "usage:\n seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler\n seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)\n seq-studio functions deploy -e <env> [-m msg] preview + confirm secrets + upload and deploy\n seq-studio functions list -e <env> [--match-local] functions visible on the environment\n seq-studio functions show -e <env> [--fn slug] detail for one function (versions, secrets)\n seq-studio functions logs -e <env> [--limit N] [--since t] Cloud Logging snapshot (reader-gated)\n seq-studio functions promote <version> -e <env> make a version live\n seq-studio functions rollback [<version>] -e <env> redeploy a prior version\n seq-studio functions delete -e <env> [--yes] archive function + tear down GCP resources\n (version history is retained)\n\n Flags: -e/--env <env|preview:<slug>> (required for network commands; see: seq-studio envs list) \u00B7 --fn <slug> \u00B7 --dir <path>\n --path <repo-subdirectory> (remote build/deploy only) select one function in a multi-function repo\n --from-env-file <path> (default: .env) source file for secret values\n --no-wait \u00B7 --yes\n --no-provision (deploy) update-only: error instead of registering a new\n shell, writing secret values, or attaching secrets (CI sweep)\n\n Source for build/deploy: a local --dir (default .), a platform git-service\n repo (--repo <ns>/<name>), or a public HTTPS git URL (--git-url <url>). --path selects\n a function directory within a remote repo; omit it for the existing root-manifest\n layout. --ref selects a branch/tag/commit (default: the repo's default branch).\n Remote sources record the pinned commit as provenance (never dirty) and NEVER\n read a repo-committed .env for secret values \u2014 provision secrets server-side\n or pass a local --from-env-file (resolved against your cwd).\n\n Interactive --repo builds clone over smart-HTTP and require a repo:read git\n PAT in ATLAS_GIT_PAT (`seq-studio auth pat create --scopes repo:read`, or\n Atlas \u2192 Settings \u2192 Tokens). Headless M2M builds use JSON materialize and\n accept only platform-managed --repo sources. Interactive builds also need\n --env + seq-studio login to resolve the repo and deploy.\n";
|
|
86
86
|
export declare function runFunctionsCommand(sub: string | undefined, args: ParsedArgs): Promise<number>;
|
|
@@ -6,16 +6,19 @@ import { promisify } from 'node:util';
|
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
7
|
import { Writable } from 'node:stream';
|
|
8
8
|
import { load as parseYaml } from 'js-yaml';
|
|
9
|
-
import {
|
|
9
|
+
import { getAccessTokenWithMode, tryGetAccessTokenWithMode } from '../auth.js';
|
|
10
10
|
import { resolveEnvWithDiscovery } from '../config.js';
|
|
11
11
|
import { AtlasApiError, deleteJson, getJson, postJson } from '../atlas-client.js';
|
|
12
12
|
import { clarifyApplyFailureReason, printCliError } from '../cli-errors.js';
|
|
13
|
+
import { REQUIRE_EXPLICIT_ENV_MESSAGE } from '../env-flags.js';
|
|
13
14
|
import { confirmYes } from '../prompt.js';
|
|
14
15
|
import { collectBundleFiles, isDirectory, validateLocalBundle, } from './bundle.js';
|
|
15
16
|
import { managedFunctionManifestSchema, MF_MANIFEST_FILENAME, manifestEgressHosts, manifestEgressIpRanges, } from './manifest.js';
|
|
16
17
|
import { buildEgressPreviewLines, egressPropagationNote, formatEgressSummary, printFunctionEgressHosts, } from './egress-preview.js';
|
|
17
|
-
import {
|
|
18
|
+
import { resolveArtifactSource, } from '@sequenceholdings/artifact-studio/source-resolver';
|
|
18
19
|
import { buildSecretPreviewLines, classifySecrets, } from './secret-reconcile.js';
|
|
20
|
+
import { parseFunctionsSourceSelection, resolveFunctionSourceDir, } from './source-selection.js';
|
|
21
|
+
export { parseFunctionsSourceSelection, parseFunctionsSourceSpec, resolveFunctionSourceDir } from './source-selection.js';
|
|
19
22
|
const execFileAsync = promisify(execFile);
|
|
20
23
|
export const LOG = '[seq-studio]';
|
|
21
24
|
const POLL_INTERVAL_MS = 5_000;
|
|
@@ -23,15 +26,48 @@ const POLL_TIMEOUT_MS = 20 * 60 * 1000;
|
|
|
23
26
|
export function flagBool(flags, ...keys) {
|
|
24
27
|
return keys.some((key) => flags[key] === true || flags[key] === 'true');
|
|
25
28
|
}
|
|
29
|
+
export function requestedEnvironment(args) {
|
|
30
|
+
return ((typeof args.flags.env === 'string' ? args.flags.env : undefined) ??
|
|
31
|
+
(typeof args.flags.e === 'string' ? args.flags.e : undefined));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve env + token for Atlas network commands. Requires an explicit
|
|
35
|
+
* `-e/--env` so partners never hit the built-in `local` default and get an
|
|
36
|
+
* opaque `fetch failed` (same contract as `functions deploy`).
|
|
37
|
+
*/
|
|
26
38
|
export async function buildContext(args) {
|
|
27
|
-
const requested = (
|
|
28
|
-
|
|
39
|
+
const requested = requestedEnvironment(args);
|
|
40
|
+
if (!requested) {
|
|
41
|
+
throw new Error(REQUIRE_EXPLICIT_ENV_MESSAGE);
|
|
42
|
+
}
|
|
29
43
|
const env = await resolveEnvWithDiscovery({ requested });
|
|
30
|
-
const
|
|
31
|
-
return { env, token };
|
|
44
|
+
const auth = await getAccessTokenWithMode({ env: env.name, targetUrl: env.url });
|
|
45
|
+
return { authMode: auth.authMode, env, token: auth.token };
|
|
32
46
|
}
|
|
33
47
|
export function clientOptions(ctx) {
|
|
34
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
baseUrl: ctx.env.url,
|
|
50
|
+
token: ctx.token,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function sourceClientOptions(ctx) {
|
|
54
|
+
return {
|
|
55
|
+
...(ctx.authMode ? { authMode: ctx.authMode } : {}),
|
|
56
|
+
...clientOptions(ctx),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function buildSourceOptions({ args, spec, }) {
|
|
60
|
+
if (spec.kind === 'git-service') {
|
|
61
|
+
return sourceClientOptions(await buildContext(args));
|
|
62
|
+
}
|
|
63
|
+
if (spec.kind === 'git-url') {
|
|
64
|
+
const auth = await tryGetAccessTokenWithMode({
|
|
65
|
+
failClosedForM2m: true,
|
|
66
|
+
env: requestedEnvironment(args),
|
|
67
|
+
});
|
|
68
|
+
return auth ? { authMode: auth.authMode } : {};
|
|
69
|
+
}
|
|
70
|
+
return {};
|
|
35
71
|
}
|
|
36
72
|
export async function readManifestOptional(dir) {
|
|
37
73
|
const path = join(dir, MF_MANIFEST_FILENAME);
|
|
@@ -51,20 +87,6 @@ export function workDir(args) {
|
|
|
51
87
|
const dir = typeof args.flags.dir === 'string' ? args.flags.dir : '.';
|
|
52
88
|
return resolve(dir);
|
|
53
89
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Source selection for build/deploy: --dir (local, default '.'), a platform
|
|
56
|
-
* git-service repo (--repo <ns>/<name>), or any git URL (--git-url <url>);
|
|
57
|
-
* --ref picks a branch/tag/commit. Reuses the artifact-studio resolver —
|
|
58
|
-
* functions name their local dir with --dir rather than a positional, so map
|
|
59
|
-
* it onto the spec parser's positional slot.
|
|
60
|
-
*/
|
|
61
|
-
export function parseFunctionsSourceSpec(args) {
|
|
62
|
-
const dir = typeof args.flags.dir === 'string' ? args.flags.dir : undefined;
|
|
63
|
-
if (dir !== undefined && (args.flags.repo !== undefined || args.flags['git-url'] !== undefined)) {
|
|
64
|
-
throw new Error('--dir cannot be combined with --repo / --git-url.');
|
|
65
|
-
}
|
|
66
|
-
return parseSourceSpec({ positional: dir === undefined ? [] : [dir], flags: args.flags });
|
|
67
|
-
}
|
|
68
90
|
/** Manifest read with a source-aware error for the missing case. */
|
|
69
91
|
async function readSourceManifest(dir, spec) {
|
|
70
92
|
const manifest = await readManifestOptional(dir);
|
|
@@ -72,7 +94,7 @@ async function readSourceManifest(dir, spec) {
|
|
|
72
94
|
return manifest;
|
|
73
95
|
throw new Error(spec.kind === 'local'
|
|
74
96
|
? `No ${MF_MANIFEST_FILENAME} in ${dir}. Run \`seq-studio functions init\` to scaffold one, or pass --dir.`
|
|
75
|
-
: `No ${MF_MANIFEST_FILENAME}
|
|
97
|
+
: `No ${MF_MANIFEST_FILENAME} in the selected remote source directory — keep it at the repo root or select a function directory with --path.`);
|
|
76
98
|
}
|
|
77
99
|
/**
|
|
78
100
|
* Resolve the target function: --fn <slug> wins, else the manifest in the
|
|
@@ -315,13 +337,15 @@ export async function functionsInitCommand(args) {
|
|
|
315
337
|
// build (local pre-flight)
|
|
316
338
|
// ---------------------------------------------------------------------------
|
|
317
339
|
export async function functionsBuildCommand(args) {
|
|
318
|
-
const spec =
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
340
|
+
const { spec, path } = parseFunctionsSourceSelection(args);
|
|
341
|
+
// A public git URL needs no bearer token, but optional auth resolution still
|
|
342
|
+
// identifies a selected M2M principal so policy can reject arbitrary CI
|
|
343
|
+
// input before cloning. Local builds remain completely offline.
|
|
344
|
+
const remote = await buildSourceOptions({ args, spec });
|
|
322
345
|
const source = await resolveArtifactSource(spec, remote);
|
|
323
346
|
try {
|
|
324
|
-
|
|
347
|
+
const dir = await resolveFunctionSourceDir({ repoDir: source.dir, path });
|
|
348
|
+
return await buildFromResolvedSource({ spec, source: { ...source, dir } });
|
|
325
349
|
}
|
|
326
350
|
finally {
|
|
327
351
|
await source.cleanup();
|
|
@@ -376,15 +400,22 @@ async function getFunctionDetail(ctx, functionId) {
|
|
|
376
400
|
}
|
|
377
401
|
}
|
|
378
402
|
export async function functionsDeployCommand(args) {
|
|
379
|
-
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
403
|
+
// Fail before source resolution / auth so a missing `-e` never becomes
|
|
404
|
+
// `fetch failed` against localhost. `buildContext` enforces the same rule.
|
|
405
|
+
if (!requestedEnvironment(args)) {
|
|
406
|
+
console.error(`${LOG} ${REQUIRE_EXPLICIT_ENV_MESSAGE}`);
|
|
407
|
+
return 1;
|
|
408
|
+
}
|
|
409
|
+
const { spec, path } = parseFunctionsSourceSelection(args);
|
|
410
|
+
// Remote deploys resolve auth up front: --repo needs it to fetch the tree,
|
|
411
|
+
// while --git-url must reject a selected M2M principal before cloning.
|
|
412
|
+
// Local sources still defer auth until after validation so malformed local
|
|
413
|
+
// input fails before any login/network work.
|
|
414
|
+
const ctx = spec.kind !== 'local' ? await buildContext(args) : null;
|
|
415
|
+
const source = await resolveArtifactSource(spec, ctx ? sourceClientOptions(ctx) : {});
|
|
386
416
|
try {
|
|
387
|
-
|
|
417
|
+
const dir = await resolveFunctionSourceDir({ repoDir: source.dir, path });
|
|
418
|
+
return await deployFromResolvedSource({ args, spec, ctx, source: { ...source, dir } });
|
|
388
419
|
}
|
|
389
420
|
finally {
|
|
390
421
|
await source.cleanup();
|
|
@@ -837,7 +868,7 @@ async function listVersions(ctx, functionId) {
|
|
|
837
868
|
export async function functionsPromoteCommand(args) {
|
|
838
869
|
const versionName = args.positional[0];
|
|
839
870
|
if (!versionName) {
|
|
840
|
-
console.error('usage: seq-studio functions promote <version>
|
|
871
|
+
console.error('usage: seq-studio functions promote <version> -e <env> [--fn slug]');
|
|
841
872
|
return 1;
|
|
842
873
|
}
|
|
843
874
|
const ctx = await buildContext(args);
|
|
@@ -932,32 +963,35 @@ export async function functionsDeleteCommand(args) {
|
|
|
932
963
|
export const FUNCTIONS_USAGE = `usage:
|
|
933
964
|
seq-studio functions init <dir> scaffold manifest + TypeScript hello-world handler
|
|
934
965
|
seq-studio functions build [--dir d] local pre-flight (manifest, lockfile, size)
|
|
935
|
-
seq-studio functions deploy
|
|
936
|
-
seq-studio functions list
|
|
937
|
-
seq-studio functions show
|
|
938
|
-
seq-studio functions logs
|
|
939
|
-
seq-studio functions promote <version>
|
|
940
|
-
seq-studio functions rollback [<version>]
|
|
941
|
-
seq-studio functions delete [--yes]
|
|
966
|
+
seq-studio functions deploy -e <env> [-m msg] preview + confirm secrets + upload and deploy
|
|
967
|
+
seq-studio functions list -e <env> [--match-local] functions visible on the environment
|
|
968
|
+
seq-studio functions show -e <env> [--fn slug] detail for one function (versions, secrets)
|
|
969
|
+
seq-studio functions logs -e <env> [--limit N] [--since t] Cloud Logging snapshot (reader-gated)
|
|
970
|
+
seq-studio functions promote <version> -e <env> make a version live
|
|
971
|
+
seq-studio functions rollback [<version>] -e <env> redeploy a prior version
|
|
972
|
+
seq-studio functions delete -e <env> [--yes] archive function + tear down GCP resources
|
|
942
973
|
(version history is retained)
|
|
943
974
|
|
|
944
|
-
Flags: -e/--env <env|preview:<slug>> (see: seq-studio envs list) · --fn <slug> · --dir <path>
|
|
975
|
+
Flags: -e/--env <env|preview:<slug>> (required for network commands; see: seq-studio envs list) · --fn <slug> · --dir <path>
|
|
976
|
+
--path <repo-subdirectory> (remote build/deploy only) select one function in a multi-function repo
|
|
945
977
|
--from-env-file <path> (default: .env) source file for secret values
|
|
946
978
|
--no-wait · --yes
|
|
947
979
|
--no-provision (deploy) update-only: error instead of registering a new
|
|
948
980
|
shell, writing secret values, or attaching secrets (CI sweep)
|
|
949
981
|
|
|
950
982
|
Source for build/deploy: a local --dir (default .), a platform git-service
|
|
951
|
-
repo (--repo <ns>/<name>), or
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
983
|
+
repo (--repo <ns>/<name>), or a public HTTPS git URL (--git-url <url>). --path selects
|
|
984
|
+
a function directory within a remote repo; omit it for the existing root-manifest
|
|
985
|
+
layout. --ref selects a branch/tag/commit (default: the repo's default branch).
|
|
986
|
+
Remote sources record the pinned commit as provenance (never dirty) and NEVER
|
|
987
|
+
read a repo-committed .env for secret values — provision secrets server-side
|
|
988
|
+
or pass a local --from-env-file (resolved against your cwd).
|
|
956
989
|
|
|
957
|
-
--repo
|
|
958
|
-
|
|
959
|
-
Atlas → Settings → Tokens).
|
|
960
|
-
|
|
990
|
+
Interactive --repo builds clone over smart-HTTP and require a repo:read git
|
|
991
|
+
PAT in ATLAS_GIT_PAT (\`seq-studio auth pat create --scopes repo:read\`, or
|
|
992
|
+
Atlas → Settings → Tokens). Headless M2M builds use JSON materialize and
|
|
993
|
+
accept only platform-managed --repo sources. Interactive builds also need
|
|
994
|
+
--env + seq-studio login to resolve the repo and deploy.
|
|
961
995
|
`;
|
|
962
996
|
export async function runFunctionsCommand(sub, args) {
|
|
963
997
|
try {
|
|
@@ -57,6 +57,7 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
|
|
|
57
57
|
}, z.core.$strip>>;
|
|
58
58
|
secrets: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
59
59
|
egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
60
|
+
service_account: z.ZodOptional<z.ZodString>;
|
|
60
61
|
input_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
61
62
|
output_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
62
63
|
capabilities: z.ZodDefault<z.ZodObject<{
|
|
@@ -488,6 +488,11 @@ function validateCapabilityGates({ gates, uses, roles, }) {
|
|
|
488
488
|
}
|
|
489
489
|
return errors;
|
|
490
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Platform service-account reference: slug (strictly lowercase) or uuid id.
|
|
493
|
+
* Mirrors atlas/src/server/services/managed-functions/manifest.ts.
|
|
494
|
+
*/
|
|
495
|
+
const SERVICE_ACCOUNT_REF_RE = /^([a-z][a-z0-9-]{1,98}|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$/;
|
|
491
496
|
export const managedFunctionManifestSchema = z.object({
|
|
492
497
|
schema_version: z.literal(1).default(1),
|
|
493
498
|
function: z.object({
|
|
@@ -538,6 +543,17 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
538
543
|
}
|
|
539
544
|
})
|
|
540
545
|
.default([]),
|
|
546
|
+
/**
|
|
547
|
+
* The platform service account this function ACTS AS for ORM data access
|
|
548
|
+
* (slug or id). Required whenever `capabilities.data` reaches a namespace,
|
|
549
|
+
* rejected otherwise. Resolved and authorized server-side at activation —
|
|
550
|
+
* the deploy actor must hold `actor` on the account when the attachment
|
|
551
|
+
* changes. Mirrors atlas/src/server/services/managed-functions/manifest.ts.
|
|
552
|
+
*/
|
|
553
|
+
service_account: z
|
|
554
|
+
.string()
|
|
555
|
+
.regex(SERVICE_ACCOUNT_REF_RE, 'service_account must be a platform service-account slug (lowercase alphanumeric with hyphens) or uuid')
|
|
556
|
+
.optional(),
|
|
541
557
|
input_schema: z.record(z.string(), z.unknown()).optional(),
|
|
542
558
|
output_schema: z.record(z.string(), z.unknown()).optional(),
|
|
543
559
|
/**
|
|
@@ -583,4 +599,24 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
583
599
|
})) {
|
|
584
600
|
ctx.addIssue({ code: 'custom', message, path: ['capabilities', 'gates'] });
|
|
585
601
|
}
|
|
602
|
+
// A namespace is "reached" when its block declares at least one table,
|
|
603
|
+
// action, or raw query — the same rule the server's floor reconciliation
|
|
604
|
+
// uses (readManifestDataNamespaces).
|
|
605
|
+
const reachesData = Object.values(manifest.capabilities.data).some((block) => block.tables.length > 0 || block.actions.length > 0 || block.query);
|
|
606
|
+
if (reachesData && manifest.service_account === undefined) {
|
|
607
|
+
ctx.addIssue({
|
|
608
|
+
code: 'custom',
|
|
609
|
+
message: 'capabilities.data requires a top-level service_account — the platform service ' +
|
|
610
|
+
'account this function acts as for ORM data access (a platform operator creates ' +
|
|
611
|
+
'the account; the deployer needs the actor role on it)',
|
|
612
|
+
path: ['service_account'],
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
if (!reachesData && manifest.service_account !== undefined) {
|
|
616
|
+
ctx.addIssue({
|
|
617
|
+
code: 'custom',
|
|
618
|
+
message: 'service_account is only used for ORM data access — declare capabilities.data or remove it',
|
|
619
|
+
path: ['service_account'],
|
|
620
|
+
});
|
|
621
|
+
}
|
|
586
622
|
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type SourceSpec } from '@sequenceholdings/artifact-studio/source-resolver';
|
|
2
|
+
import type { ParsedArgs } from '../process/commands.js';
|
|
3
|
+
export interface FunctionsSourceSelection {
|
|
4
|
+
spec: SourceSpec;
|
|
5
|
+
path?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Functions name their local source with --dir rather than a positional, so
|
|
9
|
+
* map it onto the shared source parser used by other seq-studio primitives.
|
|
10
|
+
*/
|
|
11
|
+
export declare function parseFunctionsSourceSpec(args: ParsedArgs): SourceSpec;
|
|
12
|
+
/**
|
|
13
|
+
* Select a managed function within a remote repository. Local callers already
|
|
14
|
+
* select the function root with --dir, so --path is remote-only.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseFunctionsSourceSelection(args: ParsedArgs): FunctionsSourceSelection;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the selected function root after the repository is materialized.
|
|
19
|
+
* realpath containment prevents a committed symlink from escaping the repo.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveFunctionSourceDir({ repoDir, path, }: {
|
|
22
|
+
repoDir: string;
|
|
23
|
+
path?: string;
|
|
24
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { parseSourceSpec, } from '@sequenceholdings/artifact-studio/source-resolver';
|
|
4
|
+
import { isDirectory } from './bundle.js';
|
|
5
|
+
/**
|
|
6
|
+
* Functions name their local source with --dir rather than a positional, so
|
|
7
|
+
* map it onto the shared source parser used by other seq-studio primitives.
|
|
8
|
+
*/
|
|
9
|
+
export function parseFunctionsSourceSpec(args) {
|
|
10
|
+
const dir = typeof args.flags.dir === 'string' ? args.flags.dir : undefined;
|
|
11
|
+
if (dir !== undefined && (args.flags.repo !== undefined || args.flags['git-url'] !== undefined)) {
|
|
12
|
+
throw new Error('--dir cannot be combined with --repo / --git-url.');
|
|
13
|
+
}
|
|
14
|
+
return parseSourceSpec({ positional: dir === undefined ? [] : [dir], flags: args.flags });
|
|
15
|
+
}
|
|
16
|
+
function validateFunctionRepoPath(path) {
|
|
17
|
+
const segments = path.split('/');
|
|
18
|
+
if (path.length === 0 ||
|
|
19
|
+
path.trim() !== path ||
|
|
20
|
+
isAbsolute(path) ||
|
|
21
|
+
path.includes('\\') ||
|
|
22
|
+
[...path].some((character) => character.charCodeAt(0) < 0x20) ||
|
|
23
|
+
segments.some((segment) => segment.length === 0 || segment === '.' || segment === '..')) {
|
|
24
|
+
throw new Error(`--path must be a canonical relative directory within the repository (got ${JSON.stringify(path)}).`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Select a managed function within a remote repository. Local callers already
|
|
29
|
+
* select the function root with --dir, so --path is remote-only.
|
|
30
|
+
*/
|
|
31
|
+
export function parseFunctionsSourceSelection(args) {
|
|
32
|
+
const spec = parseFunctionsSourceSpec(args);
|
|
33
|
+
const pathFlag = args.flags.path;
|
|
34
|
+
if (pathFlag === true)
|
|
35
|
+
throw new Error('--path requires a value.');
|
|
36
|
+
if (pathFlag === undefined)
|
|
37
|
+
return { spec };
|
|
38
|
+
if (spec.kind === 'local') {
|
|
39
|
+
throw new Error('--path only applies together with --repo or --git-url; use --dir for local source.');
|
|
40
|
+
}
|
|
41
|
+
validateFunctionRepoPath(pathFlag);
|
|
42
|
+
return { spec, path: pathFlag };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the selected function root after the repository is materialized.
|
|
46
|
+
* realpath containment prevents a committed symlink from escaping the repo.
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveFunctionSourceDir({ repoDir, path, }) {
|
|
49
|
+
if (path === undefined)
|
|
50
|
+
return repoDir;
|
|
51
|
+
validateFunctionRepoPath(path);
|
|
52
|
+
const candidate = resolve(repoDir, path);
|
|
53
|
+
if (!(await isDirectory(candidate))) {
|
|
54
|
+
throw new Error(`--path does not name a directory in the repository: ${path}`);
|
|
55
|
+
}
|
|
56
|
+
const [canonicalRepoDir, canonicalCandidate] = await Promise.all([
|
|
57
|
+
realpath(repoDir),
|
|
58
|
+
realpath(candidate),
|
|
59
|
+
]);
|
|
60
|
+
const relativePath = relative(canonicalRepoDir, canonicalCandidate);
|
|
61
|
+
if (relativePath === '..' ||
|
|
62
|
+
relativePath.startsWith(`..${sep}`) ||
|
|
63
|
+
isAbsolute(relativePath)) {
|
|
64
|
+
throw new Error(`--path must stay within the repository: ${path}`);
|
|
65
|
+
}
|
|
66
|
+
return canonicalCandidate;
|
|
67
|
+
}
|
package/dist/login.d.ts
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
+
import { type AuthRealm } from './auth.js';
|
|
1
2
|
interface LoginOptions {
|
|
2
3
|
port?: number;
|
|
3
4
|
timeoutMs?: number;
|
|
4
5
|
fetchImpl?: typeof fetch;
|
|
5
6
|
now?: () => number;
|
|
6
7
|
openBrowser?: (authorizationUrl: string) => void | Promise<void>;
|
|
8
|
+
/** Auth0 login context — the Sequence realm by default, or a registered
|
|
9
|
+
* OpCo environment's realm (`seq-studio envs add`), which pins the Auth0
|
|
10
|
+
* Organization so the token carries the org context tenant APIs require. */
|
|
11
|
+
realm?: AuthRealm;
|
|
7
12
|
}
|
|
8
13
|
export declare function openSystemBrowser(authorizationUrl: string): Promise<void>;
|
|
9
|
-
export declare function loginWithPkce({ fetchImpl, now, openBrowser, port, timeoutMs, }: LoginOptions): Promise<
|
|
14
|
+
export declare function loginWithPkce({ fetchImpl, now, openBrowser, port, realm, timeoutMs, }: LoginOptions): Promise<string>;
|
|
10
15
|
/**
|
|
11
16
|
* Best-effort catalog refresh after a successful login. Drop the old cache
|
|
12
17
|
* first so a prior identity's tier never lingers; discovery failures must not
|
|
13
18
|
* fail login. Exported for unit tests.
|
|
14
19
|
*/
|
|
15
20
|
export declare function refreshCatalogAfterLogin(): Promise<void>;
|
|
16
|
-
export declare function login(): Promise<void>;
|
|
17
|
-
export declare function logout(): Promise<void>;
|
|
21
|
+
export declare function login(envName?: string): Promise<void>;
|
|
22
|
+
export declare function logout(envName?: string): Promise<void>;
|
|
18
23
|
export {};
|
package/dist/login.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createServer } from 'node:http';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
import {
|
|
7
|
+
import { deleteRealmTokens, realmForEnv, saveTokens, seqapiTokenPath, SEQUENCE_AUTH_REALM, SEQUENCE_REALM, verifyTokenMatchesRealm, } from './auth.js';
|
|
8
8
|
import { manualEnvConfigHint } from './config.js';
|
|
9
9
|
import { bootstrapUrl, clearCatalog, fetchCatalog } from './env-catalog.js';
|
|
10
10
|
const DEFAULT_REDIRECT_PORT = 5099;
|
|
@@ -22,16 +22,18 @@ function configuredPort() {
|
|
|
22
22
|
}
|
|
23
23
|
return port;
|
|
24
24
|
}
|
|
25
|
-
function authorizationUrl({ challenge, redirectUri, state, }) {
|
|
26
|
-
const url = new URL(`https://${
|
|
25
|
+
function authorizationUrl({ challenge, realm, redirectUri, state, }) {
|
|
26
|
+
const url = new URL(`https://${realm.domain}/authorize`);
|
|
27
27
|
url.searchParams.set('response_type', 'code');
|
|
28
|
-
url.searchParams.set('client_id',
|
|
28
|
+
url.searchParams.set('client_id', realm.clientId);
|
|
29
29
|
url.searchParams.set('redirect_uri', redirectUri);
|
|
30
|
-
url.searchParams.set('scope', 'openid profile email
|
|
31
|
-
url.searchParams.set('audience',
|
|
30
|
+
url.searchParams.set('scope', 'openid profile email');
|
|
31
|
+
url.searchParams.set('audience', realm.audience);
|
|
32
32
|
url.searchParams.set('code_challenge', challenge);
|
|
33
33
|
url.searchParams.set('code_challenge_method', 'S256');
|
|
34
34
|
url.searchParams.set('state', state);
|
|
35
|
+
if (realm.organization)
|
|
36
|
+
url.searchParams.set('organization', realm.organization);
|
|
35
37
|
return url.toString();
|
|
36
38
|
}
|
|
37
39
|
export async function openSystemBrowser(authorizationUrl) {
|
|
@@ -142,21 +144,16 @@ function parseTokenResponse(value) {
|
|
|
142
144
|
throw new Error('Auth0 token response was not an object.');
|
|
143
145
|
}
|
|
144
146
|
const accessToken = Reflect.get(value, 'access_token');
|
|
145
|
-
const refreshToken = Reflect.get(value, 'refresh_token');
|
|
146
147
|
const expiresIn = Reflect.get(value, 'expires_in');
|
|
147
148
|
if (typeof accessToken !== 'string' || !accessToken) {
|
|
148
149
|
throw new Error('Auth0 token response missing access_token.');
|
|
149
150
|
}
|
|
150
|
-
if (typeof refreshToken !== 'string' || !refreshToken) {
|
|
151
|
-
throw new Error('No refresh token returned. Ensure Auth0 offline access is enabled.');
|
|
152
|
-
}
|
|
153
151
|
return {
|
|
154
152
|
accessToken,
|
|
155
|
-
refreshToken,
|
|
156
153
|
expiresIn: typeof expiresIn === 'number' ? expiresIn : 86_400,
|
|
157
154
|
};
|
|
158
155
|
}
|
|
159
|
-
export async function loginWithPkce({ fetchImpl = fetch, now = Date.now, openBrowser = openSystemBrowser, port = configuredPort(), timeoutMs = DEFAULT_LOGIN_TIMEOUT_MS, }) {
|
|
156
|
+
export async function loginWithPkce({ fetchImpl = fetch, now = Date.now, openBrowser = openSystemBrowser, port = configuredPort(), realm = SEQUENCE_AUTH_REALM, timeoutMs = DEFAULT_LOGIN_TIMEOUT_MS, }) {
|
|
160
157
|
const verifier = base64Url(randomBytes(32));
|
|
161
158
|
const challenge = base64Url(createHash('sha256').update(verifier).digest());
|
|
162
159
|
const state = base64Url(randomBytes(32));
|
|
@@ -165,20 +162,21 @@ export async function loginWithPkce({ fetchImpl = fetch, now = Date.now, openBro
|
|
|
165
162
|
port,
|
|
166
163
|
timeoutMs,
|
|
167
164
|
onListening: (redirectUri) => {
|
|
168
|
-
const url = authorizationUrl({ challenge, redirectUri, state });
|
|
169
|
-
console.error(`Opening browser for Sequence login. If it does not open, visit:\n${url}`);
|
|
165
|
+
const url = authorizationUrl({ challenge, realm, redirectUri, state });
|
|
166
|
+
console.error(`Opening browser for Sequence login [${realm.name}]. If it does not open, visit:\n${url}`);
|
|
170
167
|
void Promise.resolve(openBrowser(url)).catch((error) => {
|
|
171
168
|
const message = error instanceof Error ? error.message : String(error);
|
|
172
169
|
console.error(`Could not open a browser automatically: ${message}`);
|
|
173
170
|
});
|
|
174
171
|
},
|
|
175
172
|
});
|
|
176
|
-
const tokenResponse = await fetchImpl(`https://${
|
|
173
|
+
const tokenResponse = await fetchImpl(`https://${realm.domain}/oauth/token`, {
|
|
177
174
|
method: 'POST',
|
|
175
|
+
redirect: 'manual',
|
|
178
176
|
headers: { 'Content-Type': 'application/json' },
|
|
179
177
|
body: JSON.stringify({
|
|
180
178
|
grant_type: 'authorization_code',
|
|
181
|
-
client_id:
|
|
179
|
+
client_id: realm.clientId,
|
|
182
180
|
code: callback.code,
|
|
183
181
|
redirect_uri: callback.redirectUri,
|
|
184
182
|
code_verifier: verifier,
|
|
@@ -188,11 +186,12 @@ export async function loginWithPkce({ fetchImpl = fetch, now = Date.now, openBro
|
|
|
188
186
|
throw new Error(`Auth0 token exchange failed (${tokenResponse.status}): ${await tokenResponse.text()}`);
|
|
189
187
|
}
|
|
190
188
|
const tokens = parseTokenResponse(await tokenResponse.json());
|
|
189
|
+
verifyTokenMatchesRealm({ accessToken: tokens.accessToken, realm });
|
|
191
190
|
await saveTokens({
|
|
192
191
|
access_token: tokens.accessToken,
|
|
193
|
-
refresh_token: tokens.refreshToken,
|
|
194
192
|
expires_at: now() / 1_000 + tokens.expiresIn,
|
|
195
|
-
});
|
|
193
|
+
}, realm.name);
|
|
194
|
+
return tokens.accessToken;
|
|
196
195
|
}
|
|
197
196
|
/**
|
|
198
197
|
* Best-effort catalog refresh after a successful login. Drop the old cache
|
|
@@ -216,26 +215,39 @@ export async function refreshCatalogAfterLogin() {
|
|
|
216
215
|
'run: seq-studio envs refresh');
|
|
217
216
|
console.log(manualEnvConfigHint());
|
|
218
217
|
}
|
|
219
|
-
export async function login() {
|
|
220
|
-
await
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
export async function login(envName) {
|
|
219
|
+
const realm = await realmForEnv(envName);
|
|
220
|
+
if (envName && realm.name === SEQUENCE_REALM && envName !== SEQUENCE_REALM) {
|
|
221
|
+
// A name that only exists in the Sequence catalog (staging, banksouth…)
|
|
222
|
+
// resolves to the Sequence realm — one login covers all of those. A typo'd
|
|
223
|
+
// OpCo name would silently do a Sequence login otherwise, so say which
|
|
224
|
+
// realm we're using.
|
|
225
|
+
console.error(`'${envName}' uses the shared Sequence login ` +
|
|
226
|
+
'(register OpCo environments with: seq-studio envs add <name> <url>).');
|
|
227
|
+
}
|
|
228
|
+
await loginWithPkce({ realm });
|
|
229
|
+
console.log(`Authenticated [${realm.name}]. Short-lived access token saved to ${seqapiTokenPath()}.`);
|
|
230
|
+
// The environment catalog is a Sequence-deployment surface; OpCo realm
|
|
231
|
+
// logins target a single known deployment and have nothing to discover.
|
|
232
|
+
if (realm.name === SEQUENCE_REALM) {
|
|
233
|
+
await refreshCatalogAfterLogin();
|
|
234
|
+
}
|
|
223
235
|
}
|
|
224
236
|
/**
|
|
225
|
-
* Pre-unification artifact-studio token file.
|
|
226
|
-
* still
|
|
227
|
-
* `readTokenConfig`, see shared/services/artifact-studio/src/config.ts), so
|
|
228
|
-
* logout must clear it too or artifact commands would stay authenticated
|
|
229
|
-
* after a successful logout.
|
|
237
|
+
* Pre-unification artifact-studio token file. New builds never read it, but
|
|
238
|
+
* logout still removes it so legacy bearer material does not linger on disk.
|
|
230
239
|
*/
|
|
231
240
|
function legacyArtifactTokenPath() {
|
|
232
241
|
return join(homedir(), '.config', 'sequence-artifact-studio', 'tokens.json');
|
|
233
242
|
}
|
|
234
|
-
export async function logout() {
|
|
235
|
-
|
|
236
|
-
await
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
export async function logout(envName) {
|
|
244
|
+
const realm = await realmForEnv(envName);
|
|
245
|
+
await deleteRealmTokens(realm.name);
|
|
246
|
+
if (realm.name === SEQUENCE_REALM) {
|
|
247
|
+
await rm(legacyArtifactTokenPath(), { force: true });
|
|
248
|
+
// The environment catalog belongs to the Sequence identity; tenant logout
|
|
249
|
+
// must not discard a still-authenticated Sequence user's cached tier.
|
|
250
|
+
await clearCatalog();
|
|
251
|
+
}
|
|
252
|
+
console.log(`Logged out [${realm.name}] from seq-studio and seqapi.`);
|
|
241
253
|
}
|
package/dist/main.d.ts
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
* seq-studio process <sub> manage Lattice processes
|
|
6
6
|
* seq-studio artifact <sub> manage Artifact Studio apps
|
|
7
7
|
* seq-studio functions <sub> manage Managed Functions
|
|
8
|
+
* seq-studio agents <sub> manage typed agent definitions
|
|
8
9
|
* seq-studio secrets <sub> manage org-owned Managed Secrets
|
|
9
10
|
* seq-studio repos <sub> manage platform git-service repos
|
|
11
|
+
* seq-studio pipeline <sub> author + validate Data Pipelines stage specs
|
|
10
12
|
* seq-studio auth <sub> manage git-service PATs
|
|
11
|
-
* seq-studio envs <sub> list/refresh
|
|
13
|
+
* seq-studio envs <sub> add/list/refresh environments
|
|
12
14
|
* seq-studio login authenticate interactively with Auth0
|
|
13
15
|
* seq-studio logout remove cached user tokens
|
|
14
16
|
* seq-studio doctor check token + env + writer gate
|