@sequenceholdings/studio-cli 0.1.21 → 0.1.24
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 +184 -16
- package/dist/agents/commands.d.ts +1 -1
- package/dist/agents/commands.js +24 -4
- package/dist/agents/source.d.ts +3 -1
- package/dist/agents/source.js +27 -4
- package/dist/app/commands.d.ts +16 -0
- package/dist/app/commands.js +227 -0
- package/dist/app/deploy.d.ts +49 -0
- package/dist/app/deploy.js +197 -0
- package/dist/app/kinds.d.ts +10 -0
- package/dist/app/kinds.js +36 -0
- package/dist/app/manifest.d.ts +94 -0
- package/dist/app/manifest.js +273 -0
- package/dist/app/scaffold.d.ts +28 -0
- package/dist/app/scaffold.js +263 -0
- package/dist/atlas-client.js +29 -0
- package/dist/auth.js +2 -0
- package/dist/functions/commands.js +1 -0
- package/dist/functions/manifest.d.ts +24 -0
- package/dist/functions/manifest.js +84 -9
- package/dist/main.d.ts +3 -0
- package/dist/main.js +21 -0
- package/dist/pipeline/codegen.d.ts +2 -0
- package/dist/pipeline/codegen.js +118 -0
- package/dist/pipeline/commands.d.ts +7 -0
- package/dist/pipeline/commands.js +105 -10
- package/dist/pipeline/lifecycle.d.ts +3 -12
- package/dist/pipeline/lifecycle.js +245 -33
- package/dist/pipeline/templates.js +3 -1
- package/dist/repos/commands.d.ts +53 -1
- package/dist/repos/commands.js +258 -1
- package/dist/secrets/commands.d.ts +3 -1
- package/dist/secrets/commands.js +87 -26
- package/package.json +8 -8
- package/dist/pipeline/pinning.d.ts +0 -5
- package/dist/pipeline/pinning.js +0 -9
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffold ORM / function / artifact into an app monorepo and keep
|
|
3
|
+
* sequence.app.yml in sync. Delegates to each primitive's real `init`.
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
8
|
+
import { functionsInitCommand } from '../functions/commands.js';
|
|
9
|
+
import { parseArgs } from '../process/commands.js';
|
|
10
|
+
import { currentVersion } from '../update-check.js';
|
|
11
|
+
import { defaultDeployOrder, extendDeployOrder, loadAppManifest, ormNamespaceFromAppId, titleizeSlug, writeAppManifest, } from './manifest.js';
|
|
12
|
+
const LOG = '[seq-studio]';
|
|
13
|
+
const APP_MANIFEST_LABEL = 'sequence.app.yml';
|
|
14
|
+
export function defaultScaffoldNames(_appId) {
|
|
15
|
+
return { functionName: 'hello' };
|
|
16
|
+
}
|
|
17
|
+
function primitiveIdFor({ kind, name }) {
|
|
18
|
+
switch (kind) {
|
|
19
|
+
case 'orm':
|
|
20
|
+
return `${name}-orm`;
|
|
21
|
+
case 'artifact':
|
|
22
|
+
return `${name}-ui`;
|
|
23
|
+
case 'function':
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function relativePathFor({ kind, appId, name, }) {
|
|
28
|
+
switch (kind) {
|
|
29
|
+
case 'orm':
|
|
30
|
+
return join('orm', ormNamespaceFromAppId(appId));
|
|
31
|
+
case 'function':
|
|
32
|
+
return join('functions', name);
|
|
33
|
+
case 'artifact':
|
|
34
|
+
return 'artifact';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function buildPrimitiveEntry({ kind, appId, name, dependsOn, }) {
|
|
38
|
+
const path = relativePathFor({ kind, appId, name }).replace(/\\/g, '/');
|
|
39
|
+
const id = primitiveIdFor({ kind, name: kind === 'orm' || kind === 'artifact' ? appId : name });
|
|
40
|
+
const base = { id, kind, path, depends_on: dependsOn };
|
|
41
|
+
switch (kind) {
|
|
42
|
+
case 'orm':
|
|
43
|
+
return { ...base, namespace: ormNamespaceFromAppId(appId) };
|
|
44
|
+
case 'function':
|
|
45
|
+
return { ...base, slug: name };
|
|
46
|
+
case 'artifact':
|
|
47
|
+
return { ...base, project_id: appId };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function rewriteArtifactIdentity({ artifactDir, appId, }) {
|
|
51
|
+
const title = titleizeSlug(appId);
|
|
52
|
+
const manifestPath = join(artifactDir, 'artifact.bundle.yml');
|
|
53
|
+
if (existsSync(manifestPath)) {
|
|
54
|
+
const body = await readFile(manifestPath, 'utf8');
|
|
55
|
+
const next = body
|
|
56
|
+
.replace(/(project_id:\s*)\S+/g, `$1${appId}`)
|
|
57
|
+
.replace(/^(\s*)title:\s*.*$/m, `$1title: "${title}"`);
|
|
58
|
+
if (next !== body)
|
|
59
|
+
await writeFile(manifestPath, next, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
const pkgPath = join(artifactDir, 'package.json');
|
|
62
|
+
if (existsSync(pkgPath)) {
|
|
63
|
+
try {
|
|
64
|
+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
65
|
+
pkg.name = appId;
|
|
66
|
+
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// leave non-JSON package.json alone
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function scaffoldOrm({ absPath }) {
|
|
74
|
+
const { runOrmCommand } = await import('../orm/delegate.js');
|
|
75
|
+
const code = await runOrmCommand('init', [absPath]);
|
|
76
|
+
if (code !== 0) {
|
|
77
|
+
throw new Error(`orm init failed for ${absPath} (exit ${code}). ` +
|
|
78
|
+
'`seq-studio init --with orm` requires `@sequenceholdings/orm` ' +
|
|
79
|
+
'(workspace / internal installs today; not yet available via public npm).');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function scaffoldArtifact({ absPath, appId, }) {
|
|
83
|
+
const { runCli } = await import('@sequenceholdings/artifact-studio/cli');
|
|
84
|
+
const code = await runCli(['init', absPath]);
|
|
85
|
+
if (code !== 0) {
|
|
86
|
+
throw new Error(`artifact init failed for ${absPath} (exit ${code})`);
|
|
87
|
+
}
|
|
88
|
+
await rewriteArtifactIdentity({ artifactDir: absPath, appId });
|
|
89
|
+
}
|
|
90
|
+
async function scaffoldFunction({ absPath }) {
|
|
91
|
+
const code = await functionsInitCommand(parseArgs([absPath]));
|
|
92
|
+
if (code !== 0) {
|
|
93
|
+
throw new Error(`functions init failed for ${absPath} (exit ${code})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function scaffoldKind({ kind, appRoot, appId, name, }) {
|
|
97
|
+
const relPath = relativePathFor({ kind, appId, name }).replace(/\\/g, '/');
|
|
98
|
+
const absPath = join(appRoot, relPath);
|
|
99
|
+
const entry = buildPrimitiveEntry({ kind, appId, name, dependsOn: [] });
|
|
100
|
+
switch (kind) {
|
|
101
|
+
case 'orm':
|
|
102
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
103
|
+
await scaffoldOrm({ absPath });
|
|
104
|
+
break;
|
|
105
|
+
case 'function':
|
|
106
|
+
await scaffoldFunction({ absPath });
|
|
107
|
+
break;
|
|
108
|
+
case 'artifact':
|
|
109
|
+
await mkdir(absPath, { recursive: true });
|
|
110
|
+
await scaffoldArtifact({ absPath, appId });
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return entry;
|
|
114
|
+
}
|
|
115
|
+
function applyDefaultDependsOn(primitives) {
|
|
116
|
+
const orm = primitives.find((entry) => entry.kind === 'orm');
|
|
117
|
+
if (!orm)
|
|
118
|
+
return primitives;
|
|
119
|
+
return primitives.map((entry) => {
|
|
120
|
+
if (entry.kind === 'orm' || entry.depends_on.length > 0)
|
|
121
|
+
return entry;
|
|
122
|
+
return { ...entry, depends_on: [orm.id] };
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export async function initAppMonorepo({ rootDir, appId, kinds, names = defaultScaffoldNames(appId), description, }) {
|
|
126
|
+
if (kinds.length === 0) {
|
|
127
|
+
throw new Error('select at least one primitive via --with orm,function,artifact (or --orm --function …)');
|
|
128
|
+
}
|
|
129
|
+
const appRoot = resolve(rootDir);
|
|
130
|
+
if (existsSync(join(appRoot, 'sequence.app.yml'))) {
|
|
131
|
+
throw new Error(`${join(appRoot, 'sequence.app.yml')} already exists — refusing to re-init`);
|
|
132
|
+
}
|
|
133
|
+
if (existsSync(appRoot)) {
|
|
134
|
+
const { readdir } = await import('node:fs/promises');
|
|
135
|
+
const entries = await readdir(appRoot).catch((error) => {
|
|
136
|
+
if (error.code === 'ENOENT')
|
|
137
|
+
return [];
|
|
138
|
+
throw error;
|
|
139
|
+
});
|
|
140
|
+
if (entries.length > 0) {
|
|
141
|
+
throw new Error(`${appRoot} is not empty — init into a new directory, or remove existing files first`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await mkdir(appRoot, { recursive: true });
|
|
145
|
+
try {
|
|
146
|
+
const version = currentVersion();
|
|
147
|
+
const primitives = [];
|
|
148
|
+
for (const kind of kinds) {
|
|
149
|
+
const name = kind === 'function' ? names.functionName : appId;
|
|
150
|
+
primitives.push(await scaffoldKind({ kind, appRoot, appId, name }));
|
|
151
|
+
}
|
|
152
|
+
const withDeps = applyDefaultDependsOn(primitives);
|
|
153
|
+
const manifest = {
|
|
154
|
+
schema_version: 1,
|
|
155
|
+
app: {
|
|
156
|
+
id: appId,
|
|
157
|
+
title: titleizeSlug(appId),
|
|
158
|
+
...(description !== undefined ? { description } : {}),
|
|
159
|
+
},
|
|
160
|
+
studio: {
|
|
161
|
+
created_with: version,
|
|
162
|
+
min_cli: version,
|
|
163
|
+
},
|
|
164
|
+
primitives: withDeps,
|
|
165
|
+
deploy: {
|
|
166
|
+
order: defaultDeployOrder(withDeps),
|
|
167
|
+
on_error: 'stop',
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
await writeAppManifest({ rootDir: appRoot, manifest });
|
|
171
|
+
await writeFile(join(appRoot, '.gitignore'), ['node_modules/', 'dist/', '.env', '.env.*', '!.env.example', '.DS_Store', '*.log'].join('\n') + '\n', 'utf8');
|
|
172
|
+
return manifest;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
// Directory was empty (or newly created) — remove partial scaffolds so a
|
|
176
|
+
// retry can `init` into the same path without a manual cleanup.
|
|
177
|
+
await rm(appRoot, { recursive: true, force: true }).catch(() => {
|
|
178
|
+
/* best-effort */
|
|
179
|
+
});
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export async function addPrimitiveToApp({ rootDir, kind, name, }) {
|
|
184
|
+
const appRoot = resolve(rootDir);
|
|
185
|
+
const manifest = await loadAppManifest(appRoot);
|
|
186
|
+
const appId = manifest.app.id;
|
|
187
|
+
if (kind === 'orm') {
|
|
188
|
+
throw new Error('adding a second orm namespace is not supported via `seq-studio add` yet — edit sequence.app.yml and scaffold with `seq-studio orm init` manually');
|
|
189
|
+
}
|
|
190
|
+
if (kind === 'artifact' && manifest.primitives.some((entry) => entry.kind === 'artifact')) {
|
|
191
|
+
throw new Error('this app already has an artifact primitive — remove it from sequence.app.yml before re-adding');
|
|
192
|
+
}
|
|
193
|
+
const entryId = primitiveIdFor({ kind, name: kind === 'artifact' ? appId : name });
|
|
194
|
+
if (manifest.primitives.some((entry) => entry.id === entryId)) {
|
|
195
|
+
throw new Error(`primitive id "${entryId}" already exists in sequence.app.yml`);
|
|
196
|
+
}
|
|
197
|
+
const relPath = relativePathFor({ kind, appId, name }).replace(/\\/g, '/');
|
|
198
|
+
if (manifest.primitives.some((entry) => entry.path === relPath)) {
|
|
199
|
+
throw new Error(`path "${relPath}" is already declared in sequence.app.yml`);
|
|
200
|
+
}
|
|
201
|
+
const absPath = join(appRoot, relPath);
|
|
202
|
+
// Refuse before scaffolding so a failed init cannot roll back pre-existing files.
|
|
203
|
+
if (existsSync(absPath)) {
|
|
204
|
+
throw new Error(`${relPath} already exists on disk — remove it or choose a different name before \`seq-studio add\``);
|
|
205
|
+
}
|
|
206
|
+
try {
|
|
207
|
+
let entry = await scaffoldKind({ kind, appRoot, appId, name });
|
|
208
|
+
const orm = manifest.primitives.find((item) => item.kind === 'orm');
|
|
209
|
+
if (orm && entry.depends_on.length === 0) {
|
|
210
|
+
entry = { ...entry, depends_on: [orm.id] };
|
|
211
|
+
}
|
|
212
|
+
const primitives = [...manifest.primitives, entry];
|
|
213
|
+
const next = {
|
|
214
|
+
...manifest,
|
|
215
|
+
primitives,
|
|
216
|
+
deploy: {
|
|
217
|
+
...manifest.deploy,
|
|
218
|
+
order: extendDeployOrder({
|
|
219
|
+
existingOrder: manifest.deploy.order,
|
|
220
|
+
primitives,
|
|
221
|
+
newId: entry.id,
|
|
222
|
+
}),
|
|
223
|
+
on_error: manifest.deploy.on_error,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
await writeAppManifest({ rootDir: appRoot, manifest: next });
|
|
227
|
+
return { manifest: next, entry };
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
// Safe: absPath did not exist before this invocation.
|
|
231
|
+
await rm(absPath, { recursive: true, force: true }).catch(() => {
|
|
232
|
+
/* best-effort */
|
|
233
|
+
});
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export function printInitNextSteps({ appRoot, manifest, }) {
|
|
238
|
+
const rel = relative(process.cwd(), appRoot);
|
|
239
|
+
const display = !rel || rel.startsWith('..') ? appRoot : rel;
|
|
240
|
+
console.log(`${LOG} scaffolded app "${manifest.app.id}" in ${display}`);
|
|
241
|
+
console.log(`${LOG} wrote ${APP_MANIFEST_LABEL} with ${manifest.primitives.length} primitive(s):`);
|
|
242
|
+
for (const entry of manifest.primitives) {
|
|
243
|
+
console.log(`${LOG} - ${entry.kind.padEnd(9)} ${entry.id} (${entry.path})`);
|
|
244
|
+
}
|
|
245
|
+
console.log('');
|
|
246
|
+
console.log('Next:');
|
|
247
|
+
console.log(` cd ${display}`);
|
|
248
|
+
for (const entry of manifest.primitives) {
|
|
249
|
+
console.log(` (cd ${entry.path} && pnpm install)`);
|
|
250
|
+
}
|
|
251
|
+
console.log(' seq-studio login');
|
|
252
|
+
console.log(' seq-studio deploy -e local --yes');
|
|
253
|
+
console.log(' # or targeted:');
|
|
254
|
+
const fn = manifest.primitives.find((entry) => entry.kind === 'function');
|
|
255
|
+
if (fn) {
|
|
256
|
+
console.log(` seq-studio functions deploy --dir ${fn.path} -e local --yes`);
|
|
257
|
+
}
|
|
258
|
+
if (manifest.primitives.some((entry) => entry.kind === 'artifact')) {
|
|
259
|
+
console.log(' seq-studio artifact deploy artifact -e local');
|
|
260
|
+
}
|
|
261
|
+
console.log(' # add another function later:');
|
|
262
|
+
console.log(' seq-studio add function list-types');
|
|
263
|
+
}
|
package/dist/atlas-client.js
CHANGED
|
@@ -9,6 +9,7 @@ import { PREVIEW_DOMAIN } from './preview.js';
|
|
|
9
9
|
const MAX_503_RETRIES = 5;
|
|
10
10
|
const DEFAULT_RETRY_AFTER_SECONDS = 2;
|
|
11
11
|
const LOG_PREFIX = '[seq-studio]';
|
|
12
|
+
const CF_ACCESS_DOMAIN = 'seqholdings.com';
|
|
12
13
|
export class AtlasApiError extends Error {
|
|
13
14
|
status;
|
|
14
15
|
path;
|
|
@@ -42,6 +43,7 @@ async function authenticatedFetch({ baseUrl, init = {}, path, token, }) {
|
|
|
42
43
|
redirect: 'manual',
|
|
43
44
|
headers: {
|
|
44
45
|
...previewAccessHeaders(baseUrl),
|
|
46
|
+
...cfAccessHeaders(baseUrl),
|
|
45
47
|
...init.headers,
|
|
46
48
|
Authorization: `Bearer ${token}`,
|
|
47
49
|
},
|
|
@@ -63,6 +65,33 @@ function previewAccessHeaders(baseUrl) {
|
|
|
63
65
|
return {};
|
|
64
66
|
}
|
|
65
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Cloudflare Access fronts *.seqholdings.com and only bypasses the company
|
|
70
|
+
* network, so a CI runner is turned away at the edge with an HTML "Access
|
|
71
|
+
* Restricted" page before Atlas ever sees the bearer token. A service token
|
|
72
|
+
* gets through the edge; the bearer still authenticates at the app.
|
|
73
|
+
*
|
|
74
|
+
* Scoped to seqholdings.com so the token is never sent to localhost, a tenant
|
|
75
|
+
* domain, or any other host the CLI can be pointed at.
|
|
76
|
+
*/
|
|
77
|
+
function cfAccessHeaders(baseUrl) {
|
|
78
|
+
const clientId = process.env.CF_ACCESS_CLIENT_ID?.trim();
|
|
79
|
+
const clientSecret = process.env.CF_ACCESS_CLIENT_SECRET?.trim();
|
|
80
|
+
if (!clientId || !clientSecret)
|
|
81
|
+
return {};
|
|
82
|
+
try {
|
|
83
|
+
const parsed = new URL(baseUrl);
|
|
84
|
+
if (parsed.protocol !== 'https:')
|
|
85
|
+
return {};
|
|
86
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
87
|
+
if (hostname !== CF_ACCESS_DOMAIN && !hostname.endsWith(`.${CF_ACCESS_DOMAIN}`))
|
|
88
|
+
return {};
|
|
89
|
+
return { 'CF-Access-Client-Id': clientId, 'CF-Access-Client-Secret': clientSecret };
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
66
95
|
function messageFromBody(body, status, statusText) {
|
|
67
96
|
if (body && typeof body === 'object') {
|
|
68
97
|
const record = body;
|
package/dist/auth.js
CHANGED
|
@@ -43,6 +43,8 @@ const SEQUENCE_BUILTIN_ENVS = new Set(['local', 'staging', 'production', 'bankso
|
|
|
43
43
|
export function isSequenceAuthEnvName(envName) {
|
|
44
44
|
return (envName === SEQUENCE_REALM ||
|
|
45
45
|
SEQUENCE_BUILTIN_ENVS.has(envName) ||
|
|
46
|
+
envName === 'worktree' ||
|
|
47
|
+
envName.startsWith('local:') ||
|
|
46
48
|
envName === 'preview' ||
|
|
47
49
|
envName.startsWith('preview:'));
|
|
48
50
|
}
|
|
@@ -5,6 +5,20 @@ import { z } from 'zod';
|
|
|
5
5
|
* as artifact-studio's duplicated manifest. Keep the two in sync.
|
|
6
6
|
*/
|
|
7
7
|
export declare const MF_MANIFEST_FILENAME = "managed-function.yml";
|
|
8
|
+
export declare const managedFunctionAuthorizationAdapterSchema: z.ZodEnum<{
|
|
9
|
+
"encompass.loan-read-by-guid": "encompass.loan-read-by-guid";
|
|
10
|
+
"encompass.loan-read-by-number": "encompass.loan-read-by-number";
|
|
11
|
+
"encompass.loan-search": "encompass.loan-search";
|
|
12
|
+
}>;
|
|
13
|
+
export type ManagedFunctionAuthorizationAdapter = z.infer<typeof managedFunctionAuthorizationAdapterSchema>;
|
|
14
|
+
export declare const managedFunctionAuthorizationSchema: z.ZodObject<{
|
|
15
|
+
version: z.ZodLiteral<1>;
|
|
16
|
+
adapter: z.ZodEnum<{
|
|
17
|
+
"encompass.loan-read-by-guid": "encompass.loan-read-by-guid";
|
|
18
|
+
"encompass.loan-read-by-number": "encompass.loan-read-by-number";
|
|
19
|
+
"encompass.loan-search": "encompass.loan-search";
|
|
20
|
+
}>;
|
|
21
|
+
}, z.core.$strict>;
|
|
8
22
|
/**
|
|
9
23
|
* CLI-side copy of the server's egress entry validation (see
|
|
10
24
|
* atlas/src/server/services/managed-functions/egress.ts). Returns the
|
|
@@ -52,12 +66,21 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
|
|
|
52
66
|
limits: z.ZodDefault<z.ZodObject<{
|
|
53
67
|
memory_mb: z.ZodDefault<z.ZodNumber>;
|
|
54
68
|
timeout_seconds: z.ZodDefault<z.ZodNumber>;
|
|
69
|
+
min_instances: z.ZodDefault<z.ZodNumber>;
|
|
55
70
|
max_instances: z.ZodDefault<z.ZodNumber>;
|
|
56
71
|
invoke_rate_per_minute: z.ZodDefault<z.ZodNumber>;
|
|
57
72
|
}, z.core.$strip>>;
|
|
58
73
|
secrets: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
59
74
|
egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
60
75
|
service_account: z.ZodOptional<z.ZodString>;
|
|
76
|
+
authorization: z.ZodOptional<z.ZodObject<{
|
|
77
|
+
version: z.ZodLiteral<1>;
|
|
78
|
+
adapter: z.ZodEnum<{
|
|
79
|
+
"encompass.loan-read-by-guid": "encompass.loan-read-by-guid";
|
|
80
|
+
"encompass.loan-read-by-number": "encompass.loan-read-by-number";
|
|
81
|
+
"encompass.loan-search": "encompass.loan-search";
|
|
82
|
+
}>;
|
|
83
|
+
}, z.core.$strict>>;
|
|
61
84
|
input_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
62
85
|
output_schema: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
63
86
|
capabilities: z.ZodDefault<z.ZodObject<{
|
|
@@ -83,6 +106,7 @@ export declare const managedFunctionManifestSchema: z.ZodObject<{
|
|
|
83
106
|
data: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
84
107
|
tables: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
85
108
|
actions: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
109
|
+
operations: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
86
110
|
query: z.ZodDefault<z.ZodBoolean>;
|
|
87
111
|
}, z.core.$strip>>>;
|
|
88
112
|
}, z.core.$strip>>;
|
|
@@ -6,6 +6,22 @@ import { z } from 'zod';
|
|
|
6
6
|
*/
|
|
7
7
|
export const MF_MANIFEST_FILENAME = 'managed-function.yml';
|
|
8
8
|
const SECRET_NAME_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
9
|
+
export const managedFunctionAuthorizationAdapterSchema = z.enum([
|
|
10
|
+
'encompass.loan-read-by-guid',
|
|
11
|
+
'encompass.loan-read-by-number',
|
|
12
|
+
'encompass.loan-search',
|
|
13
|
+
]);
|
|
14
|
+
export const managedFunctionAuthorizationSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
version: z.literal(1),
|
|
17
|
+
adapter: managedFunctionAuthorizationAdapterSchema,
|
|
18
|
+
})
|
|
19
|
+
.strict();
|
|
20
|
+
const AUTHORIZATION_ADAPTER_BY_FUNCTION_ID = {
|
|
21
|
+
'encompass-get-common-loan-fields': 'encompass.loan-read-by-guid',
|
|
22
|
+
'encompass-get-loan-by-number': 'encompass.loan-read-by-number',
|
|
23
|
+
'encompass-search-pipeline': 'encompass.loan-search',
|
|
24
|
+
};
|
|
9
25
|
/** RFC 1035 label: alnum, optional inner dashes, max 63 chars. */
|
|
10
26
|
const DNS_LABEL_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
11
27
|
/**
|
|
@@ -493,6 +509,12 @@ function validateCapabilityGates({ gates, uses, roles, }) {
|
|
|
493
509
|
* Mirrors atlas/src/server/services/managed-functions/manifest.ts.
|
|
494
510
|
*/
|
|
495
511
|
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})$/;
|
|
512
|
+
/**
|
|
513
|
+
* GraphQL Name grammar — persisted-operation names exactly as they appear in
|
|
514
|
+
* a namespace's operations manifest. Mirrors
|
|
515
|
+
* atlas/src/server/services/managed-functions/manifest.ts.
|
|
516
|
+
*/
|
|
517
|
+
const GRAPHQL_OPERATION_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
496
518
|
export const managedFunctionManifestSchema = z.object({
|
|
497
519
|
schema_version: z.literal(1).default(1),
|
|
498
520
|
function: z.object({
|
|
@@ -510,12 +532,23 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
510
532
|
.object({
|
|
511
533
|
memory_mb: z.number().int().min(128).max(2048).default(256),
|
|
512
534
|
timeout_seconds: z.number().int().min(1).max(540).default(60),
|
|
535
|
+
min_instances: z.number().int().min(0).max(10).default(0),
|
|
513
536
|
max_instances: z.number().int().min(1).max(10).default(3),
|
|
514
537
|
invoke_rate_per_minute: z.number().int().min(1).max(600).default(60),
|
|
538
|
+
})
|
|
539
|
+
.superRefine((limits, ctx) => {
|
|
540
|
+
if (limits.min_instances > limits.max_instances) {
|
|
541
|
+
ctx.addIssue({
|
|
542
|
+
code: 'custom',
|
|
543
|
+
message: 'min_instances cannot exceed max_instances',
|
|
544
|
+
path: ['min_instances'],
|
|
545
|
+
});
|
|
546
|
+
}
|
|
515
547
|
})
|
|
516
548
|
.default({
|
|
517
549
|
memory_mb: 256,
|
|
518
550
|
timeout_seconds: 60,
|
|
551
|
+
min_instances: 0,
|
|
519
552
|
max_instances: 3,
|
|
520
553
|
invoke_rate_per_minute: 60,
|
|
521
554
|
}),
|
|
@@ -554,6 +587,11 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
554
587
|
.string()
|
|
555
588
|
.regex(SERVICE_ACCOUNT_REF_RE, 'service_account must be a platform service-account slug (lowercase alphanumeric with hyphens) or uuid')
|
|
556
589
|
.optional(),
|
|
590
|
+
/**
|
|
591
|
+
* Server-owned resource authorization. Authors select a reviewed adapter;
|
|
592
|
+
* they cannot provide input/output paths or executable policy.
|
|
593
|
+
*/
|
|
594
|
+
authorization: managedFunctionAuthorizationSchema.optional(),
|
|
557
595
|
input_schema: z.record(z.string(), z.unknown()).optional(),
|
|
558
596
|
output_schema: z.record(z.string(), z.unknown()).optional(),
|
|
559
597
|
/**
|
|
@@ -567,12 +605,13 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
567
605
|
gates: capabilityGatesSchema,
|
|
568
606
|
/**
|
|
569
607
|
* ORM Data API consumer reach, grouped by namespace: the `tables` this
|
|
570
|
-
* function may read, the `actions`
|
|
571
|
-
* (arbitrary read SQL over the namespace)
|
|
572
|
-
* invoking user. The invoke proxy mints
|
|
573
|
-
* exactly these refs; the Data API
|
|
574
|
-
*
|
|
575
|
-
*
|
|
608
|
+
* function may read, the v1 `actions` and v2 persisted `operations` it may
|
|
609
|
+
* invoke, and whether raw `query` (arbitrary read SQL over the namespace)
|
|
610
|
+
* is allowed — all ON BEHALF OF the invoking user. The invoke proxy mints
|
|
611
|
+
* a short-lived token scoped to exactly these refs; the Data API
|
|
612
|
+
* re-resolves the user's claims per call, so declaring a table never
|
|
613
|
+
* widens what the user could see. Writes go through declared actions and
|
|
614
|
+
* operations only — raw table writes don't exist.
|
|
576
615
|
*
|
|
577
616
|
* Mirrors atlas/src/server/services/managed-functions/manifest.ts — must
|
|
578
617
|
* stay in sync so `seq-studio` doesn't strip the block before deploy.
|
|
@@ -581,11 +620,47 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
581
620
|
.record(z.string().regex(/^[a-z][a-z0-9_]{0,40}$/, 'capabilities.data keys are ORM namespace names'), z.object({
|
|
582
621
|
tables: z.array(z.string().regex(/^[a-z_][a-z0-9_]*$/, 'table names')).max(64).default([]),
|
|
583
622
|
actions: z.array(z.string().regex(/^[a-z_][a-z0-9_]*$/, 'action names')).max(64).default([]),
|
|
623
|
+
/**
|
|
624
|
+
* ORM v2 persisted operations (GraphQL mutations/actions by name)
|
|
625
|
+
* this function may execute — each mints a `<ns>/ops/<Name>` write
|
|
626
|
+
* ref on the invoke token.
|
|
627
|
+
*/
|
|
628
|
+
operations: z
|
|
629
|
+
.array(z
|
|
630
|
+
.string()
|
|
631
|
+
.max(128)
|
|
632
|
+
.regex(GRAPHQL_OPERATION_NAME_RE, 'operation names must match the GraphQL Name grammar'))
|
|
633
|
+
.max(64)
|
|
634
|
+
.default([]),
|
|
584
635
|
query: z.boolean().default(false),
|
|
585
636
|
}))
|
|
586
637
|
.default({}),
|
|
587
638
|
}).default({ uses: [], gates: [], data: {} }),
|
|
588
639
|
}).superRefine((manifest, ctx) => {
|
|
640
|
+
const expectedAdapter = AUTHORIZATION_ADAPTER_BY_FUNCTION_ID[manifest.function.id];
|
|
641
|
+
if (expectedAdapter !== undefined && manifest.authorization === undefined) {
|
|
642
|
+
ctx.addIssue({
|
|
643
|
+
code: 'custom',
|
|
644
|
+
message: `function "${manifest.function.id}" requires authorization adapter "${expectedAdapter}"`,
|
|
645
|
+
path: ['authorization'],
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
else if (expectedAdapter !== undefined &&
|
|
649
|
+
manifest.authorization?.adapter !== expectedAdapter) {
|
|
650
|
+
ctx.addIssue({
|
|
651
|
+
code: 'custom',
|
|
652
|
+
message: `function "${manifest.function.id}" must use authorization adapter "${expectedAdapter}"`,
|
|
653
|
+
path: ['authorization', 'adapter'],
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
else if (expectedAdapter === undefined &&
|
|
657
|
+
manifest.authorization !== undefined) {
|
|
658
|
+
ctx.addIssue({
|
|
659
|
+
code: 'custom',
|
|
660
|
+
message: `function "${manifest.function.id}" is not registered for a server-owned authorization adapter`,
|
|
661
|
+
path: ['authorization'],
|
|
662
|
+
});
|
|
663
|
+
}
|
|
589
664
|
for (const message of validateCapabilityPins({
|
|
590
665
|
uses: manifest.capabilities.uses,
|
|
591
666
|
roles: manifest.capabilities.roles,
|
|
@@ -600,9 +675,9 @@ export const managedFunctionManifestSchema = z.object({
|
|
|
600
675
|
ctx.addIssue({ code: 'custom', message, path: ['capabilities', 'gates'] });
|
|
601
676
|
}
|
|
602
677
|
// A namespace is "reached" when its block declares at least one table,
|
|
603
|
-
// action, or raw query — the same rule the server's floor
|
|
604
|
-
// uses (readManifestDataNamespaces).
|
|
605
|
-
const reachesData = Object.values(manifest.capabilities.data).some((block) => block.tables.length > 0 || block.actions.length > 0 || block.query);
|
|
678
|
+
// action, operation, or raw query — the same rule the server's floor
|
|
679
|
+
// reconciliation uses (readManifestDataNamespaces).
|
|
680
|
+
const reachesData = Object.values(manifest.capabilities.data).some((block) => block.tables.length > 0 || block.actions.length > 0 || block.operations.length > 0 || block.query);
|
|
606
681
|
if (reachesData && manifest.service_account === undefined) {
|
|
607
682
|
ctx.addIssue({
|
|
608
683
|
code: 'custom',
|
package/dist/main.d.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* seq-studio argv router.
|
|
3
3
|
*
|
|
4
4
|
* Top-level commands:
|
|
5
|
+
* seq-studio init scaffold an app monorepo (sequence.app.yml)
|
|
6
|
+
* seq-studio add add a primitive to an app monorepo
|
|
7
|
+
* seq-studio deploy deploy all primitives from sequence.app.yml
|
|
5
8
|
* seq-studio process <sub> manage Lattice processes
|
|
6
9
|
* seq-studio artifact <sub> manage Artifact Studio apps
|
|
7
10
|
* seq-studio functions <sub> manage Managed Functions
|
package/dist/main.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* seq-studio argv router.
|
|
3
3
|
*
|
|
4
4
|
* Top-level commands:
|
|
5
|
+
* seq-studio init scaffold an app monorepo (sequence.app.yml)
|
|
6
|
+
* seq-studio add add a primitive to an app monorepo
|
|
7
|
+
* seq-studio deploy deploy all primitives from sequence.app.yml
|
|
5
8
|
* seq-studio process <sub> manage Lattice processes
|
|
6
9
|
* seq-studio artifact <sub> manage Artifact Studio apps
|
|
7
10
|
* seq-studio functions <sub> manage Managed Functions
|
|
@@ -21,6 +24,9 @@ import { applyCommand, bundleCommand, doctorCommand, initCommand, lintCommand, p
|
|
|
21
24
|
// Lazy-load artifact delegate: published @sequenceholdings/artifact-studio/cli
|
|
22
25
|
// still auto-runs runCli() at module load; importing it here breaks doctor/process.
|
|
23
26
|
const TOP_LEVEL_USAGE = `usage:
|
|
27
|
+
seq-studio init <dir> --with <kinds> scaffold an app monorepo (sequence.app.yml)
|
|
28
|
+
seq-studio add <kind> <name> add a primitive to the current app monorepo
|
|
29
|
+
seq-studio deploy -e <env> deploy primitives from sequence.app.yml
|
|
24
30
|
seq-studio process <sub> [args] lint | plan | apply | test | simulate | bundle | init
|
|
25
31
|
seq-studio artifact <sub> [args] init | build | plan | deploy | dev | list | show | pull | promote | rollback
|
|
26
32
|
seq-studio functions <sub> [args] init | build | deploy | list | show | logs | promote | rollback | delete
|
|
@@ -65,6 +71,21 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
65
71
|
return namespace ? 0 : 1;
|
|
66
72
|
}
|
|
67
73
|
switch (namespace) {
|
|
74
|
+
case 'init': {
|
|
75
|
+
const { runAppInitCommand } = await import('./app/commands.js');
|
|
76
|
+
return runAppInitCommand(parseArgs([sub, ...rest].filter((a) => Boolean(a))));
|
|
77
|
+
}
|
|
78
|
+
case 'add': {
|
|
79
|
+
const { runAppAddCommand } = await import('./app/commands.js');
|
|
80
|
+
return runAppAddCommand({
|
|
81
|
+
kindArg: sub,
|
|
82
|
+
args: parseArgs(rest),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
case 'deploy': {
|
|
86
|
+
const { runAppDeployCommand } = await import('./app/commands.js');
|
|
87
|
+
return runAppDeployCommand(parseArgs([sub, ...rest].filter((a) => Boolean(a))));
|
|
88
|
+
}
|
|
68
89
|
case 'process':
|
|
69
90
|
return runProcessNamespace(sub, rest);
|
|
70
91
|
case 'artifact': {
|