@walkeros/config 4.7.0-next-1790245733116 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,28 @@
1
1
  # @walkeros/config
2
2
 
3
- ## 4.7.0-next-1790245733116
3
+ ## 4.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 64b06de: Packages with several exports, such as the GCP and AWS sources and
8
+ destinations, now publish a settings schema per export. `walkeros validate`
9
+ and MCP `package_get` use the schema of the export a step imports. Pub/Sub,
10
+ BigQuery, SNS and SQS steps pinned to this release or later are checked
11
+ instead of skipped; older pins still report a skip.
12
+
13
+ ### Patch Changes
14
+
15
+ - 74821ed: Simulate records every vendor call through one recorder, `observeEnv`
16
+ from `@walkeros/core`, and runs offline: BigQuery, Data Manager and the
17
+ Sheets, GCS and S3 stores reach the network through `env`. Client and `Env`
18
+ types of BigQuery, Firehose, PostHog, SQS and Pub/Sub are structural.
19
+ `@walkeros/cli` exports `resolveExportName` and `selectDevExamples` and drops
20
+ `findExample` and `compareOutput`.
21
+ - 74821ed: Simulate starts only the simulated step, and a simulated transformer
22
+ continues through its `next`. Source simulations list the commands a source
23
+ issues as `elb` calls; `--page-url` sets the web page. Express flows need no
24
+ port. Packages with several exports use per-export examples and mocks, and a
25
+ destination without a mock env is refused instead of calling the vendor.
4
26
 
5
27
  ## 4.6.1
6
28
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@walkeros/config",
3
- "version": "4.7.0-next-1790245733116",
3
+ "version": "4.7.0",
4
4
  "type": "module",
5
5
  "description": "Shared development configuration for walkerOS packages (TypeScript, ESLint, Jest, tsup)",
6
6
  "license": "MIT",
@@ -0,0 +1,136 @@
1
+ import {
2
+ assertNoPublishedCredentials,
3
+ findPublishedCredential,
4
+ } from '../credentialGuard.mjs';
5
+
6
+ const keyMaterial =
7
+ 'MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7kJ8sL2mNqRtVxYz'.repeat(
8
+ 3,
9
+ );
10
+ const wrapped = keyMaterial.match(/.{1,64}/g).join('\n');
11
+ const realPem = `-----BEGIN PRIVATE KEY-----\n${wrapped}\n-----END PRIVATE KEY-----\n`;
12
+
13
+ describe('assertNoPublishedCredentials', () => {
14
+ it.each([
15
+ ['a PEM with real key material', realPem, 'PEM private key'],
16
+ [
17
+ 'a PEM with escaped newlines',
18
+ realPem.replace(/\n/g, '\\n'),
19
+ 'PEM private key',
20
+ ],
21
+ ['a live Stripe key', 'sk_live_51Habcdefghijkl', 'live key token sk_live_'],
22
+ ['a GitHub token', 'token ghp_abcdefghijklmnop', 'live key token ghp_'],
23
+ ['a Slack bot token', 'xoxb-1234-5678-abcd', 'live key token xoxb-'],
24
+ ['an AWS access key id', 'AKIAABCDEFGHIJKLMNOP', 'live key token AKIA'],
25
+ ])(
26
+ 'throws on %s with the path and fix, never the value',
27
+ (_l, value, rule) => {
28
+ const examples = { step: { in: { settings: { credentials: value } } } };
29
+ let message = '';
30
+ try {
31
+ assertNoPublishedCredentials('@walkeros/x', { examples });
32
+ } catch (error) {
33
+ message = error.message;
34
+ }
35
+ expect(message).toBe(
36
+ `@walkeros/x: examples.step.in.settings.credentials looks like a real credential (${rule}). Replace it with a placeholder, e.g. sk_test_..., or a PEM with fewer than 64 characters of key material.`,
37
+ );
38
+ expect(message).not.toContain(value);
39
+ },
40
+ );
41
+
42
+ it.each(['github_pat_', 'ghu_', 'ghs_', 'ghr_'])(
43
+ 'throws on a GitHub %s token',
44
+ (prefix) => {
45
+ const value = `${prefix}${'a1B2c3D4e5'.repeat(3)}f6G7h8`;
46
+ expect(() =>
47
+ assertNoPublishedCredentials('@walkeros/x', {
48
+ examples: { step: { in: value } },
49
+ }),
50
+ ).toThrow(`(live key token ${prefix})`);
51
+ },
52
+ );
53
+
54
+ it.each([
55
+ [
56
+ 'a credential as an object key',
57
+ { step: { in: { sk_live_51Habcdefghijkl: 'placeholder' } } },
58
+ 'examples.step.in.<key>',
59
+ 'live key token sk_live_',
60
+ ['sk_live_51Habcdefghijkl'],
61
+ ],
62
+ [
63
+ 'a credential key above a credential value',
64
+ {
65
+ step: {
66
+ in: {
67
+ ghs_abcdefghijklmnopqrstuvwxyz0123456789: {
68
+ token: 'github_pat_abcdefghijklmnopqrstuvwxyz0123456789',
69
+ },
70
+ },
71
+ },
72
+ },
73
+ 'examples.step.in.<key>',
74
+ 'live key token ghs_',
75
+ [
76
+ 'ghs_abcdefghijklmnopqrstuvwxyz0123456789',
77
+ 'github_pat_abcdefghijklmnopqrstuvwxyz0123456789',
78
+ ],
79
+ ],
80
+ ])(
81
+ 'throws on %s without the key or value text',
82
+ (_l, examples, path, rule, secrets) => {
83
+ let message = '';
84
+ try {
85
+ assertNoPublishedCredentials('@walkeros/x', { examples });
86
+ } catch (error) {
87
+ message = error.message;
88
+ }
89
+ expect(message).toBe(
90
+ `@walkeros/x: ${path} looks like a real credential (${rule}). Replace it with a placeholder, e.g. sk_test_..., or a PEM with fewer than 64 characters of key material.`,
91
+ );
92
+ for (const secret of secrets) expect(message).not.toContain(secret);
93
+ },
94
+ );
95
+
96
+ it('passes a plain header key with a placeholder value', () => {
97
+ expect(() =>
98
+ assertNoPublishedCredentials('@walkeros/x', {
99
+ examples: {
100
+ step: { in: { headers: { Authorization: 'Bearer ***' } } },
101
+ },
102
+ }),
103
+ ).not.toThrow();
104
+ });
105
+
106
+ it('names the section and array index of the hit', () => {
107
+ expect(
108
+ findPublishedCredential({
109
+ examples: {},
110
+ hints: { auth: { examples: ['ok', 'sk_live_abc'] } },
111
+ }),
112
+ ).toEqual({
113
+ path: 'hints.auth.examples.1',
114
+ rule: 'live key token sk_live_',
115
+ });
116
+ });
117
+
118
+ it.each([
119
+ ['a short PEM placeholder', '-----BEGIN PRIVATE KEY-----\ntest'],
120
+ [
121
+ 'an escaped short PEM placeholder',
122
+ '-----BEGIN PRIVATE KEY-----\\ntest\\n-----END PRIVATE KEY-----\\n',
123
+ ],
124
+ ['a test Stripe key', 'sk_test_x'],
125
+ ['the AWS documentation key id', 'AKIDEXAMPLE'],
126
+ ['a prefix inside a word', 'mask_live_ and task_ghp_x'],
127
+ ])('passes %s', (_l, value) => {
128
+ expect(() =>
129
+ assertNoPublishedCredentials('@walkeros/x', {
130
+ examples: { step: { in: value } },
131
+ exportExamples: { other: { step: { out: [value] } } },
132
+ hints: undefined,
133
+ }),
134
+ ).not.toThrow();
135
+ });
136
+ });
@@ -0,0 +1,129 @@
1
+ import {
2
+ assertExportMaps,
3
+ assertStepSettings,
4
+ publishedExports,
5
+ } from '../exportMaps.mjs';
6
+
7
+ const two = { destinationA: 'A', destinationB: 'B' };
8
+ const examples = { destinationA: {}, destinationB: {} };
9
+ const schemas = {
10
+ destinationA: { settings: {} },
11
+ destinationB: { settings: {}, setup: {} },
12
+ };
13
+
14
+ describe('assertExportMaps', () => {
15
+ it.each([
16
+ ['no declared exports and no maps', undefined, {}],
17
+ ['a single export and no maps', { destinationA: 'A' }, {}],
18
+ [
19
+ 'two exports with matching maps',
20
+ two,
21
+ { exportExamples: examples, exportSchemas: schemas },
22
+ ],
23
+ ])('passes for %s', (_label, declared, maps) => {
24
+ expect(() => assertExportMaps('@walkeros/x', declared, maps)).not.toThrow();
25
+ });
26
+
27
+ it.each([
28
+ [
29
+ 'exportSchemas missing',
30
+ two,
31
+ { exportExamples: examples },
32
+ '@walkeros/x: exportSchemas is missing',
33
+ ],
34
+ [
35
+ 'exportExamples missing',
36
+ two,
37
+ { exportSchemas: schemas },
38
+ '@walkeros/x: exportExamples is missing',
39
+ ],
40
+ [
41
+ 'an export missing from exportSchemas',
42
+ two,
43
+ {
44
+ exportExamples: examples,
45
+ exportSchemas: { destinationA: { settings: {} } },
46
+ },
47
+ '@walkeros/x: exportSchemas is missing export "destinationB"',
48
+ ],
49
+ [
50
+ 'an undeclared export in exportSchemas',
51
+ two,
52
+ {
53
+ exportExamples: examples,
54
+ exportSchemas: { ...schemas, destinationC: { settings: {} } },
55
+ },
56
+ '@walkeros/x: exportSchemas has undeclared export "destinationC"',
57
+ ],
58
+ [
59
+ 'an exportSchemas entry without settings',
60
+ two,
61
+ {
62
+ exportExamples: examples,
63
+ exportSchemas: { ...schemas, destinationB: { setup: {} } },
64
+ },
65
+ '@walkeros/x: exportSchemas.destinationB has no settings schema',
66
+ ],
67
+ [
68
+ 'an export missing from exportExamples',
69
+ two,
70
+ { exportExamples: { destinationA: {} }, exportSchemas: schemas },
71
+ '@walkeros/x: exportExamples is missing export "destinationB"',
72
+ ],
73
+ [
74
+ 'maps on a single-export package',
75
+ { destinationA: 'A' },
76
+ { exportSchemas: { destinationA: { settings: {} } } },
77
+ '@walkeros/x: exportSchemas is set but the package declares fewer than two exports',
78
+ ],
79
+ ])('throws for %s', (_label, declared, maps, message) => {
80
+ expect(() => assertExportMaps('@walkeros/x', declared, maps)).toThrow(
81
+ new Error(message),
82
+ );
83
+ });
84
+ });
85
+
86
+ describe('assertStepSettings', () => {
87
+ it.each(['source', 'transformer', 'destination', 'store'])(
88
+ 'throws for a %s package without schemas.settings',
89
+ (type) => {
90
+ expect(() => assertStepSettings('@walkeros/x', type, {})).toThrow(
91
+ new Error('@walkeros/x: schemas.settings is missing'),
92
+ );
93
+ },
94
+ );
95
+
96
+ it.each([
97
+ ['a destination with settings', 'destination', { settings: {} }],
98
+ ['a package without a type', undefined, {}],
99
+ ['a non-step package', 'core', {}],
100
+ ])('passes for %s', (_label, type, published) => {
101
+ expect(() =>
102
+ assertStepSettings('@walkeros/x', type, published),
103
+ ).not.toThrow();
104
+ });
105
+ });
106
+
107
+ describe('publishedExports', () => {
108
+ it.each([
109
+ [
110
+ 'a multi-export package keeps its declared exports',
111
+ { type: 'destination', exports: two },
112
+ two,
113
+ ],
114
+ [
115
+ 'a single-export package keeps its one declared export',
116
+ { type: 'destination', exports: { destinationA: 'A' } },
117
+ { destinationA: 'A' },
118
+ ],
119
+ [
120
+ 'a step package without declared exports publishes an empty list',
121
+ { type: 'source' },
122
+ {},
123
+ ],
124
+ ['a non-step package publishes no list', { type: 'core' }, undefined],
125
+ ['a package without a type publishes no list', {}, undefined],
126
+ ])('%s', (_label, walkerOS, expected) => {
127
+ expect(publishedExports(walkerOS)).toEqual(expected);
128
+ });
129
+ });
@@ -0,0 +1,87 @@
1
+ // Guard for published step metadata: dist/walkerOS.json ships to npm and the
2
+ // hub, so a real credential pasted into an example or a hint must fail the
3
+ // build. Placeholders pass: a PEM with under 64 characters of key material,
4
+ // `sk_test_...`, `AKIDEXAMPLE`.
5
+
6
+ // Base64 characters of key material a PEM body needs to count as real.
7
+ const MIN_PEM_KEY_CHARS = 64;
8
+
9
+ // The marker, then the body: base64 characters, whitespace or an escaped
10
+ // `\n`. The alternatives start on different characters, so the scan is linear.
11
+ const RE_PEM_BODY =
12
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----((?:[A-Za-z0-9+/=]|\s|\\n)*)/g;
13
+
14
+ const RE_LIVE_TOKEN =
15
+ /(?:^|[^A-Za-z0-9_])(sk_live_|github_pat_|ghp_|gho_|ghu_|ghs_|ghr_|xoxb-|xoxp-|AKIA[A-Z0-9]{16})/;
16
+
17
+ // Path segment reported for an object key that breaks a rule: the key text is
18
+ // the credential, so it never appears in the message.
19
+ const KEY_PLACEHOLDER = '<key>';
20
+
21
+ function countKeyChars(body) {
22
+ return body.replace(/\\n|\s/g, '').length;
23
+ }
24
+
25
+ /** The rule a string breaks, or undefined when it looks like a placeholder. */
26
+ function credentialRule(value) {
27
+ RE_PEM_BODY.lastIndex = 0;
28
+ let match;
29
+ while ((match = RE_PEM_BODY.exec(value)) !== null) {
30
+ if (countKeyChars(match[1]) >= MIN_PEM_KEY_CHARS) return 'PEM private key';
31
+ }
32
+ const token = RE_LIVE_TOKEN.exec(value);
33
+ if (token) {
34
+ const prefix = token[1].startsWith('AKIA') ? 'AKIA' : token[1];
35
+ return `live key token ${prefix}`;
36
+ }
37
+ return undefined;
38
+ }
39
+
40
+ function findInValue(value, path) {
41
+ if (typeof value === 'string') {
42
+ const rule = credentialRule(value);
43
+ return rule ? { path, rule } : undefined;
44
+ }
45
+ if (Array.isArray(value)) {
46
+ for (let i = 0; i < value.length; i++) {
47
+ const hit = findInValue(value[i], `${path}.${i}`);
48
+ if (hit) return hit;
49
+ }
50
+ return undefined;
51
+ }
52
+ if (value && typeof value === 'object') {
53
+ for (const [key, child] of Object.entries(value)) {
54
+ // A key is published text too: check it before descending.
55
+ const keyRule = credentialRule(key);
56
+ if (keyRule) return { path: `${path}.${KEY_PLACEHOLDER}`, rule: keyRule };
57
+ const hit = findInValue(child, `${path}.${key}`);
58
+ if (hit) return hit;
59
+ }
60
+ }
61
+ return undefined;
62
+ }
63
+
64
+ /**
65
+ * The first serialized string in `sections` (e.g. `{ examples, hints }`) that
66
+ * looks like a real credential, as `{ path, rule }`; undefined when none does.
67
+ * The value itself is never returned.
68
+ */
69
+ function findPublishedCredential(sections) {
70
+ for (const [name, value] of Object.entries(sections)) {
71
+ if (value === undefined) continue;
72
+ const hit = findInValue(value, name);
73
+ if (hit) return hit;
74
+ }
75
+ return undefined;
76
+ }
77
+
78
+ /** Throw when a published section carries a real-looking credential. */
79
+ function assertNoPublishedCredentials(packageName, sections) {
80
+ const hit = findPublishedCredential(sections);
81
+ if (!hit) return;
82
+ throw new Error(
83
+ `${packageName}: ${hit.path} looks like a real credential (${hit.rule}). Replace it with a placeholder, e.g. sk_test_..., or a PEM with fewer than 64 characters of key material.`,
84
+ );
85
+ }
86
+
87
+ export { findPublishedCredential, assertNoPublishedCredentials };
@@ -0,0 +1,78 @@
1
+ // Guards for published step metadata in dist/walkerOS.json. A package that
2
+ // declares two or more exports in package.json `walkerOS.exports` ships
3
+ // `exportExamples` and `exportSchemas` keyed by exactly those export names,
4
+ // the default export included, so a reader never falls back to another
5
+ // export's examples or settings schema.
6
+
7
+ const STEP_TYPES = ['source', 'transformer', 'destination', 'store'];
8
+
9
+ function isRecord(value) {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ function assertKeys(packageName, mapName, map, declared) {
14
+ if (!isRecord(map)) throw new Error(`${packageName}: ${mapName} is missing`);
15
+ for (const name of declared) {
16
+ if (!(name in map))
17
+ throw new Error(`${packageName}: ${mapName} is missing export "${name}"`);
18
+ }
19
+ for (const name of Object.keys(map)) {
20
+ if (!declared.includes(name))
21
+ throw new Error(
22
+ `${packageName}: ${mapName} has undeclared export "${name}"`,
23
+ );
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Throw when the per-export maps do not match the declared exports: with two
29
+ * or more exports both maps list every export and each `exportSchemas` entry
30
+ * carries a `settings` schema; with fewer, neither map is present.
31
+ */
32
+ function assertExportMaps(packageName, declaredExports, maps) {
33
+ const declared = isRecord(declaredExports)
34
+ ? Object.keys(declaredExports)
35
+ : [];
36
+ const { exportExamples, exportSchemas } = maps;
37
+
38
+ if (declared.length < 2) {
39
+ for (const [mapName, map] of [
40
+ ['exportExamples', exportExamples],
41
+ ['exportSchemas', exportSchemas],
42
+ ]) {
43
+ if (map !== undefined)
44
+ throw new Error(
45
+ `${packageName}: ${mapName} is set but the package declares fewer than two exports`,
46
+ );
47
+ }
48
+ return;
49
+ }
50
+
51
+ assertKeys(packageName, 'exportExamples', exportExamples, declared);
52
+ assertKeys(packageName, 'exportSchemas', exportSchemas, declared);
53
+ for (const [name, entry] of Object.entries(exportSchemas)) {
54
+ if (!isRecord(entry) || !isRecord(entry.settings))
55
+ throw new Error(
56
+ `${packageName}: exportSchemas.${name} has no settings schema`,
57
+ );
58
+ }
59
+ }
60
+
61
+ /** Throw when a step package (source, transformer, destination, store) publishes no settings schema. */
62
+ function assertStepSettings(packageName, type, schemas) {
63
+ if (!STEP_TYPES.includes(type)) return;
64
+ if (!isRecord(schemas) || !isRecord(schemas.settings))
65
+ throw new Error(`${packageName}: schemas.settings is missing`);
66
+ }
67
+
68
+ /**
69
+ * The `$meta.exports` a package publishes: its declared `walkerOS.exports`,
70
+ * else an empty list for a step package, so a reader can tell a package
71
+ * without named exports from an older package that published no list.
72
+ */
73
+ function publishedExports(walkerOS) {
74
+ if (isRecord(walkerOS.exports)) return walkerOS.exports;
75
+ return STEP_TYPES.includes(walkerOS.type) ? {} : undefined;
76
+ }
77
+
78
+ export { assertExportMaps, assertStepSettings, publishedExports };
package/tsup/index.mjs CHANGED
@@ -2,6 +2,12 @@ import { defineConfig } from 'tsup';
2
2
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
3
3
  import { resolve, join } from 'path';
4
4
  import { pathToFileURL } from 'url';
5
+ import { assertNoPublishedCredentials } from './credentialGuard.mjs';
6
+ import {
7
+ assertExportMaps,
8
+ assertStepSettings,
9
+ publishedExports,
10
+ } from './exportMaps.mjs';
5
11
 
6
12
  const baseConfig = {
7
13
  entry: ['src/index.ts'],
@@ -16,7 +22,10 @@ const getVersion = () => {
16
22
  const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
17
23
  return pkg.version || '0.0.0';
18
24
  } catch (error) {
19
- console.warn('Could not read package.json for version injection:', error.message);
25
+ console.warn(
26
+ 'Could not read package.json for version injection:',
27
+ error.message,
28
+ );
20
29
  return '0.0.0';
21
30
  }
22
31
  };
@@ -140,7 +149,9 @@ const buildDev = (customConfig = {}) => {
140
149
  // Check the built module exists
141
150
  const devMjsPath = resolve(cwd, 'dist/dev.mjs');
142
151
  if (!existsSync(devMjsPath)) {
143
- console.warn('[buildDev] dist/dev.mjs not found, skipping walkerOS.json generation');
152
+ console.warn(
153
+ '[buildDev] dist/dev.mjs not found, skipping walkerOS.json generation',
154
+ );
144
155
  return;
145
156
  }
146
157
 
@@ -173,7 +184,8 @@ const buildDev = (customConfig = {}) => {
173
184
  : [walkerOS.platform];
174
185
  }
175
186
  if (walkerOS.renderer) meta.renderer = walkerOS.renderer;
176
- if (walkerOS.exports) meta.exports = walkerOS.exports;
187
+ const exports = publishedExports(walkerOS);
188
+ if (exports) meta.exports = exports;
177
189
 
178
190
  // Docs URL (manual, from walkerOS.docs in package.json)
179
191
  if (walkerOS.docs) meta.docs = walkerOS.docs;
@@ -181,7 +193,9 @@ const buildDev = (customConfig = {}) => {
181
193
  // Source URL (auto-derived from repository.directory)
182
194
  const repo = pkg.repository;
183
195
  if (repo?.directory) {
184
- const repoUrl = (repo.url || '').replace(/^git\+/, '').replace(/\.git$/, '');
196
+ const repoUrl = (repo.url || '')
197
+ .replace(/^git\+/, '')
198
+ .replace(/\.git$/, '');
185
199
  if (repoUrl) meta.source = `${repoUrl}/tree/main/${repo.directory}/src`;
186
200
  }
187
201
 
@@ -191,10 +205,39 @@ const buildDev = (customConfig = {}) => {
191
205
  : undefined;
192
206
 
193
207
  const output = { $meta: meta, schemas, examples };
208
+
209
+ // Multi-export packages ship examples per export, keyed by export name
210
+ if (devModule.exportExamples) {
211
+ const exportExamples = toSerializable(devModule.exportExamples);
212
+ if (exportExamples && Object.keys(exportExamples).length > 0)
213
+ output.exportExamples = exportExamples;
214
+ }
215
+
216
+ // Multi-export packages ship schemas per export, same shape as schemas
217
+ if (devModule.exportSchemas) {
218
+ const exportSchemas = toSerializable(devModule.exportSchemas);
219
+ if (exportSchemas && Object.keys(exportSchemas).length > 0)
220
+ output.exportSchemas = exportSchemas;
221
+ }
222
+
194
223
  if (hints && Object.keys(hints).length > 0) {
195
224
  output.hints = hints;
196
225
  }
197
226
 
227
+ // Published metadata must never carry a real credential: fail the build
228
+ assertNoPublishedCredentials(pkg.name, {
229
+ examples: output.examples,
230
+ exportExamples: output.exportExamples,
231
+ hints: output.hints,
232
+ });
233
+
234
+ // Per-export maps match walkerOS.exports; step packages ship settings
235
+ assertExportMaps(pkg.name, walkerOS.exports, {
236
+ exportExamples: output.exportExamples,
237
+ exportSchemas: output.exportSchemas,
238
+ });
239
+ assertStepSettings(pkg.name, walkerOS.type, schemas);
240
+
198
241
  // Validate
199
242
  if (Object.keys(schemas).length === 0) {
200
243
  console.warn('[buildDev] Warning: schemas is empty');