@mettlecast/domain-cli 0.2.0 → 0.2.2
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/builder/build-registry.js +3 -0
- package/dist/builder/load-module.js +5 -2
- package/dist/commands/create-project.js +1 -1
- package/dist/commands/doctor.js +10 -10
- package/dist/commands/upgrade-frontend.js +0 -2
- package/dist/commands/upgrade.js +4 -0
- package/dist/templates/patterns/api/create-with-event.js +3 -7
- package/dist/templates/patterns/api/idempotent-mutation.js +17 -40
- package/dist/templates/patterns/api/paginated-list.js +1 -9
- package/dist/templates/patterns/api/simple-crud.js +2 -2
- package/dist/templates/patterns/api/system-admin.js +1 -4
- package/dist/templates/patterns/api/webhook-receiver-style.js +7 -53
- package/dist/templates/patterns/subscriber/audit-relay.js +14 -24
- package/dist/templates/patterns/subscriber/cascade-deletion.js +6 -22
- package/dist/templates/patterns/subscriber/single-step-projection.js +6 -10
- package/dist/templates/subscriber-skeleton.js +1 -1
- package/dist/utils/install-file.d.ts +5 -6
- package/dist/utils/install-file.js +9 -25
- package/dist/utils/manifest.d.ts +2 -2
- package/dist/utils/manifest.js +47 -5
- package/dist/utils/scaffold-config.d.ts +7 -0
- package/dist/utils/scaffold-config.js +3 -0
- package/package.json +1 -1
- package/src/__tests__/commands/upgrade.test.ts +8 -8
- package/src/__tests__/doctor.test.ts +16 -11
- package/src/__tests__/scaffold-src/part-a-layout.test.ts +0 -8
- package/src/__tests__/scripts/package-scaffold.test.ts +14 -14
- package/src/__tests__/utils/install-file.test.ts +58 -35
- package/src/__tests__/utils/manifest.test.ts +74 -29
- package/src/builder/build-registry.ts +2 -0
- package/src/builder/load-module.ts +5 -3
- package/src/commands/add-module.ts +1 -1
- package/src/commands/create-project.ts +2 -2
- package/src/commands/doctor.ts +11 -11
- package/src/commands/upgrade-frontend.ts +0 -2
- package/src/commands/upgrade.ts +7 -2
- package/src/templates/dashboard-pages/account/api-keys.tsx +1 -1
- package/src/templates/dashboard-pages/account/audit-log.tsx +1 -1
- package/src/templates/dashboard-pages/account/members.tsx +1 -1
- package/src/templates/dashboard-pages/account/profile.tsx +1 -1
- package/src/templates/dashboard-pages/account/workspace-settings.tsx +1 -1
- package/src/templates/dashboard-pages/auth/accept-invitation.tsx +1 -1
- package/src/templates/dashboard-pages/auth/choose-org.tsx +1 -1
- package/src/templates/dashboard-pages/auth/forgot-password.tsx +1 -1
- package/src/templates/dashboard-pages/auth/login.tsx +1 -1
- package/src/templates/dashboard-pages/auth/mfa-setup.tsx +1 -1
- package/src/templates/dashboard-pages/auth/mfa-verify.tsx +1 -1
- package/src/templates/dashboard-pages/auth/reset-password.tsx +1 -1
- package/src/templates/dashboard-pages/auth/signup.tsx +1 -1
- package/src/templates/dashboard-pages/auth/verify-email.tsx +1 -1
- package/src/templates/patterns/api/create-with-event.ts +3 -7
- package/src/templates/patterns/api/idempotent-mutation.ts +17 -40
- package/src/templates/patterns/api/paginated-list.ts +1 -9
- package/src/templates/patterns/api/simple-crud.ts +2 -2
- package/src/templates/patterns/api/system-admin.ts +1 -4
- package/src/templates/patterns/api/webhook-receiver-style.ts +7 -53
- package/src/templates/patterns/subscriber/audit-relay.ts +14 -24
- package/src/templates/patterns/subscriber/cascade-deletion.ts +6 -22
- package/src/templates/patterns/subscriber/single-step-projection.ts +6 -10
- package/src/templates/subscriber-skeleton.ts +1 -1
- package/src/utils/install-file.ts +11 -29
- package/src/utils/manifest.ts +51 -6
- package/src/utils/scaffold-config.ts +11 -0
|
@@ -42,6 +42,9 @@ export async function buildRegistry(domainRoot) {
|
|
|
42
42
|
const domainExports = await load(paths.domain);
|
|
43
43
|
const domainRaw = domainExports.find(e => e['_kind'] === 'domain');
|
|
44
44
|
if (!domainRaw) {
|
|
45
|
+
// Emit any suppressed tsx load errors to stderr before throwing so they appear in CI logs.
|
|
46
|
+
for (const w of warnings)
|
|
47
|
+
process.stderr.write(`[tib validate] ${w}\n`);
|
|
45
48
|
throw new Error(`buildRegistry: no 'domain' export found in ${paths.domain}`);
|
|
46
49
|
}
|
|
47
50
|
const domain = {
|
|
@@ -87,12 +87,15 @@ export async function loadModuleExports(absoluteFilePath) {
|
|
|
87
87
|
return await new Promise((resolve, reject) => {
|
|
88
88
|
const tsxChild = spawn(process.execPath, [tsxCli, tempPath], {
|
|
89
89
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
-
env: { ...process.env },
|
|
91
90
|
});
|
|
92
91
|
let stdout = '';
|
|
93
92
|
let stderr = '';
|
|
94
93
|
tsxChild.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
95
|
-
|
|
94
|
+
// Pipe tsx stderr directly to our process stderr so errors are visible in CI logs.
|
|
95
|
+
tsxChild.stderr.on('data', (chunk) => {
|
|
96
|
+
stderr += chunk.toString();
|
|
97
|
+
process.stderr.write(chunk);
|
|
98
|
+
});
|
|
96
99
|
tsxChild.on('close', (code) => {
|
|
97
100
|
if (code !== 0) {
|
|
98
101
|
reject(new Error(`loadModuleExports: tsx exited ${code} for ${absoluteFilePath}\n${stderr}`));
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1028,7 +1028,7 @@ async function checkLayoutPolicy(projectRoot) {
|
|
|
1028
1028
|
}
|
|
1029
1029
|
// Check 1: owned files in unexpected locations
|
|
1030
1030
|
// Policy: owned files should be in .mc/ OR .github/workflows/tib-*
|
|
1031
|
-
const ownedOutsideTib = manifest.files.filter(f => f.policy === '
|
|
1031
|
+
const ownedOutsideTib = manifest.files.filter(f => f.policy === 'managed' &&
|
|
1032
1032
|
!f.path.startsWith('.mc/') &&
|
|
1033
1033
|
!/^\.github\/workflows\/tib-/.test(f.path));
|
|
1034
1034
|
if (ownedOutsideTib.length === 0) {
|
|
@@ -1050,7 +1050,7 @@ async function checkLayoutPolicy(projectRoot) {
|
|
|
1050
1050
|
}
|
|
1051
1051
|
}
|
|
1052
1052
|
// Check 2: tracked/seed files inside .mc/
|
|
1053
|
-
const nonOwnedInsideTib = manifest.files.filter(f => (f.policy === '
|
|
1053
|
+
const nonOwnedInsideTib = manifest.files.filter(f => (f.policy === 'editable' || f.policy === 'seed') &&
|
|
1054
1054
|
f.path.startsWith('.mc/'));
|
|
1055
1055
|
if (nonOwnedInsideTib.length === 0) {
|
|
1056
1056
|
results.push({
|
|
@@ -1157,14 +1157,14 @@ async function checkLayoutPolicy(projectRoot) {
|
|
|
1157
1157
|
* Old path → new path mapping for known scaffold files
|
|
1158
1158
|
*/
|
|
1159
1159
|
const RELOCATION_MAP = [
|
|
1160
|
-
{ oldPath: 'infra/modules/app.ts', newPath: '.mc/infra/modules/app.ts', policy: '
|
|
1161
|
-
{ oldPath: 'infra/modules/shared/SharedStack.ts', newPath: '.mc/infra/modules/shared/SharedStack.ts', policy: '
|
|
1162
|
-
{ oldPath: 'infra/modules/domains/dispatch-middleware.ts', newPath: '.mc/infra/modules/domains/dispatch-middleware.ts', policy: '
|
|
1163
|
-
{ oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: '
|
|
1164
|
-
{ oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: '
|
|
1165
|
-
{ oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: '
|
|
1166
|
-
{ oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: '
|
|
1167
|
-
{ oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: '
|
|
1160
|
+
{ oldPath: 'infra/modules/app.ts', newPath: '.mc/infra/modules/app.ts', policy: 'managed' },
|
|
1161
|
+
{ oldPath: 'infra/modules/shared/SharedStack.ts', newPath: '.mc/infra/modules/shared/SharedStack.ts', policy: 'managed' },
|
|
1162
|
+
{ oldPath: 'infra/modules/domains/dispatch-middleware.ts', newPath: '.mc/infra/modules/domains/dispatch-middleware.ts', policy: 'managed' },
|
|
1163
|
+
{ oldPath: 'infra/modules/PowerTuningStack.ts', newPath: '.mc/infra/modules/PowerTuningStack.ts', policy: 'managed' },
|
|
1164
|
+
{ oldPath: 'infra/cdk.json', newPath: '.mc/infra/cdk.json', policy: 'managed' },
|
|
1165
|
+
{ oldPath: 'infra/tsconfig.json', newPath: '.mc/infra/tsconfig.json', policy: 'managed' },
|
|
1166
|
+
{ oldPath: 'infra/package.json', newPath: '.mc/infra/package.json', policy: 'managed' },
|
|
1167
|
+
{ oldPath: 'mc-deploy.yml', newPath: '.github/workflows/mc-deploy.yml', policy: 'managed' },
|
|
1168
1168
|
];
|
|
1169
1169
|
/**
|
|
1170
1170
|
* Run relocation of old-layout scaffold files to new layout
|
|
@@ -2,7 +2,6 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { cliLogger } from '../utils/logger.js';
|
|
4
4
|
const TIB_PACKAGES = [
|
|
5
|
-
'@mettlecast/design-system',
|
|
6
5
|
'@mettlecast/dashboard-shell',
|
|
7
6
|
'@mettlecast/sdk',
|
|
8
7
|
'@mettlecast/observability',
|
|
@@ -10,7 +9,6 @@ const TIB_PACKAGES = [
|
|
|
10
9
|
'@mettlecast/tsconfig-react',
|
|
11
10
|
];
|
|
12
11
|
const REQUIRED_PACKAGES = [
|
|
13
|
-
'@mettlecast/design-system',
|
|
14
12
|
'@mettlecast/dashboard-shell',
|
|
15
13
|
'@mettlecast/sdk',
|
|
16
14
|
];
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -369,6 +369,10 @@ export async function runUpgrade(packageSpec, opts) {
|
|
|
369
369
|
}
|
|
370
370
|
// Add to results
|
|
371
371
|
allResults.push({ path: installPath, status: installResult.status, module: mod.id });
|
|
372
|
+
// update-available: do NOT update manifest (file was not written to disk)
|
|
373
|
+
if (installResult.status === 'update-available') {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
372
376
|
// Update manifest if file was not skipped
|
|
373
377
|
const newChecksum = computeChecksumString(newContent);
|
|
374
378
|
upsertManifestFile(updatedManifest, {
|
|
@@ -38,7 +38,7 @@ export const ${varName} = defineApi({
|
|
|
38
38
|
const entityId = crypto.randomUUID();
|
|
39
39
|
|
|
40
40
|
// ── Persist entity ────────────────────────────────────
|
|
41
|
-
|
|
41
|
+
await ctx.store.put(entityId, { id: entityId, name: input.name });
|
|
42
42
|
|
|
43
43
|
// ── Publish domain event ──────────────────────────────
|
|
44
44
|
await ctx.publish(
|
|
@@ -46,16 +46,12 @@ export const ${varName} = defineApi({
|
|
|
46
46
|
{
|
|
47
47
|
id: entityId,
|
|
48
48
|
name: input.name,
|
|
49
|
-
tenantId: ctx.
|
|
49
|
+
tenantId: ctx.tenant.id,
|
|
50
50
|
},
|
|
51
51
|
1,
|
|
52
52
|
);
|
|
53
53
|
|
|
54
|
-
await ctx.
|
|
55
|
-
action: '${id}.create',
|
|
56
|
-
tenantId: ctx.tenantId,
|
|
57
|
-
entityId,
|
|
58
|
-
});
|
|
54
|
+
await ctx.audit.log('${id}.create', entityId, { tenantId: ctx.tenant.id });
|
|
59
55
|
|
|
60
56
|
return {
|
|
61
57
|
id: entityId,
|
|
@@ -11,10 +11,6 @@ export function idempotentMutationTemplate(domain, id, tenancy) {
|
|
|
11
11
|
return `import { z } from 'zod';
|
|
12
12
|
import { defineApi } from '@mettlecast/domain-runtime';
|
|
13
13
|
|
|
14
|
-
// ── Idempotency key header ───────────────────────────────────
|
|
15
|
-
|
|
16
|
-
const IdempotencyKey = z.string().min(1);
|
|
17
|
-
|
|
18
14
|
// ── Zod schemas ──────────────────────────────────────────────
|
|
19
15
|
|
|
20
16
|
const ${varName}Input = z.object({
|
|
@@ -40,56 +36,37 @@ export const ${varName} = defineApi({
|
|
|
40
36
|
input: ${varName}Input,
|
|
41
37
|
output: ${varName}Output,
|
|
42
38
|
handler: async (input, ctx) => {
|
|
43
|
-
// ──
|
|
44
|
-
const
|
|
45
|
-
if (!idempotencyKey) {
|
|
46
|
-
throw new Error('Missing x-idempotency-key header');
|
|
47
|
-
}
|
|
48
|
-
IdempotencyKey.parse(idempotencyKey);
|
|
49
|
-
|
|
50
|
-
// ── Check for duplicate ───────────────────────────────
|
|
51
|
-
const existing = await ctx.db.get({
|
|
52
|
-
key: { pk: 'IDEMPOTENCY', sk: idempotencyKey },
|
|
53
|
-
});
|
|
39
|
+
// ── Check for duplicate (idempotency key = input.id) ──
|
|
40
|
+
const existing = await ctx.store.get('IDEMPOTENT#' + input.id);
|
|
54
41
|
|
|
55
42
|
if (existing) {
|
|
56
|
-
await ctx.
|
|
57
|
-
action: '${id}.idempotent.duplicate',
|
|
58
|
-
tenantId: ctx.tenantId,
|
|
59
|
-
meta: { idempotencyKey },
|
|
60
|
-
});
|
|
43
|
+
await ctx.audit.log('${id}.idempotent.duplicate', input.id);
|
|
61
44
|
return {
|
|
62
|
-
id:
|
|
45
|
+
id: input.id,
|
|
63
46
|
status: 'already-processed',
|
|
64
|
-
idempotencyKey,
|
|
47
|
+
idempotencyKey: input.id,
|
|
65
48
|
};
|
|
66
49
|
}
|
|
67
50
|
|
|
68
51
|
// ── Apply mutation ────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
52
|
+
await ctx.store.put(
|
|
53
|
+
'${id.toUpperCase()}#' + input.id,
|
|
54
|
+
{ id: input.id, payload: input.payload, createdAt: new Date().toISOString() },
|
|
55
|
+
);
|
|
74
56
|
|
|
75
|
-
// ── Record idempotency token
|
|
76
|
-
await ctx.
|
|
77
|
-
|
|
78
|
-
entityId: input.id,
|
|
79
|
-
ttl: Math.floor(Date.now() / 1000) + 86400,
|
|
80
|
-
|
|
57
|
+
// ── Record idempotency token with 24h TTL ─────────────
|
|
58
|
+
await ctx.store.put(
|
|
59
|
+
'IDEMPOTENT#' + input.id,
|
|
60
|
+
{ entityId: input.id },
|
|
61
|
+
{ ttl: Math.floor(Date.now() / 1000) + 86400 },
|
|
62
|
+
);
|
|
81
63
|
|
|
82
|
-
await ctx.
|
|
83
|
-
action: '${id}.idempotent.applied',
|
|
84
|
-
tenantId: ctx.tenantId,
|
|
85
|
-
entityId: input.id,
|
|
86
|
-
meta: { idempotencyKey },
|
|
87
|
-
});
|
|
64
|
+
await ctx.audit.log('${id}.idempotent.applied', input.id, { tenantId: ctx.tenant.id });
|
|
88
65
|
|
|
89
66
|
return {
|
|
90
67
|
id: input.id,
|
|
91
68
|
status: 'applied',
|
|
92
|
-
idempotencyKey,
|
|
69
|
+
idempotencyKey: input.id,
|
|
93
70
|
};
|
|
94
71
|
},
|
|
95
72
|
},
|
|
@@ -44,15 +44,7 @@ export const ${varName} = defineApi({
|
|
|
44
44
|
handler: async (input, ctx) => {
|
|
45
45
|
const { cursor, limit, filter } = input;
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
ctx.metrics?.putMetric('${id}.pageRequest', 1, 'Count');
|
|
49
|
-
ctx.metrics?.putMetric('${id}.pageSize', limit, 'Count');
|
|
50
|
-
|
|
51
|
-
await ctx.auditLog({
|
|
52
|
-
action: '${id}.list',
|
|
53
|
-
tenantId: ctx.tenantId,
|
|
54
|
-
meta: { cursor, limit },
|
|
55
|
-
});
|
|
47
|
+
await ctx.audit.log('${id}.list', ctx.tenant.id, { cursor, limit });
|
|
56
48
|
|
|
57
49
|
// ── paginated data fetch ──────────────────────────────
|
|
58
50
|
const items: Array<{ id: string }> = [];
|
|
@@ -41,7 +41,7 @@ export const ${varName} = defineApi({
|
|
|
41
41
|
output: ${varName}Output,
|
|
42
42
|
handler: async (input, ctx) => {
|
|
43
43
|
// Create / Read / Update / Delete based on method
|
|
44
|
-
await ctx.
|
|
44
|
+
await ctx.audit.log('${id}.v1', ctx.tenant.id, { input });
|
|
45
45
|
return {
|
|
46
46
|
id: input.id ?? crypto.randomUUID(),
|
|
47
47
|
createdAt: new Date().toISOString(),
|
|
@@ -85,7 +85,7 @@ export const ${varName}List = defineApi({
|
|
|
85
85
|
input: ${varName}ListInput,
|
|
86
86
|
output: ${varName}ListOutput,
|
|
87
87
|
handler: async (input, ctx) => {
|
|
88
|
-
await ctx.
|
|
88
|
+
await ctx.audit.log('${id}-list.v1', ctx.tenant.id);
|
|
89
89
|
return { items: [], nextCursor: undefined };
|
|
90
90
|
},
|
|
91
91
|
},
|
|
@@ -45,10 +45,7 @@ export const ${varName} = defineApi({
|
|
|
45
45
|
input: ${varName}Input,
|
|
46
46
|
output: ${varName}Output,
|
|
47
47
|
handler: async (input, ctx) => {
|
|
48
|
-
await ctx.
|
|
49
|
-
action: '${id}.system',
|
|
50
|
-
meta: { command: input.command },
|
|
51
|
-
});
|
|
48
|
+
await ctx.audit.log('${id}.system', 'system', { command: input.command });
|
|
52
49
|
|
|
53
50
|
switch (input.command) {
|
|
54
51
|
case 'status':
|
|
@@ -9,7 +9,6 @@ export function webhookReceiverStyleTemplate(domain, id, tenancy) {
|
|
|
9
9
|
const varName = camelCase(id);
|
|
10
10
|
return `import { z } from 'zod';
|
|
11
11
|
import { defineApi } from '@mettlecast/domain-runtime';
|
|
12
|
-
import crypto from 'crypto';
|
|
13
12
|
|
|
14
13
|
// ── Supported webhook providers ───────────────────────────────
|
|
15
14
|
|
|
@@ -35,40 +34,6 @@ const ${varName}Output = z.object({
|
|
|
35
34
|
provider: ProviderEnum,
|
|
36
35
|
});
|
|
37
36
|
|
|
38
|
-
// ── Signature verification ────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
function verifySignature(
|
|
41
|
-
provider: z.infer<typeof ProviderEnum>,
|
|
42
|
-
payload: string,
|
|
43
|
-
signatureHeader: string,
|
|
44
|
-
secret: string,
|
|
45
|
-
): boolean {
|
|
46
|
-
switch (provider) {
|
|
47
|
-
case 'stripe':
|
|
48
|
-
// Stripe uses HMAC-SHA256
|
|
49
|
-
return crypto.timingSafeEqual(
|
|
50
|
-
Buffer.from(signatureHeader),
|
|
51
|
-
crypto.createHmac('sha256', secret).update(payload).digest(),
|
|
52
|
-
);
|
|
53
|
-
case 'github':
|
|
54
|
-
// GitHub uses HMAC-SHA256 with 'sha256=' prefix
|
|
55
|
-
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
56
|
-
return crypto.timingSafeEqual(
|
|
57
|
-
Buffer.from(signatureHeader),
|
|
58
|
-
Buffer.from(expected),
|
|
59
|
-
);
|
|
60
|
-
case 'slack':
|
|
61
|
-
// Slack uses HMAC-SHA256 with 'v0=' prefix
|
|
62
|
-
const slackExpected = 'v0=' + crypto.createHmac('sha256', secret).update(payload).digest('hex');
|
|
63
|
-
return crypto.timingSafeEqual(
|
|
64
|
-
Buffer.from(signatureHeader),
|
|
65
|
-
Buffer.from(slackExpected),
|
|
66
|
-
);
|
|
67
|
-
default:
|
|
68
|
-
return true; // custom provider — verify externally
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
37
|
// ── API definition ───────────────────────────────────────────
|
|
73
38
|
|
|
74
39
|
export const ${varName} = defineApi({
|
|
@@ -81,19 +46,8 @@ export const ${varName} = defineApi({
|
|
|
81
46
|
input: ${varName}Input,
|
|
82
47
|
output: ${varName}Output,
|
|
83
48
|
handler: async (input, ctx) => {
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
'${domain.toUpperCase()}_WEBHOOK_SECRET',
|
|
87
|
-
{ provider: input.provider },
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
// ── Verify signature ──────────────────────────────────
|
|
91
|
-
const signatureHeader = ctx.headers?.['x-webhook-signature'] ?? '';
|
|
92
|
-
const rawPayload = input.rawBody ?? JSON.stringify(input.payload);
|
|
93
|
-
|
|
94
|
-
if (!verifySignature(input.provider, rawPayload, signatureHeader, secret)) {
|
|
95
|
-
throw new Error('Invalid webhook signature');
|
|
96
|
-
}
|
|
49
|
+
// Signature verification belongs in a Lambda authorizer (x-webhook-signature header
|
|
50
|
+
// is not accessible here). Wire an HttpLambdaAuthorizer in DomainStack for production.
|
|
97
51
|
|
|
98
52
|
const eventId = crypto.randomUUID();
|
|
99
53
|
|
|
@@ -103,13 +57,13 @@ export const ${varName} = defineApi({
|
|
|
103
57
|
provider: input.provider,
|
|
104
58
|
eventType: input.eventType,
|
|
105
59
|
payload: input.payload,
|
|
106
|
-
tenantId: ctx.
|
|
60
|
+
tenantId: ctx.tenant.id,
|
|
107
61
|
}, 1);
|
|
108
62
|
|
|
109
|
-
await ctx.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
63
|
+
await ctx.audit.log('${id}.webhook.received', ctx.tenant.id, {
|
|
64
|
+
provider: input.provider,
|
|
65
|
+
eventType: input.eventType,
|
|
66
|
+
eventId,
|
|
113
67
|
});
|
|
114
68
|
|
|
115
69
|
return {
|
|
@@ -20,31 +20,21 @@ export const ${varName} = defineSubscriber({
|
|
|
20
20
|
event: '${event}',
|
|
21
21
|
semverRange: '^1',
|
|
22
22
|
handler: async (event, ctx) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
sk: 'EVENT#' + event.id,
|
|
26
|
-
eventType: event.type,
|
|
27
|
-
eventVersion: event.version,
|
|
28
|
-
payload: event.data,
|
|
29
|
-
timestamp: event.timestamp,
|
|
30
|
-
source: event.source,
|
|
31
|
-
correlationId: event.correlationId,
|
|
32
|
-
ttl: Math.floor(Date.now() / 1000) + 90 * 86400, // 90-day retention
|
|
33
|
-
};
|
|
23
|
+
const ev = event as { id?: string; type?: string; version?: number; data?: unknown; timestamp?: string };
|
|
24
|
+
const eventId = ev.id ?? crypto.randomUUID();
|
|
34
25
|
|
|
35
|
-
// ── Write
|
|
36
|
-
await ctx.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
ctx.metrics?.putMetric('${id}.audit.relayed', 1, 'Count');
|
|
26
|
+
// ── Write immutable audit record ─────────────────────────
|
|
27
|
+
await ctx.store.put(
|
|
28
|
+
'AUDIT#' + eventId,
|
|
29
|
+
{
|
|
30
|
+
eventId,
|
|
31
|
+
eventType: ev.type,
|
|
32
|
+
eventVersion: ev.version,
|
|
33
|
+
payload: ev.data,
|
|
34
|
+
timestamp: ev.timestamp ?? new Date().toISOString(),
|
|
35
|
+
},
|
|
36
|
+
{ ttl: Math.floor(Date.now() / 1000) + 90 * 86400 }, // 90-day retention
|
|
37
|
+
);
|
|
48
38
|
},
|
|
49
39
|
});
|
|
50
40
|
`;
|
|
@@ -22,38 +22,22 @@ export const ${varName} = defineSubscriber({
|
|
|
22
22
|
event: '${event}',
|
|
23
23
|
semverRange: '^1',
|
|
24
24
|
handler: async (event, ctx) => {
|
|
25
|
-
const { entityId
|
|
25
|
+
const data = (event as { data: { entityId: string; deletedBy?: string } }).data;
|
|
26
26
|
|
|
27
27
|
// ── Find all children ────────────────────────────────────
|
|
28
|
-
const children = await ctx.
|
|
29
|
-
pk: 'TENANT#' + tenantId,
|
|
30
|
-
skPrefix: 'CHILD#' + entityId,
|
|
31
|
-
});
|
|
28
|
+
const { items: children } = await ctx.store.query({ skPrefix: 'CHILD#' + data.entityId });
|
|
32
29
|
|
|
33
30
|
// ── Soft-delete each child ───────────────────────────────
|
|
34
31
|
const now = new Date().toISOString();
|
|
35
32
|
for (const child of children) {
|
|
36
|
-
await ctx.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
deletedAt: now,
|
|
40
|
-
deletedBy: event.data.deletedBy ?? 'system',
|
|
41
|
-
},
|
|
33
|
+
await ctx.store.update(child['sk'] as string, {
|
|
34
|
+
deletedAt: now,
|
|
35
|
+
deletedBy: data.deletedBy ?? 'system',
|
|
42
36
|
});
|
|
43
37
|
}
|
|
44
38
|
|
|
45
39
|
// ── Audit log ────────────────────────────────────────────
|
|
46
|
-
await ctx.
|
|
47
|
-
action: 'cascade.delete',
|
|
48
|
-
tenantId,
|
|
49
|
-
entityId,
|
|
50
|
-
meta: {
|
|
51
|
-
childrenDeleted: children.length,
|
|
52
|
-
triggeredBy: event.id,
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
ctx.metrics?.putMetric('${id}.cascade.deleted', children.length, 'Count');
|
|
40
|
+
await ctx.audit.log('cascade.delete', data.entityId, { childrenDeleted: children.length });
|
|
57
41
|
},
|
|
58
42
|
});
|
|
59
43
|
`;
|
|
@@ -17,19 +17,15 @@ export const ${varName} = defineSubscriber({
|
|
|
17
17
|
event: '${event}',
|
|
18
18
|
semverRange: '^1',
|
|
19
19
|
handler: async (event, ctx) => {
|
|
20
|
-
const { tenantId, ...eventData } = event.data;
|
|
20
|
+
const { tenantId: _tenantId, ...eventData } = (event as { data: Record<string, unknown> }).data;
|
|
21
|
+
const eventId = (event as { id?: string }).id ?? 'unknown';
|
|
21
22
|
|
|
22
23
|
// ── Upsert projection ────────────────────────────────────
|
|
23
24
|
// Each event updates the projection row for fast reads.
|
|
24
|
-
await ctx.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
lastEventId: event.id,
|
|
29
|
-
projectedAt: new Date().toISOString(),
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
ctx.metrics?.putMetric('${id}.projection.applied', 1, 'Count');
|
|
25
|
+
await ctx.store.put(
|
|
26
|
+
'PROJECTION#' + eventId,
|
|
27
|
+
{ ...eventData, lastEventId: eventId, projectedAt: new Date().toISOString() },
|
|
28
|
+
);
|
|
33
29
|
},
|
|
34
30
|
});
|
|
35
31
|
`;
|
|
@@ -16,7 +16,7 @@ export const ${subscriberId.replace(/-([a-z])/g, (_, c) => c.toUpperCase())} = d
|
|
|
16
16
|
event: '${eventId}',
|
|
17
17
|
semverRange: '*',
|
|
18
18
|
handler: async (_event, _ctx) => {
|
|
19
|
-
//
|
|
19
|
+
// receive _event.data (typed payload) and use _ctx.store, _ctx.publish, _ctx.audit
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
22
|
`;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import type { ManifestFileEntry } from './manifest.js';
|
|
2
|
-
export type FilePolicy = '
|
|
2
|
+
export type FilePolicy = 'managed' | 'editable' | 'seed';
|
|
3
3
|
export interface InstallResult {
|
|
4
|
-
status: 'added' | 'updated' | 'conflict' | 'skipped' | 'unchanged';
|
|
4
|
+
status: 'added' | 'updated' | 'conflict' | 'skipped' | 'unchanged' | 'update-available';
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Install a scaffold file according to its policy.
|
|
8
8
|
*
|
|
9
|
-
* -
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* If no currentEntry → write new → 'added'.
|
|
9
|
+
* - managed: always overwrite. Return 'added' if new, 'updated' if existed.
|
|
10
|
+
* - editable: check sha256. If same → 'unchanged'. If different → 'update-available'
|
|
11
|
+
* (does not write .tib-upgrade or overwrite — user opts in via Updates tab).
|
|
13
12
|
* - seed: if file exists → 'skipped'. Else write → 'added'.
|
|
14
13
|
*/
|
|
15
14
|
export declare function installScaffoldFile(absPath: string, content: string, policy: FilePolicy, currentEntry: ManifestFileEntry | undefined, opts?: {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { writeFile, mkdir, access } from 'node:fs/promises';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
-
import { computeChecksumString
|
|
3
|
+
import { computeChecksumString } from './checksum.js';
|
|
4
4
|
/**
|
|
5
5
|
* Install a scaffold file according to its policy.
|
|
6
6
|
*
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* If no currentEntry → write new → 'added'.
|
|
7
|
+
* - managed: always overwrite. Return 'added' if new, 'updated' if existed.
|
|
8
|
+
* - editable: check sha256. If same → 'unchanged'. If different → 'update-available'
|
|
9
|
+
* (does not write .tib-upgrade or overwrite — user opts in via Updates tab).
|
|
11
10
|
* - seed: if file exists → 'skipped'. Else write → 'added'.
|
|
12
11
|
*/
|
|
13
12
|
export async function installScaffoldFile(absPath, content, policy, currentEntry, opts) {
|
|
@@ -23,8 +22,8 @@ export async function installScaffoldFile(absPath, content, policy, currentEntry
|
|
|
23
22
|
return false;
|
|
24
23
|
}
|
|
25
24
|
};
|
|
26
|
-
if (policy === '
|
|
27
|
-
//
|
|
25
|
+
if (policy === 'managed') {
|
|
26
|
+
// managed: always overwrite, no sha256 check
|
|
28
27
|
const exists = await fileExists(absPath);
|
|
29
28
|
if (!dryRun) {
|
|
30
29
|
await mkdir(dirname(absPath), { recursive: true });
|
|
@@ -44,7 +43,7 @@ export async function installScaffoldFile(absPath, content, policy, currentEntry
|
|
|
44
43
|
}
|
|
45
44
|
return { status: 'added' };
|
|
46
45
|
}
|
|
47
|
-
// policy === '
|
|
46
|
+
// policy === 'editable'
|
|
48
47
|
if (!currentEntry) {
|
|
49
48
|
// New file: write it
|
|
50
49
|
if (!dryRun) {
|
|
@@ -58,21 +57,6 @@ export async function installScaffoldFile(absPath, content, policy, currentEntry
|
|
|
58
57
|
// Content unchanged
|
|
59
58
|
return { status: 'unchanged' };
|
|
60
59
|
}
|
|
61
|
-
// Content changed:
|
|
62
|
-
|
|
63
|
-
const drifted = diskChecksum !== null && diskChecksum !== currentEntry.sha256;
|
|
64
|
-
if (drifted) {
|
|
65
|
-
// Drift detected: write .tib-upgrade file, don't touch original
|
|
66
|
-
if (!dryRun) {
|
|
67
|
-
await mkdir(dirname(absPath), { recursive: true });
|
|
68
|
-
await writeFile(`${absPath}.tib-upgrade`, content, 'utf-8');
|
|
69
|
-
}
|
|
70
|
-
return { status: 'conflict' };
|
|
71
|
-
}
|
|
72
|
-
// No drift: safe to overwrite
|
|
73
|
-
if (!dryRun) {
|
|
74
|
-
await mkdir(dirname(absPath), { recursive: true });
|
|
75
|
-
await writeFile(absPath, content, 'utf-8');
|
|
76
|
-
}
|
|
77
|
-
return { status: 'updated' };
|
|
60
|
+
// Content changed: report update available, do NOT modify disk
|
|
61
|
+
return { status: 'update-available' };
|
|
78
62
|
}
|
package/dist/utils/manifest.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type FilePolicy = '
|
|
1
|
+
export type FilePolicy = 'managed' | 'editable' | 'seed';
|
|
2
2
|
export interface ManifestFileEntry {
|
|
3
3
|
path: string;
|
|
4
4
|
module: string;
|
|
@@ -25,4 +25,4 @@ export declare function createManifest(scaffoldVersion: string, projectName: str
|
|
|
25
25
|
export declare function upsertManifestFile(manifest: TibManifest, entry: ManifestFileEntry): void;
|
|
26
26
|
export declare function removeManifestFile(manifest: TibManifest, filePath: string): void;
|
|
27
27
|
export declare function getManifestFile(manifest: TibManifest, filePath: string): ManifestFileEntry | undefined;
|
|
28
|
-
export declare function
|
|
28
|
+
export declare function isScaffoldManaged(manifest: TibManifest, filePath: string): boolean;
|