@objectstack/plugin-webhooks 17.0.0-rc.0 → 17.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +379 -0
- package/dist/index.cjs +31 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +31 -12
- package/dist/index.js.map +1 -1
- package/dist/schema.d.cts +190 -175
- package/dist/schema.d.ts +190 -175
- package/package.json +11 -5
- package/.turbo/turbo-build.log +0 -36
- package/scripts/i18n-extract.config.ts +0 -34
- package/src/auto-enqueuer.test.ts +0 -431
- package/src/auto-enqueuer.ts +0 -383
- package/src/bootstrap-declared-webhooks.test.ts +0 -283
- package/src/bootstrap-declared-webhooks.ts +0 -205
- package/src/index.ts +0 -29
- package/src/schema.ts +0 -21
- package/src/sys-webhook.object.ts +0 -231
- package/src/translations/bundle-ownership.test.ts +0 -41
- package/src/translations/en.objects.generated.ts +0 -101
- package/src/translations/es-ES.objects.generated.ts +0 -101
- package/src/translations/index.ts +0 -23
- package/src/translations/ja-JP.objects.generated.ts +0 -101
- package/src/translations/zh-CN.objects.generated.ts +0 -101
- package/src/webhook-outbox-plugin.ts +0 -305
- package/src/webhook-provenance.ts +0 -86
- package/tsconfig.json +0 -10
- package/tsup.config.ts +0 -14
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* bootstrapDeclaredWebhooks — materialize stack/connector-declared `webhooks`
|
|
5
|
-
* into `sys_webhook` rows so the dispatcher can actually see them (closes #3461).
|
|
6
|
-
*
|
|
7
|
-
* ## The disconnect this closes
|
|
8
|
-
* The spec authoring surface (`WebhookSchema` — `defineStack({ webhooks })`,
|
|
9
|
-
* `@objectstack/spec/automation/webhook`) declares `object` / `isActive`, and
|
|
10
|
-
* is generically decomposed into the ObjectQL registry at boot as metadata
|
|
11
|
-
* type `webhook`. But the runtime dispatcher ({@link AutoEnqueuer}) reads
|
|
12
|
-
* `sys_webhook` DATA rows (`object_name` / `active`), which until now were only
|
|
13
|
-
* ever written by hand through the object's CRUD UI. Nothing bridged the two —
|
|
14
|
-
* so authoring `webhooks:` on a stack produced metadata artifacts that never
|
|
15
|
-
* became dispatchable rows (a silent no-op; ADR-0078). This seeder is that
|
|
16
|
-
* missing ingestion path.
|
|
17
|
-
*
|
|
18
|
-
* ## Shape translation (authoring → runtime row)
|
|
19
|
-
* The spec shape diverges from the runtime column names; we map only at this
|
|
20
|
-
* boundary and stash the full validated envelope in `definition_json` (whence
|
|
21
|
-
* the enqueuer reads headers / secret / timeout):
|
|
22
|
-
* - `object` → `object_name`
|
|
23
|
-
* - `isActive` → `active`
|
|
24
|
-
* - `triggers` / `url` / `method` / `label` / `description` → same-named columns
|
|
25
|
-
* - the entire parsed {@link Webhook} → `definition_json` (JSON string)
|
|
26
|
-
*
|
|
27
|
-
* Each item is validated through `WebhookSchema.parse()` first — this gives the
|
|
28
|
-
* spec schema a real consumer (defaults for `method`/`isActive`/`timeoutMs` get
|
|
29
|
-
* applied) and rejects malformed authoring with a warning instead of crashing
|
|
30
|
-
* boot.
|
|
31
|
-
*
|
|
32
|
-
* ## Seed-not-clobber (mirrors sys_sharing_rule, #2909)
|
|
33
|
-
* `sys_webhook` is admin-editable (`managedBy: 'config'`). Declared webhooks
|
|
34
|
-
* ship with the app/package, so they seed with `managed_by: 'package'`
|
|
35
|
-
* provenance and re-seed on every boot — but a row an admin has created
|
|
36
|
-
* (`managed_by: 'admin'`) or edited (`customized: true`, stamped by
|
|
37
|
-
* {@link bindWebhookProvenanceStamp}) is never overwritten. Most importantly,
|
|
38
|
-
* an admin's `active: false` on a noisy webhook survives redeploys.
|
|
39
|
-
*
|
|
40
|
-
* MUST run before {@link AutoEnqueuer.start} so the enqueuer's first cache
|
|
41
|
-
* refresh already sees the declared rows.
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
import type { IDataEngine } from '@objectstack/spec/contracts';
|
|
45
|
-
import { WebhookSchema, type Webhook } from '@objectstack/spec/automation';
|
|
46
|
-
|
|
47
|
-
/** System write context — the boot seeder is not an admin authoring action. */
|
|
48
|
-
const SYSTEM_CTX = { isSystem: true, positions: [], permissions: [] } as const;
|
|
49
|
-
|
|
50
|
-
interface Logger {
|
|
51
|
-
info?: (msg: string, meta?: unknown) => void;
|
|
52
|
-
warn?: (msg: string, meta?: unknown) => void;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Random id with a stable prefix — mirrors the sharing-rule seeder. */
|
|
56
|
-
function uid(prefix: string): string {
|
|
57
|
-
const g: any = globalThis as any;
|
|
58
|
-
if (g.crypto?.randomUUID) return `${prefix}_${g.crypto.randomUUID()}`;
|
|
59
|
-
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Read declared `webhook` items from the ObjectQL registry (where the manifest
|
|
64
|
-
* decomposition parks `stack.webhooks`), falling back to the metadata service.
|
|
65
|
-
* Items may be wrapped as `{ content }` — unwrap to the raw authoring object.
|
|
66
|
-
*/
|
|
67
|
-
function readDeclared(engine: any, metadataService: any, type: string): any[] {
|
|
68
|
-
try {
|
|
69
|
-
const reg = engine?._registry;
|
|
70
|
-
if (reg?.listItems) {
|
|
71
|
-
const items = (reg.listItems(type) ?? []).map((i: any) => i?.content ?? i).filter(Boolean);
|
|
72
|
-
if (items.length > 0) return items;
|
|
73
|
-
}
|
|
74
|
-
} catch {
|
|
75
|
-
/* fall through to metadata service */
|
|
76
|
-
}
|
|
77
|
-
try {
|
|
78
|
-
const listed = metadataService?.list?.(type);
|
|
79
|
-
const arr = typeof (listed as any)?.then === 'function' ? [] : (listed ?? []);
|
|
80
|
-
return Array.isArray(arr) ? arr.map((i: any) => i?.content ?? i).filter(Boolean) : [];
|
|
81
|
-
} catch {
|
|
82
|
-
return [];
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface BootstrapDeclaredWebhooksResult {
|
|
87
|
-
seeded: number;
|
|
88
|
-
skipped: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Materialize declared webhooks into `sys_webhook`. Idempotent and safe to run
|
|
93
|
-
* on every boot.
|
|
94
|
-
*/
|
|
95
|
-
export async function bootstrapDeclaredWebhooks(
|
|
96
|
-
engine: IDataEngine,
|
|
97
|
-
metadataService: any,
|
|
98
|
-
logger?: Logger,
|
|
99
|
-
subscriptionsObject = 'sys_webhook',
|
|
100
|
-
): Promise<BootstrapDeclaredWebhooksResult> {
|
|
101
|
-
const declared = readDeclared(engine, metadataService, 'webhook');
|
|
102
|
-
if (declared.length === 0) return { seeded: 0, skipped: 0 };
|
|
103
|
-
|
|
104
|
-
const now = new Date().toISOString();
|
|
105
|
-
let seeded = 0;
|
|
106
|
-
let skipped = 0;
|
|
107
|
-
|
|
108
|
-
for (const raw of declared) {
|
|
109
|
-
// Validate + fill defaults through the canonical spec schema. A real
|
|
110
|
-
// consumer at last — a malformed webhook warns and is skipped, never
|
|
111
|
-
// crashing boot.
|
|
112
|
-
let wh: Webhook;
|
|
113
|
-
try {
|
|
114
|
-
wh = WebhookSchema.parse(raw);
|
|
115
|
-
} catch (err: any) {
|
|
116
|
-
logger?.warn?.('[webhook] declared webhook failed validation — skipped', {
|
|
117
|
-
name: (raw as any)?.name,
|
|
118
|
-
error: err?.message ?? String(err),
|
|
119
|
-
});
|
|
120
|
-
skipped += 1;
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const existing = await engine.find(subscriptionsObject, {
|
|
126
|
-
filter: { name: wh.name },
|
|
127
|
-
limit: 1,
|
|
128
|
-
context: SYSTEM_CTX,
|
|
129
|
-
} as any);
|
|
130
|
-
const row: any = Array.isArray(existing) ? existing[0] : undefined;
|
|
131
|
-
|
|
132
|
-
if (row) {
|
|
133
|
-
// Admin owns a same-named row, or has edited this seeded one — never
|
|
134
|
-
// clobber. `active: false` on a noisy webhook must survive redeploys.
|
|
135
|
-
if (row.managed_by === 'admin') {
|
|
136
|
-
logger?.warn?.('[webhook] declared name collides with an admin-authored row — seed skipped', {
|
|
137
|
-
name: wh.name,
|
|
138
|
-
});
|
|
139
|
-
skipped += 1;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (row.customized === true) {
|
|
143
|
-
skipped += 1;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
const patch = {
|
|
147
|
-
id: row.id,
|
|
148
|
-
...mapWebhookToRow(wh),
|
|
149
|
-
// Adopt pristine/legacy (pre-provenance) rows so future boots
|
|
150
|
-
// recognize them as package-managed.
|
|
151
|
-
managed_by: 'package',
|
|
152
|
-
updated_at: now,
|
|
153
|
-
};
|
|
154
|
-
await engine.update(subscriptionsObject, patch, { context: SYSTEM_CTX } as any);
|
|
155
|
-
seeded += 1;
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const newRow = {
|
|
160
|
-
id: uid('whk'),
|
|
161
|
-
...mapWebhookToRow(wh),
|
|
162
|
-
managed_by: 'package',
|
|
163
|
-
customized: false,
|
|
164
|
-
created_at: now,
|
|
165
|
-
updated_at: now,
|
|
166
|
-
};
|
|
167
|
-
await engine.insert(subscriptionsObject, newRow, { context: SYSTEM_CTX } as any);
|
|
168
|
-
seeded += 1;
|
|
169
|
-
} catch (err: any) {
|
|
170
|
-
logger?.warn?.('[webhook] declared webhook seed failed', {
|
|
171
|
-
name: wh.name,
|
|
172
|
-
error: err?.message ?? String(err),
|
|
173
|
-
});
|
|
174
|
-
skipped += 1;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
logger?.info?.('[webhook] declared webhooks materialized into sys_webhook', {
|
|
179
|
-
seeded,
|
|
180
|
-
skipped,
|
|
181
|
-
total: declared.length,
|
|
182
|
-
});
|
|
183
|
-
return { seeded, skipped };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Translate a validated {@link Webhook} into `sys_webhook` column values.
|
|
188
|
-
* `object → object_name`, `isActive → active`; the full envelope is stashed in
|
|
189
|
-
* `definition_json` for the enqueuer's advanced-config read (headers/secret/…).
|
|
190
|
-
*/
|
|
191
|
-
function mapWebhookToRow(wh: Webhook): Record<string, unknown> {
|
|
192
|
-
return {
|
|
193
|
-
name: wh.name,
|
|
194
|
-
label: wh.label ?? wh.name,
|
|
195
|
-
object_name: wh.object ?? null,
|
|
196
|
-
triggers: wh.triggers ?? [],
|
|
197
|
-
url: wh.url,
|
|
198
|
-
// Store lowercase to match the object's Field.select option values
|
|
199
|
-
// (get/post/…); the enqueuer upper-cases before delivery either way.
|
|
200
|
-
method: String(wh.method ?? 'POST').toLowerCase(),
|
|
201
|
-
description: wh.description ?? null,
|
|
202
|
-
active: wh.isActive !== false,
|
|
203
|
-
definition_json: JSON.stringify(wh),
|
|
204
|
-
};
|
|
205
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @objectstack/plugin-webhooks
|
|
5
|
-
*
|
|
6
|
-
* Webhook fan-out on top of the shared outbound-HTTP delivery substrate
|
|
7
|
-
* (ADR-0018 M3). The durable outbox, cluster-coordinated dispatcher, retry /
|
|
8
|
-
* backoff / dead-letter, and retention all live in
|
|
9
|
-
* `@objectstack/service-messaging` (`sys_http_delivery` + `HttpDispatcher`).
|
|
10
|
-
*
|
|
11
|
-
* This package ships only the webhook-specific concerns:
|
|
12
|
-
* - the `sys_webhook` configuration object,
|
|
13
|
-
* - the {@link AutoEnqueuer} that turns `data.record.*` events into outbox
|
|
14
|
-
* rows (`source: 'webhook'`),
|
|
15
|
-
* - the redeliver admin endpoint.
|
|
16
|
-
*
|
|
17
|
-
* **Requires** `MessagingServicePlugin` (a foundational, always-on capability).
|
|
18
|
-
*
|
|
19
|
-
* ## Subpath exports
|
|
20
|
-
* - `@objectstack/plugin-webhooks/schema` — `SysWebhook` object schema.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
export {
|
|
24
|
-
WebhookOutboxPlugin,
|
|
25
|
-
type WebhookOutboxPluginOptions,
|
|
26
|
-
} from './webhook-outbox-plugin.js';
|
|
27
|
-
|
|
28
|
-
export { AutoEnqueuer, type AutoEnqueuerOptions, type HttpEnqueueFn } from './auto-enqueuer.js';
|
|
29
|
-
export { SysWebhook } from './sys-webhook.object.js';
|
package/src/schema.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Public schema subpath: `@objectstack/plugin-webhooks/schema`.
|
|
5
|
-
*
|
|
6
|
-
* Thin re-export barrel kept stable across refactors. The object definition
|
|
7
|
-
* lives in `sys-webhook.object.ts` (matching the `*.object.ts` convention used
|
|
8
|
-
* everywhere else in the monorepo for `sys_*` schemas).
|
|
9
|
-
*
|
|
10
|
-
* `sys_webhook` moved here from `@objectstack/platform-objects` per ADR-0029
|
|
11
|
-
* (K2.a) so this plugin owns its configuration object. Delivery telemetry is no
|
|
12
|
-
* longer a webhook-owned object: post-ADR-0018 M3 deliveries are rows in the
|
|
13
|
-
* shared `sys_http_delivery` outbox owned by `@objectstack/service-messaging`.
|
|
14
|
-
*
|
|
15
|
-
* Note: callers that just need the runtime should import from the package root
|
|
16
|
-
* (`@objectstack/plugin-webhooks`), which auto-registers `sys_webhook` via the
|
|
17
|
-
* plugin manifest. This subpath exists for read-only inspection from a
|
|
18
|
-
* different runtime.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
export { SysWebhook } from './sys-webhook.object.js';
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
import { ObjectSchema, Field } from '@objectstack/spec/data';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* sys_webhook — Outbound HTTP integration configuration (runtime).
|
|
7
|
-
*
|
|
8
|
-
* Persists a single {@link Webhook} envelope per row so administrators
|
|
9
|
-
* can author, enable/disable, and edit webhook subscriptions from the
|
|
10
|
-
* Studio UI without code changes. The canonical Zod schema for the
|
|
11
|
-
* `definition_json` envelope lives at `@objectstack/spec/automation/webhook`.
|
|
12
|
-
*
|
|
13
|
-
* ## Two authoring doors, one row
|
|
14
|
-
* Rows land here two ways, distinguished by the `managed_by` provenance column:
|
|
15
|
-
* - **admin** — created/edited directly through this object's CRUD UI.
|
|
16
|
-
* - **package** — declared in code (`defineStack({ webhooks })` /
|
|
17
|
-
* `defineWebhook()`) and materialized on boot by
|
|
18
|
-
* `bootstrapDeclaredWebhooks` (#3461). Re-seeded every boot, but an admin
|
|
19
|
-
* edit stamps `customized: true` and freezes the row (seed-not-clobber,
|
|
20
|
-
* mirrors `sys_sharing_rule` #2909).
|
|
21
|
-
*
|
|
22
|
-
* One row per `name`. This plugin's {@link AutoEnqueuer} loads active rows on
|
|
23
|
-
* boot + on `sys_webhook:changed` events, and turns matching `data.record.*`
|
|
24
|
-
* events into deliveries on the shared `service-messaging` HTTP outbox
|
|
25
|
-
* (ADR-0018 M3 — `sys_http_delivery`, drained by the messaging dispatcher).
|
|
26
|
-
*
|
|
27
|
-
* Ownership (ADR-0029 K2.a): this object is **owned by
|
|
28
|
-
* `@objectstack/plugin-webhooks`** — the plugin that consumes these rows. It
|
|
29
|
-
* used to live in the `@objectstack/platform-objects` monolith and be imported
|
|
30
|
-
* here; the definition now lives with its owner so the plugin ships both data
|
|
31
|
-
* and behavior as one unit.
|
|
32
|
-
*
|
|
33
|
-
* Platform-wide on purpose: every project (standalone, single-tenant,
|
|
34
|
-
* cloud) can integrate with external systems (Slack, Stripe, internal
|
|
35
|
-
* services) the same way.
|
|
36
|
-
*
|
|
37
|
-
* @namespace sys
|
|
38
|
-
*/
|
|
39
|
-
export const SysWebhook = ObjectSchema.create({
|
|
40
|
-
name: 'sys_webhook',
|
|
41
|
-
label: 'Webhook',
|
|
42
|
-
pluralLabel: 'Webhooks',
|
|
43
|
-
icon: 'webhook',
|
|
44
|
-
isSystem: true,
|
|
45
|
-
managedBy: 'config',
|
|
46
|
-
// Authoring a webhook from the UI requires a structured form for the
|
|
47
|
-
// headers / auth / retry / payload blocks — the generic JSON textarea
|
|
48
|
-
// is acceptable as a v1 until a dedicated builder lands. Re-enable
|
|
49
|
-
// create/edit/delete so admins can at least toggle `active` and edit
|
|
50
|
-
// simple URL/method fields without round-tripping through code.
|
|
51
|
-
userActions: { create: true, edit: true, delete: true, import: false },
|
|
52
|
-
description: 'Outbound HTTP webhook subscription. Declared in code via defineStack({ webhooks }) / defineWebhook() (materialized into rows on boot) or authored directly in the Studio editor; dispatched by the webhook auto-enqueuer onto the shared HTTP outbox.',
|
|
53
|
-
displayNameField: 'name',
|
|
54
|
-
nameField: 'name', // [ADR-0079] canonical primary-title pointer (mirrors deprecated displayNameField)
|
|
55
|
-
titleFormat: '{label}',
|
|
56
|
-
highlightFields: ['name', 'object_name', 'url', 'active', 'updated_at'],
|
|
57
|
-
|
|
58
|
-
listViews: {
|
|
59
|
-
active: {
|
|
60
|
-
type: 'grid',
|
|
61
|
-
name: 'active',
|
|
62
|
-
label: 'Active',
|
|
63
|
-
data: { provider: 'object', object: 'sys_webhook' },
|
|
64
|
-
columns: ['label', 'object_name', 'url', 'method', 'active', 'updated_at'],
|
|
65
|
-
filter: [{ field: 'active', operator: 'equals', value: true }],
|
|
66
|
-
sort: [{ field: 'label', order: 'asc' }],
|
|
67
|
-
pagination: { pageSize: 50 },
|
|
68
|
-
},
|
|
69
|
-
inactive: {
|
|
70
|
-
type: 'grid',
|
|
71
|
-
name: 'inactive',
|
|
72
|
-
label: 'Inactive',
|
|
73
|
-
data: { provider: 'object', object: 'sys_webhook' },
|
|
74
|
-
columns: ['label', 'object_name', 'url', 'method', 'active', 'updated_at'],
|
|
75
|
-
filter: [{ field: 'active', operator: 'equals', value: false }],
|
|
76
|
-
sort: [{ field: 'label', order: 'asc' }],
|
|
77
|
-
pagination: { pageSize: 50 },
|
|
78
|
-
},
|
|
79
|
-
by_object: {
|
|
80
|
-
type: 'grid',
|
|
81
|
-
name: 'by_object',
|
|
82
|
-
label: 'By Object',
|
|
83
|
-
data: { provider: 'object', object: 'sys_webhook' },
|
|
84
|
-
columns: ['object_name', 'label', 'url', 'active', 'updated_at'],
|
|
85
|
-
sort: [{ field: 'object_name', order: 'asc' }, { field: 'label', order: 'asc' }],
|
|
86
|
-
grouping: { fields: [{ field: 'object_name', order: 'asc', collapsed: false }] },
|
|
87
|
-
pagination: { pageSize: 100 },
|
|
88
|
-
},
|
|
89
|
-
all_webhooks: {
|
|
90
|
-
type: 'grid',
|
|
91
|
-
name: 'all_webhooks',
|
|
92
|
-
label: 'All',
|
|
93
|
-
data: { provider: 'object', object: 'sys_webhook' },
|
|
94
|
-
columns: ['label', 'object_name', 'url', 'method', 'active', 'updated_at'],
|
|
95
|
-
sort: [{ field: 'label', order: 'asc' }],
|
|
96
|
-
pagination: { pageSize: 50 },
|
|
97
|
-
},
|
|
98
|
-
},
|
|
99
|
-
|
|
100
|
-
fields: {
|
|
101
|
-
id: Field.text({ label: 'Webhook ID', required: true, readonly: true, group: 'System' }),
|
|
102
|
-
|
|
103
|
-
name: Field.text({
|
|
104
|
-
label: 'Name',
|
|
105
|
-
required: true,
|
|
106
|
-
maxLength: 100,
|
|
107
|
-
description: 'Unique snake_case name — referenced in logs and audit',
|
|
108
|
-
group: 'Definition',
|
|
109
|
-
}),
|
|
110
|
-
|
|
111
|
-
label: Field.text({
|
|
112
|
-
label: 'Display Label',
|
|
113
|
-
required: false,
|
|
114
|
-
maxLength: 200,
|
|
115
|
-
group: 'Definition',
|
|
116
|
-
}),
|
|
117
|
-
|
|
118
|
-
object_name: Field.text({
|
|
119
|
-
label: 'Object',
|
|
120
|
-
required: false,
|
|
121
|
-
maxLength: 100,
|
|
122
|
-
// Object picker (same widget as sys_sharing_rule) instead of a free-text
|
|
123
|
-
// machine name. Falls back to a text input when the widget is unavailable.
|
|
124
|
-
widget: 'object-ref',
|
|
125
|
-
description: 'Short object name whose record events (create/update/delete) fire this webhook',
|
|
126
|
-
group: 'Definition',
|
|
127
|
-
}),
|
|
128
|
-
|
|
129
|
-
triggers: Field.select(
|
|
130
|
-
['create', 'update', 'delete'],
|
|
131
|
-
{
|
|
132
|
-
label: 'Triggers',
|
|
133
|
-
required: false,
|
|
134
|
-
// Multi-select instead of a hand-typed comma-separated string. Stored as
|
|
135
|
-
// an array; the auto-enqueuer parser also tolerates the legacy
|
|
136
|
-
// comma-separated / JSON-string forms so existing rows keep working.
|
|
137
|
-
multiple: true,
|
|
138
|
-
description: 'Record events that fire this webhook',
|
|
139
|
-
group: 'Definition',
|
|
140
|
-
},
|
|
141
|
-
),
|
|
142
|
-
|
|
143
|
-
url: Field.text({
|
|
144
|
-
label: 'Target URL',
|
|
145
|
-
required: true,
|
|
146
|
-
maxLength: 2048,
|
|
147
|
-
description: 'External endpoint that receives the POST',
|
|
148
|
-
group: 'Definition',
|
|
149
|
-
}),
|
|
150
|
-
|
|
151
|
-
method: Field.select(
|
|
152
|
-
['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
153
|
-
{
|
|
154
|
-
label: 'HTTP Method',
|
|
155
|
-
required: true,
|
|
156
|
-
// Select instead of free text. Option values are lowercased by the
|
|
157
|
-
// Field.select helper (get/post/…); the auto-enqueuer upper-cases the
|
|
158
|
-
// resolved method before delivery, so existing 'POST' rows and the
|
|
159
|
-
// lowercase option values both normalise correctly.
|
|
160
|
-
defaultValue: 'post',
|
|
161
|
-
description: 'HTTP method used for the callback request',
|
|
162
|
-
group: 'Definition',
|
|
163
|
-
},
|
|
164
|
-
),
|
|
165
|
-
|
|
166
|
-
description: Field.textarea({ label: 'Description', required: false, group: 'Definition' }),
|
|
167
|
-
|
|
168
|
-
active: Field.boolean({
|
|
169
|
-
label: 'Active',
|
|
170
|
-
required: true,
|
|
171
|
-
defaultValue: true,
|
|
172
|
-
description: 'Inactive webhooks are skipped by the dispatcher',
|
|
173
|
-
group: 'Definition',
|
|
174
|
-
}),
|
|
175
|
-
|
|
176
|
-
definition_json: Field.textarea({
|
|
177
|
-
label: 'Definition',
|
|
178
|
-
required: true,
|
|
179
|
-
description: 'Serialised Webhook JSON (see @objectstack/spec/automation/webhook) — full headers/auth/retry/payload config',
|
|
180
|
-
group: 'Definition',
|
|
181
|
-
}),
|
|
182
|
-
|
|
183
|
-
// ── Provenance (#3461 — record-authoritative seed-not-clobber) ──
|
|
184
|
-
// Mirrors sys_sharing_rule (#2909). Both columns are `readonly`: the
|
|
185
|
-
// engine strips them from non-system payloads (forge/clear-proof), while
|
|
186
|
-
// bootstrapDeclaredWebhooks and the provenance stamp hook write with
|
|
187
|
-
// isSystem. Deliberately NOT a write gate: webhooks are a first-class admin
|
|
188
|
-
// authoring/tuning surface — admins may edit or deactivate a package row;
|
|
189
|
-
// the seeder simply stops overwriting it once `customized` is stamped.
|
|
190
|
-
managed_by: Field.select(
|
|
191
|
-
['platform', 'package', 'admin'],
|
|
192
|
-
{
|
|
193
|
-
label: 'Managed By',
|
|
194
|
-
required: false,
|
|
195
|
-
readonly: true,
|
|
196
|
-
defaultValue: 'admin',
|
|
197
|
-
description:
|
|
198
|
-
'Record provenance: platform = framework built-in / package = app/package-declared ' +
|
|
199
|
-
'(boot-seeded from defineStack webhooks) / admin = created in Setup.',
|
|
200
|
-
group: 'System',
|
|
201
|
-
},
|
|
202
|
-
),
|
|
203
|
-
|
|
204
|
-
customized: Field.boolean({
|
|
205
|
-
label: 'Customized',
|
|
206
|
-
required: false,
|
|
207
|
-
readonly: true,
|
|
208
|
-
defaultValue: false,
|
|
209
|
-
description:
|
|
210
|
-
'Set when an admin edits a package-declared webhook; boot seeding will no longer ' +
|
|
211
|
-
'overwrite the row (a deactivated noisy webhook survives redeploys). Meaningless on admin rows.',
|
|
212
|
-
group: 'System',
|
|
213
|
-
}),
|
|
214
|
-
|
|
215
|
-
created_at: Field.datetime({
|
|
216
|
-
label: 'Created At',
|
|
217
|
-
required: true,
|
|
218
|
-
defaultValue: 'NOW()',
|
|
219
|
-
readonly: true,
|
|
220
|
-
group: 'System',
|
|
221
|
-
}),
|
|
222
|
-
|
|
223
|
-
updated_at: Field.datetime({ label: 'Updated At', required: false, group: 'System' }),
|
|
224
|
-
},
|
|
225
|
-
|
|
226
|
-
indexes: [
|
|
227
|
-
{ fields: ['name'], unique: true },
|
|
228
|
-
{ fields: ['object_name'] },
|
|
229
|
-
{ fields: ['active', 'object_name'] },
|
|
230
|
-
],
|
|
231
|
-
});
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
//
|
|
3
|
-
// Bundle-ownership guard (#2834 ⑤ / ADR-0029 D8): this package's generated
|
|
4
|
-
// object-translation bundles must carry ONLY objects its extract config
|
|
5
|
-
// (`scripts/i18n-extract.config.ts`) actually imports. When an object moves to
|
|
6
|
-
// another package, its translations move with it — a leftover copy here
|
|
7
|
-
// silently DIES on the next `os i18n extract` run, taking curated translations
|
|
8
|
-
// with it (the sys_audit_log incident). This test turns that silent loss into a
|
|
9
|
-
// red build: an object present in the bundle but not in the ownership list below
|
|
10
|
-
// means either (a) the extract config gained an object — add it here — or (b) a
|
|
11
|
-
// moved/removed object's keys were left behind — remove them from the bundles
|
|
12
|
-
// (or migrate them to the owning package), then keep this list in sync.
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect } from 'vitest';
|
|
15
|
-
import { enObjects } from './en.objects.generated.js';
|
|
16
|
-
|
|
17
|
-
// Objects the extract config (scripts/i18n-extract.config.ts) imports —
|
|
18
|
-
// keep the two lists in sync when adding/moving objects.
|
|
19
|
-
const OWNED_OBJECTS = new Set([
|
|
20
|
-
'sys_webhook',
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
describe('objects translation bundle ownership (ADR-0029 D8)', () => {
|
|
24
|
-
it('the en bundle contains no objects owned by other packages', () => {
|
|
25
|
-
const strays = Object.keys(enObjects).filter((o) => !OWNED_OBJECTS.has(o));
|
|
26
|
-
expect(
|
|
27
|
-
strays,
|
|
28
|
-
`bundle carries objects this package's extract config does not own: ${strays.join(', ')} — ` +
|
|
29
|
-
'their curated translations would be silently deleted on the next `os i18n extract`. ' +
|
|
30
|
-
'Remove the dead block from the four locale bundles, or add the object to the extract config + this list.',
|
|
31
|
-
).toEqual([]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('every owned object is present in the bundle (extract config regression)', () => {
|
|
35
|
-
const missing = [...OWNED_OBJECTS].filter((o) => !(o in enObjects));
|
|
36
|
-
expect(
|
|
37
|
-
missing,
|
|
38
|
-
`objects the extract config should emit are missing from the bundle: ${missing.join(', ')} — was an import dropped from scripts/i18n-extract.config.ts?`,
|
|
39
|
-
).toEqual([]);
|
|
40
|
-
});
|
|
41
|
-
});
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Auto-generated by 'os i18n extract' for locale 'en'.
|
|
5
|
-
* Edit translations in place; re-run extract (with --merge) to fill new gaps.
|
|
6
|
-
* Do not hand-edit the structure — only the leaf string values.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { TranslationData } from '@objectstack/spec/system';
|
|
10
|
-
|
|
11
|
-
export const enObjects: NonNullable<TranslationData['objects']> = {
|
|
12
|
-
sys_webhook: {
|
|
13
|
-
label: "Webhook",
|
|
14
|
-
pluralLabel: "Webhooks",
|
|
15
|
-
description: "Outbound HTTP webhook subscription. Authored via defineWebhook() in code or the Studio editor; executed by the HTTP connector plugin.",
|
|
16
|
-
fields: {
|
|
17
|
-
id: {
|
|
18
|
-
label: "Webhook ID"
|
|
19
|
-
},
|
|
20
|
-
name: {
|
|
21
|
-
label: "Name",
|
|
22
|
-
help: "Unique snake_case name — referenced in logs and audit"
|
|
23
|
-
},
|
|
24
|
-
label: {
|
|
25
|
-
label: "Display Label"
|
|
26
|
-
},
|
|
27
|
-
object_name: {
|
|
28
|
-
label: "Object",
|
|
29
|
-
help: "Short object name whose record events (create/update/delete) fire this webhook"
|
|
30
|
-
},
|
|
31
|
-
triggers: {
|
|
32
|
-
label: "Triggers",
|
|
33
|
-
help: "Comma-separated event list: create,update,delete",
|
|
34
|
-
options: {
|
|
35
|
-
create: "create",
|
|
36
|
-
update: "update",
|
|
37
|
-
delete: "delete"
|
|
38
|
-
}
|
|
39
|
-
},
|
|
40
|
-
url: {
|
|
41
|
-
label: "Target URL",
|
|
42
|
-
help: "External endpoint that receives the POST"
|
|
43
|
-
},
|
|
44
|
-
method: {
|
|
45
|
-
label: "HTTP Method",
|
|
46
|
-
help: "GET / POST / PUT / PATCH / DELETE",
|
|
47
|
-
options: {
|
|
48
|
-
get: "GET",
|
|
49
|
-
post: "POST",
|
|
50
|
-
put: "PUT",
|
|
51
|
-
patch: "PATCH",
|
|
52
|
-
delete: "DELETE"
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
description: {
|
|
56
|
-
label: "Description"
|
|
57
|
-
},
|
|
58
|
-
active: {
|
|
59
|
-
label: "Active",
|
|
60
|
-
help: "Inactive webhooks are skipped by the dispatcher"
|
|
61
|
-
},
|
|
62
|
-
definition_json: {
|
|
63
|
-
label: "Definition",
|
|
64
|
-
help: "Serialised Webhook JSON (see @objectstack/spec/automation/webhook) — full headers/auth/retry/payload config"
|
|
65
|
-
},
|
|
66
|
-
managed_by: {
|
|
67
|
-
label: "Managed By",
|
|
68
|
-
help: "Record provenance: platform = framework built-in / package = app/package-declared (boot-seeded from defineStack webhooks) / admin = created in Setup.",
|
|
69
|
-
options: {
|
|
70
|
-
platform: "platform",
|
|
71
|
-
package: "package",
|
|
72
|
-
admin: "admin"
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
customized: {
|
|
76
|
-
label: "Customized",
|
|
77
|
-
help: "Set when an admin edits a package-declared webhook; boot seeding will no longer overwrite the row (a deactivated noisy webhook survives redeploys). Meaningless on admin rows."
|
|
78
|
-
},
|
|
79
|
-
created_at: {
|
|
80
|
-
label: "Created At"
|
|
81
|
-
},
|
|
82
|
-
updated_at: {
|
|
83
|
-
label: "Updated At"
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
_views: {
|
|
87
|
-
active: {
|
|
88
|
-
label: "Active"
|
|
89
|
-
},
|
|
90
|
-
inactive: {
|
|
91
|
-
label: "Inactive"
|
|
92
|
-
},
|
|
93
|
-
by_object: {
|
|
94
|
-
label: "By Object"
|
|
95
|
-
},
|
|
96
|
-
all_webhooks: {
|
|
97
|
-
label: "All"
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
};
|