skedyul 1.6.0 → 1.7.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/dist/cli/index.js CHANGED
@@ -3870,6 +3870,72 @@ async function handleProvision(body, hooks) {
3870
3870
  }
3871
3871
  }
3872
3872
 
3873
+ // src/server/handlers/setup-revalidate-handler.ts
3874
+ async function handleSetupRevalidate(body, hooks) {
3875
+ const revalidateHook = hooks?.setup?.revalidate;
3876
+ if (!revalidateHook) {
3877
+ return {
3878
+ status: 404,
3879
+ body: { error: "Setup revalidate handler not configured" }
3880
+ };
3881
+ }
3882
+ if (!body.context?.appInstallationId || !body.context?.workplace || !body.context?.app || !body.reason || !Array.isArray(body.steps)) {
3883
+ return {
3884
+ status: 400,
3885
+ body: {
3886
+ error: {
3887
+ code: -32602,
3888
+ message: "Missing context (appInstallationId, workplace, app, reason, and steps required)"
3889
+ }
3890
+ }
3891
+ };
3892
+ }
3893
+ const ctx = {
3894
+ env: body.env ?? {},
3895
+ workplace: body.context.workplace,
3896
+ appInstallationId: body.context.appInstallationId,
3897
+ app: body.context.app,
3898
+ reason: body.reason,
3899
+ steps: body.steps,
3900
+ crm: body.crm,
3901
+ envChange: body.envChange,
3902
+ invocation: body.invocation,
3903
+ log: createContextLogger()
3904
+ };
3905
+ const requestConfig = {
3906
+ baseUrl: body.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
3907
+ apiToken: body.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
3908
+ };
3909
+ try {
3910
+ const handler = typeof revalidateHook === "function" ? revalidateHook : revalidateHook.handler;
3911
+ const result = await runWithLogContext(
3912
+ { invocation: body.invocation },
3913
+ async () => {
3914
+ return await runWithConfig(requestConfig, async () => {
3915
+ return await handler(ctx);
3916
+ });
3917
+ }
3918
+ );
3919
+ return {
3920
+ status: 200,
3921
+ body: {
3922
+ completed: result?.completed ?? [],
3923
+ invalidated: result?.invalidated ?? []
3924
+ }
3925
+ };
3926
+ } catch (err) {
3927
+ return {
3928
+ status: 500,
3929
+ body: {
3930
+ error: {
3931
+ code: -32603,
3932
+ message: err instanceof Error ? err.message : String(err ?? "")
3933
+ }
3934
+ }
3935
+ };
3936
+ }
3937
+ }
3938
+
3873
3939
  // src/server/handler-helpers.ts
3874
3940
  function parseHandlerEnvelope(parsedBody) {
3875
3941
  if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody) || !("env" in parsedBody) || !("request" in parsedBody)) {
@@ -4261,6 +4327,14 @@ async function handleProvisionRoute(req, ctx) {
4261
4327
  const result = await handleProvision(parseResult.data, ctx.config.hooks);
4262
4328
  return { status: result.status, body: result.body };
4263
4329
  }
4330
+ async function handleSetupRevalidateRoute(req, ctx) {
4331
+ const parseResult = parseJsonBody(req);
4332
+ if (!parseResult.success) {
4333
+ return parseResult.error;
4334
+ }
4335
+ const result = await handleSetupRevalidate(parseResult.data, ctx.config.hooks);
4336
+ return { status: result.status, body: result.body };
4337
+ }
4264
4338
  async function handleOAuthCallbackRoute(req, ctx) {
4265
4339
  const parseResult = parseJsonBody(req);
4266
4340
  if (!parseResult.success) {
@@ -4622,6 +4696,9 @@ async function routeRequest(req, ctx) {
4622
4696
  if (req.path === "/provision" && req.method === "POST") {
4623
4697
  return handleProvisionRoute(req, ctx);
4624
4698
  }
4699
+ if (req.path === "/setup/revalidate" && req.method === "POST") {
4700
+ return handleSetupRevalidateRoute(req, ctx);
4701
+ }
4625
4702
  if (req.path === "/oauth_callback" && req.method === "POST") {
4626
4703
  return handleOAuthCallbackRoute(req, ctx);
4627
4704
  }
@@ -6,7 +6,7 @@
6
6
  import type { ToolRegistry, WebhookRegistry, ToolMetadata, WebhookMetadata } from '../types';
7
7
  import type { ServerHooks } from '../types/handlers';
8
8
  import type { CoreApiConfig } from '../core/types';
9
- import type { EnvSchema, ComputeLayer, ModelDefinition, RelationshipDefinition, ChannelDefinition, WorkflowDefinition, PageDefinition, NavigationConfig, AgentDefinition, SignalDefinition } from './types';
9
+ import type { EnvSchema, ComputeLayer, ModelDefinition, EntityDefinition, RelationshipDefinition, ChannelDefinition, WorkflowDefinition, PageDefinition, NavigationConfig, AgentDefinition, SignalDefinition, SetupStepDefinition } from './types';
10
10
  import type { AppEventDefinition } from './types/app-event';
11
11
  import type { QueueRegistry, SerializableQueueConfig } from './queue-config';
12
12
  import type { SequencerRegistry, SerializableSequencerConfig } from './sequencer-config';
@@ -33,6 +33,11 @@ export interface ProvisionConfig {
33
33
  env?: EnvSchema;
34
34
  /** INTERNAL model definitions (app-owned, not visible to users) */
35
35
  models?: ModelDefinition[];
36
+ /**
37
+ * App entity contracts for install-time CRM maps (not shared models).
38
+ * Workplaces map these to CRM; workflows apply maps via app-handle Liquid filters.
39
+ */
40
+ entities?: EntityDefinition[];
36
41
  /** Relationship definitions between INTERNAL models */
37
42
  relationships?: RelationshipDefinition[];
38
43
  /** Communication channel definitions */
@@ -45,6 +50,11 @@ export interface ProvisionConfig {
45
50
  pages?: PageDefinition[];
46
51
  /** App signal definitions (event bus subscriptions created on install) */
47
52
  signals?: SignalDefinition[];
53
+ /**
54
+ * Install setup checklist steps (opt-in).
55
+ * Seeded as AppInstallSetupStep rows; exposed via setup.* Core API.
56
+ */
57
+ setup?: SetupStepDefinition[];
48
58
  }
49
59
  /**
50
60
  * Build configuration - controls how the integration is bundled.
@@ -16,12 +16,14 @@
16
16
  * })
17
17
  */
18
18
  import type { ModelDefinition } from './types/model';
19
+ import type { EntityDefinition } from './types/entity';
19
20
  import type { ChannelDefinition } from './types/channel';
20
21
  import type { PageDefinition } from './types/page';
21
22
  import type { WorkflowDefinition } from './types/workflow';
22
23
  import type { AgentDefinition } from './types/agent';
23
24
  import type { EnvSchema } from './types/env';
24
25
  import type { NavigationConfig } from './types/navigation';
26
+ import type { SetupStepDefinition } from './types/setup';
25
27
  /**
26
28
  * Define a model with full type safety.
27
29
  *
@@ -37,6 +39,28 @@ import type { NavigationConfig } from './types/navigation';
37
39
  * })
38
40
  */
39
41
  export declare function defineModel(model: ModelDefinition): ModelDefinition;
42
+ /**
43
+ * Define an app entity (CRM map contract) with full type safety.
44
+ *
45
+ * Entities describe external payload shapes that workplaces map to CRM at
46
+ * install time. They are not shared models — workflows apply maps via Liquid
47
+ * filters named after the app handle (e.g. `| bft: "format", "member"`).
48
+ *
49
+ * @example
50
+ * export default defineEntity({
51
+ * handle: 'member',
52
+ * label: 'Glofox Member',
53
+ * fields: [
54
+ * { handle: 'glofox_id', label: 'Glofox ID', matchCandidate: true, required: true },
55
+ * { handle: 'first_name', label: 'First Name' },
56
+ * ],
57
+ * contextFields: [
58
+ * { handle: 'membership_status', label: 'Membership Status',
59
+ * options: ['prospect', 'trial_member', 'member', 'inactive_member'] },
60
+ * ],
61
+ * })
62
+ */
63
+ export declare function defineEntity(entity: EntityDefinition): EntityDefinition;
40
64
  /**
41
65
  * Define a channel with full type safety.
42
66
  *
@@ -127,3 +151,18 @@ export declare function defineEnv(env: EnvSchema): EnvSchema;
127
151
  * })
128
152
  */
129
153
  export declare function defineNavigation(navigation: NavigationConfig): NavigationConfig;
154
+ /**
155
+ * Define an install setup step with full type safety.
156
+ *
157
+ * @example
158
+ * export default defineSetupStep({
159
+ * handle: 'crm_members_bookings_events',
160
+ * label: 'Setup Members, Bookings and Events',
161
+ * kind: 'crm',
162
+ * requires: ['access_granted'],
163
+ * entities: ['member', 'booking', 'event'],
164
+ * listenToCrm: true,
165
+ * capabilities: ['crm.member', 'crm.booking', 'crm.event'],
166
+ * })
167
+ */
168
+ export declare function defineSetupStep(step: SetupStepDefinition): SetupStepDefinition;
@@ -11,7 +11,7 @@
11
11
  export * from './types';
12
12
  export type { InstallConfig, ProvisionConfig, BuildConfig, CorsOptions, SkedyulConfig, SerializableSkedyulConfig, QueueScope, QueueConfig, SerializableQueueConfig, QueueRegistry, SequencerScope, SerializableSequencerConfig, SequencerRegistry, } from './app-config';
13
13
  export { defineConfig } from './app-config';
14
- export { defineModel, defineChannel, definePage, defineWorkflow, defineAgent, defineEnv, defineNavigation, } from './define';
14
+ export { defineModel, defineEntity, defineChannel, definePage, defineWorkflow, defineAgent, defineEnv, defineNavigation, defineSetupStep, } from './define';
15
15
  export { SCHEMA_FILE_EXTENSIONS, loadSchema, saveSchema, serializeSchemaToJson, serializeSchemaToTypeScript, transformToBackendSchema, transformFromBackendSchema, type LoadSchemaOptions, type LoadSchemaResult, type SerializeSchemaOptions, type BackendModelDefinition, type BackendFieldDefinition, type BackendRelationshipDefinition, type BackendDesiredSchema, } from './schema-loader';
16
16
  export { loadAndResolveConfig, serializeResolvedConfig, type ResolvedConfig } from './resolver';
17
17
  export { getAllEnvKeys, getRequiredInstallEnvKeys } from './utils';
@@ -0,0 +1,67 @@
1
+ /**
2
+ * App entity definition types.
3
+ *
4
+ * Entities describe external/logical payload shapes (e.g. Glofox member) that
5
+ * workplaces map to CRM via install-time CRM maps. They are NOT shared models —
6
+ * the app does not own CRM writes; workflows/agents apply maps.
7
+ */
8
+ import type { BaseDefinition, FieldOption } from './base';
9
+ /**
10
+ * Field on an app entity contract (maps to a path on tool/event payloads).
11
+ */
12
+ export interface EntityFieldDefinition {
13
+ /** Unique field handle (snake_case), used as source path by default */
14
+ handle: string;
15
+ /** Human-readable label */
16
+ label: string;
17
+ /** Optional description */
18
+ description?: string;
19
+ /** Data type hint for the install mapper UI */
20
+ type?: 'string' | 'long_string' | 'number' | 'boolean' | 'date' | 'datetime' | 'object';
21
+ /**
22
+ * Whether this field uniquely identifies a record and can be used as an
23
+ * upsert match key (e.g. external id, email, phone).
24
+ */
25
+ isUnique?: boolean;
26
+ /**
27
+ * @deprecated Prefer `isUnique`. Kept for backward compatibility —
28
+ * treated the same as `isUnique` by the CRM map editor.
29
+ */
30
+ matchCandidate?: boolean;
31
+ /** Whether this field should be mapped for a complete configuration */
32
+ required?: boolean;
33
+ /** Enum options when the source value is constrained */
34
+ options?: FieldOption[] | string[];
35
+ }
36
+ /**
37
+ * Relationship declared on an entity (for install relationship mapping UI).
38
+ */
39
+ export interface EntityRelationshipDefinition {
40
+ /** Handle used when writing relationship ids (e.g. "customer", "plan") */
41
+ handle: string;
42
+ /** Human-readable label */
43
+ label: string;
44
+ /** Target entity handle this relates to (optional documentation) */
45
+ targetEntity?: string;
46
+ description?: string;
47
+ }
48
+ /**
49
+ * App entity — mapping contract for install CRM maps + Liquid filters.
50
+ */
51
+ export interface EntityDefinition extends BaseDefinition {
52
+ /** Unique entity handle (e.g. "member", "booking") */
53
+ handle: string;
54
+ /** Display label */
55
+ label: string;
56
+ labelPlural?: string;
57
+ description?: string;
58
+ /** Fields on the primary entity object */
59
+ fields: EntityFieldDefinition[];
60
+ /**
61
+ * Fields that live on the event/tool envelope alongside the entity
62
+ * (e.g. membership_status on member events).
63
+ */
64
+ contextFields?: EntityFieldDefinition[];
65
+ /** Relationship handles that can be mapped to CRM relationship fields */
66
+ relationships?: EntityRelationshipDefinition[];
67
+ }
@@ -352,15 +352,7 @@ export interface ListBlock {
352
352
  /** Message when no items */
353
353
  emptyMessage?: string;
354
354
  }
355
- /**
356
- * Model mapper block definition - for mapping SHARED models to workspace models.
357
- */
358
- export interface ModelMapperBlock {
359
- type: 'model_mapper';
360
- /** The SHARED model handle from provision config */
361
- model: string;
362
- }
363
355
  /**
364
356
  * Union of all block types.
365
357
  */
366
- export type BlockDefinition = CardBlock | ListBlock | ModelMapperBlock;
358
+ export type BlockDefinition = CardBlock | ListBlock;
@@ -7,11 +7,13 @@ export * from './base';
7
7
  export * from './env';
8
8
  export * from './resource';
9
9
  export * from './model';
10
+ export * from './entity';
10
11
  export * from './channel';
11
12
  export * from './workflow';
12
13
  export * from './agent';
13
14
  export * from './webhook';
14
15
  export * from './signal';
16
+ export * from './setup';
15
17
  export * from './app-event';
16
18
  export * from './navigation';
17
19
  export * from './context';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * App install setup step definition for provision config.
3
+ *
4
+ * Declared steps are seeded onto each AppInstallation as AppInstallSetupStep
5
+ * rows. Apps and the platform UI read/update status via the setup.* Core API.
6
+ */
7
+ export type SetupStepKind = 'app' | 'crm' | 'realtime' | 'env';
8
+ export interface SetupStepDefinition {
9
+ /** Stable step handle (unique within the app) */
10
+ handle: string;
11
+ /** UI label */
12
+ label: string;
13
+ /** Optional longer description */
14
+ description?: string;
15
+ /** Step kind — drives default CTAs and listeners */
16
+ kind: SetupStepKind;
17
+ /** Prerequisite step handles that must be READY */
18
+ requires?: string[];
19
+ /** CRM entity handles this step covers (kind: crm) */
20
+ entities?: string[];
21
+ /** Bundled workflow handles this step wires (kind: realtime) */
22
+ workflowHandles?: string[];
23
+ /** Env keys this step depends on (kind: env) */
24
+ envKeys?: string[];
25
+ /**
26
+ * When true, platform emits install.setup.crm_changed after CRM migrations
27
+ * and invokes hooks.setup.revalidate so the app can complete/invalidate.
28
+ */
29
+ listenToCrm?: boolean;
30
+ /**
31
+ * When true, platform emits install.setup.env_changed when matching env
32
+ * keys are removed/cleared and invokes hooks.setup.revalidate.
33
+ */
34
+ listenToEnv?: boolean;
35
+ /** Capability keys unlocked when this step is READY */
36
+ capabilities?: string[];
37
+ }
@@ -1241,4 +1241,43 @@ export declare const report: {
1241
1241
  */
1242
1242
  get(handle: string): Promise<ReportDefinition | null>;
1243
1243
  };
1244
+ export type SetupStepStatus = 'PENDING' | 'READY' | 'SKIPPED' | 'INVALIDATED' | 'BLOCKED';
1245
+ export type SetupStepKindApi = 'APP' | 'CRM' | 'REALTIME' | 'ENV';
1246
+ export type SetupStepView = {
1247
+ id: string;
1248
+ handle: string;
1249
+ kind: SetupStepKindApi;
1250
+ status: SetupStepStatus;
1251
+ sortOrder: number;
1252
+ label: string;
1253
+ description: string | null;
1254
+ requires: string[];
1255
+ config: {
1256
+ entities?: string[];
1257
+ workflowHandles?: string[];
1258
+ envKeys?: string[];
1259
+ listenToCrm?: boolean;
1260
+ listenToEnv?: boolean;
1261
+ capabilities?: string[];
1262
+ };
1263
+ completedAt: string | null;
1264
+ skippedAt: string | null;
1265
+ invalidatedAt: string | null;
1266
+ metadata: Record<string, unknown> | null;
1267
+ };
1268
+ export type SetupListResult = {
1269
+ steps: SetupStepView[];
1270
+ capabilities: Record<string, boolean>;
1271
+ };
1272
+ /**
1273
+ * Install setup checklist API (requires sk_wkp_ token).
1274
+ */
1275
+ export declare const setup: {
1276
+ list(): Promise<SetupListResult>;
1277
+ get(handle: string): Promise<SetupStepView | null>;
1278
+ complete(handle: string, metadata?: Record<string, unknown>): Promise<SetupStepView>;
1279
+ skip(handle: string, metadata?: Record<string, unknown>): Promise<SetupStepView>;
1280
+ invalidate(handle: string, reason?: string, metadata?: Record<string, unknown>): Promise<SetupStepView>;
1281
+ capabilities(): Promise<Record<string, boolean>>;
1282
+ };
1244
1283
  export {};
@@ -1200,6 +1200,72 @@ async function handleProvision(body, hooks) {
1200
1200
  }
1201
1201
  }
1202
1202
 
1203
+ // src/server/handlers/setup-revalidate-handler.ts
1204
+ async function handleSetupRevalidate(body, hooks) {
1205
+ const revalidateHook = hooks?.setup?.revalidate;
1206
+ if (!revalidateHook) {
1207
+ return {
1208
+ status: 404,
1209
+ body: { error: "Setup revalidate handler not configured" }
1210
+ };
1211
+ }
1212
+ if (!body.context?.appInstallationId || !body.context?.workplace || !body.context?.app || !body.reason || !Array.isArray(body.steps)) {
1213
+ return {
1214
+ status: 400,
1215
+ body: {
1216
+ error: {
1217
+ code: -32602,
1218
+ message: "Missing context (appInstallationId, workplace, app, reason, and steps required)"
1219
+ }
1220
+ }
1221
+ };
1222
+ }
1223
+ const ctx = {
1224
+ env: body.env ?? {},
1225
+ workplace: body.context.workplace,
1226
+ appInstallationId: body.context.appInstallationId,
1227
+ app: body.context.app,
1228
+ reason: body.reason,
1229
+ steps: body.steps,
1230
+ crm: body.crm,
1231
+ envChange: body.envChange,
1232
+ invocation: body.invocation,
1233
+ log: createContextLogger()
1234
+ };
1235
+ const requestConfig = {
1236
+ baseUrl: body.env?.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
1237
+ apiToken: body.env?.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
1238
+ };
1239
+ try {
1240
+ const handler = typeof revalidateHook === "function" ? revalidateHook : revalidateHook.handler;
1241
+ const result = await runWithLogContext(
1242
+ { invocation: body.invocation },
1243
+ async () => {
1244
+ return await runWithConfig(requestConfig, async () => {
1245
+ return await handler(ctx);
1246
+ });
1247
+ }
1248
+ );
1249
+ return {
1250
+ status: 200,
1251
+ body: {
1252
+ completed: result?.completed ?? [],
1253
+ invalidated: result?.invalidated ?? []
1254
+ }
1255
+ };
1256
+ } catch (err) {
1257
+ return {
1258
+ status: 500,
1259
+ body: {
1260
+ error: {
1261
+ code: -32603,
1262
+ message: err instanceof Error ? err.message : String(err ?? "")
1263
+ }
1264
+ }
1265
+ };
1266
+ }
1267
+ }
1268
+
1203
1269
  // src/server/handler-helpers.ts
1204
1270
  function parseHandlerEnvelope(parsedBody) {
1205
1271
  if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody) || !("env" in parsedBody) || !("request" in parsedBody)) {
@@ -1591,6 +1657,14 @@ async function handleProvisionRoute(req, ctx) {
1591
1657
  const result = await handleProvision(parseResult.data, ctx.config.hooks);
1592
1658
  return { status: result.status, body: result.body };
1593
1659
  }
1660
+ async function handleSetupRevalidateRoute(req, ctx) {
1661
+ const parseResult = parseJsonBody(req);
1662
+ if (!parseResult.success) {
1663
+ return parseResult.error;
1664
+ }
1665
+ const result = await handleSetupRevalidate(parseResult.data, ctx.config.hooks);
1666
+ return { status: result.status, body: result.body };
1667
+ }
1594
1668
  async function handleOAuthCallbackRoute(req, ctx) {
1595
1669
  const parseResult = parseJsonBody(req);
1596
1670
  if (!parseResult.success) {
@@ -1952,6 +2026,9 @@ async function routeRequest(req, ctx) {
1952
2026
  if (req.path === "/provision" && req.method === "POST") {
1953
2027
  return handleProvisionRoute(req, ctx);
1954
2028
  }
2029
+ if (req.path === "/setup/revalidate" && req.method === "POST") {
2030
+ return handleSetupRevalidateRoute(req, ctx);
2031
+ }
1955
2032
  if (req.path === "/oauth_callback" && req.method === "POST") {
1956
2033
  return handleOAuthCallbackRoute(req, ctx);
1957
2034
  }