thinkwork-cli 0.13.1 → 1.0.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/README.md +3 -14
- package/dist/{api-client-4DZ266J5.js → api-client-4P4JZOBX.js} +3 -1
- package/dist/bowser-6Z6JFAIR.js +2822 -0
- package/dist/{chunk-Y2WHJECR.js → chunk-4OMDWYZQ.js} +5 -44
- package/dist/chunk-AD2NIWB5.js +6516 -0
- package/dist/chunk-DV3MRBR5.js +43 -0
- package/dist/chunk-EI5ZX7CN.js +522 -0
- package/dist/chunk-EUJ7X6YD.js +126 -0
- package/dist/chunk-GG5WKBOX.js +436 -0
- package/dist/chunk-I64EZSU5.js +358 -0
- package/dist/chunk-KMIJ5KGT.js +128 -0
- package/dist/chunk-MWQ7RO76.js +137 -0
- package/dist/chunk-OCPHU34C.js +996 -0
- package/dist/chunk-OLUQ4U2Z.js +1728 -0
- package/dist/chunk-XYM7PE2A.js +2264 -0
- package/dist/chunk-YQCYD6NE.js +3299 -0
- package/dist/cli.js +49156 -31257
- package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
- package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
- package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
- package/dist/dist-es-2OTNTL4N.js +315 -0
- package/dist/dist-es-2XVAH4BQ.js +14726 -0
- package/dist/dist-es-3DPDUJAH.js +2000 -0
- package/dist/dist-es-BNZTBBVC.js +176 -0
- package/dist/dist-es-CD2RBDAC.js +88 -0
- package/dist/dist-es-I4FW57PS.js +88 -0
- package/dist/dist-es-KL7Y5NB2.js +377 -0
- package/dist/dist-es-KVFWJNDY.js +318 -0
- package/dist/dist-es-PS73DWNL.js +70 -0
- package/dist/dist-es-S3NZQFGT.js +170 -0
- package/dist/dist-es-SGCA4P32.js +48 -0
- package/dist/dist-es-WBM6J4RA.js +70 -0
- package/dist/dist-es-ZGVADYIF.js +485 -0
- package/dist/dist-es-ZKC2QKD2.js +377 -0
- package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +2 -1
- package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +1 -0
- package/dist/drizzle/0090_brain_schema_extraction.sql +10 -0
- package/dist/drizzle/0132_drop_computer_tables.sql +3 -0
- package/dist/drizzle/0160_compliance_event_types_plugins.sql +2 -0
- package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +1 -0
- package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +1 -0
- package/dist/drizzle/0227_analyst_reader_role.sql +17 -20
- package/dist/drizzle/0230_analyst_rls.sql +28 -36
- package/dist/drizzle/0250_kg_schema_extraction.sql +27 -0
- package/dist/drizzle/0251_brain_stragglers.sql +21 -0
- package/dist/drizzle/0253_drop_brain_substrate.sql +2 -0
- package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
- package/dist/drizzle/0261_native_auth_control_plane.sql +332 -0
- package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
- package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
- package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
- package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
- package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
- package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
- package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
- package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
- package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
- package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
- package/dist/drizzle/0269_ontology_system_map.sql +31 -0
- package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
- package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
- package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
- package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
- package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
- package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
- package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
- package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
- package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
- package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
- package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
- package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
- package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
- package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
- package/dist/drizzle/0284_user_brain_claims.sql +69 -0
- package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
- package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
- package/dist/drizzle/0287_brain_subject.sql +25 -0
- package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
- package/dist/event-streams-7JNLJEEL.js +894 -0
- package/dist/event-streams-KDF5RWR6.js +1515 -0
- package/dist/loadSso-ETCXTR4I.js +576 -0
- package/dist/loadSso-UBD7KUHE.js +583 -0
- package/dist/plugins/README.md +2 -2
- package/dist/plugins/catalog/package.json +1 -5
- package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -4
- package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
- package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -94
- package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -8
- package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -20
- package/dist/plugins/catalog/src/contracts.ts +17 -179
- package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -24
- package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
- package/dist/plugins/twenty/package.json +2 -1
- package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
- package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
- package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
- package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
- package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
- package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
- package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
- package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
- package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
- package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
- package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
- package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
- package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
- package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
- package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
- package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
- package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
- package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
- package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
- package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
- package/dist/plugins/twenty/test/manifest.test.ts +2 -4
- package/dist/scripts/post-deploy.sh +79 -1
- package/dist/signin-5QZNRSOO.js +821 -0
- package/dist/sso-oidc-XK673P54.js +837 -0
- package/dist/sts-OXI2UALF.js +1113 -0
- package/dist/sts-SQW4NBK3.js +1117 -0
- package/dist/terraform/examples/greenfield/main.tf +391 -176
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +26 -8
- package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
- package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
- package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
- package/dist/terraform/modules/app/agentcore-identity/main.tf +253 -0
- package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +239 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +22 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +55 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +45 -0
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +123 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
- package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
- package/dist/terraform/modules/app/agentcore-identity/variables.tf +60 -0
- package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
- package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
- package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
- package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
- package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
- package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
- package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
- package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
- package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
- package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
- package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
- package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
- package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
- package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
- package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
- package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
- package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
- package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
- package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
- package/dist/terraform/modules/app/deployment-control-plane/runner.py +3689 -233
- package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2579 -140
- package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
- package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
- package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
- package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
- package/dist/terraform/modules/app/lambda-api/handlers.tf +531 -619
- package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +350 -241
- package/dist/terraform/modules/app/lambda-api/main.tf +75 -13
- package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
- package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
- package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -10
- package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
- package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +307 -121
- package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
- package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
- package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
- package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
- package/dist/terraform/modules/app/static-site/main.tf +1 -1
- package/dist/terraform/modules/app/www-dns/main.tf +9 -27
- package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
- package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
- package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
- package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
- package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
- package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
- package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
- package/dist/terraform/modules/foundation/cognito/main.tf +367 -72
- package/dist/terraform/modules/foundation/cognito/outputs.tf +47 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +131 -9
- package/dist/terraform/modules/thinkwork/README.md +5 -1
- package/dist/terraform/modules/thinkwork/main.tf +459 -234
- package/dist/terraform/modules/thinkwork/outputs.tf +44 -62
- package/dist/terraform/modules/thinkwork/variables.tf +353 -89
- package/dist/terraform/schema.graphql +69 -36
- package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
- package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
- package/dist/workspace-defaults/files/ROUTER.md +1 -1
- package/dist/workspace-defaults/files/TOOLS.md +9 -39
- package/dist/workspace-defaults/files/USER.md +1 -0
- package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
- package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
- package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
- package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
- package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
- package/package.json +3 -2
- package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +0 -11
- package/dist/plugins/company-data/README.md +0 -49
- package/dist/plugins/company-data/package.json +0 -21
- package/dist/plugins/company-data/src/index.ts +0 -30
- package/dist/plugins/company-data/src/manifest.ts +0 -26
- package/dist/plugins/company-data/test/manifest.test.ts +0 -116
- package/dist/plugins/company-data/tsconfig.json +0 -8
- package/dist/plugins/company-etl/README.md +0 -46
- package/dist/plugins/company-etl/package.json +0 -21
- package/dist/plugins/company-etl/src/index.ts +0 -29
- package/dist/plugins/company-etl/src/manifest.ts +0 -26
- package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
- package/dist/plugins/company-etl/tsconfig.json +0 -8
- package/dist/plugins/lastmile/README.md +0 -50
- package/dist/plugins/lastmile/package.json +0 -23
- package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
- package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
- package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
- package/dist/plugins/lastmile/src/index.ts +0 -46
- package/dist/plugins/lastmile/src/manifest.ts +0 -139
- package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
- package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
- package/dist/plugins/lastmile/tsconfig.json +0 -8
- package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
- package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
- package/dist/plugins/workos-auth/package.json +0 -22
- package/dist/plugins/workos-auth/src/index.ts +0 -30
- package/dist/plugins/workos-auth/src/manifest.ts +0 -40
- package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
- package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
- package/dist/plugins/workos-auth/tsconfig.json +0 -8
- package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
- package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
- package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
- package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -101
- package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -9
- package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -25
- package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
- package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
- package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
- /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
- /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
* - `skills` — bundled skill folders seeded into the tenant catalog
|
|
9
9
|
* - `infrastructure` — a managed-app Terraform deployment
|
|
10
10
|
* - `ui-surface` — declared or launchable UI identity
|
|
11
|
-
* - `auth-provider` — declared/admin-configured login federation capability
|
|
12
11
|
*
|
|
13
12
|
* Capability declarations are catalog metadata, not install lifecycle
|
|
14
13
|
* components. They let a plugin advertise channel contracts whose runtime is
|
|
@@ -34,7 +33,6 @@ export const PLUGIN_COMPONENT_TYPES = [
|
|
|
34
33
|
"skills",
|
|
35
34
|
"infrastructure",
|
|
36
35
|
"ui-surface",
|
|
37
|
-
"auth-provider",
|
|
38
36
|
] as const;
|
|
39
37
|
|
|
40
38
|
export type PluginComponentType = (typeof PLUGIN_COMPONENT_TYPES)[number];
|
|
@@ -212,6 +210,15 @@ export interface McpServerComponent {
|
|
|
212
210
|
resultTransforms?: McpResultTransform[];
|
|
213
211
|
/** Optional human notes about the tools the server exposes. */
|
|
214
212
|
toolNotes?: string[];
|
|
213
|
+
/**
|
|
214
|
+
* THINK-623 — opt in to the long-call profile for this server's tools.
|
|
215
|
+
* Set it only when the server streams MCP progress notifications during
|
|
216
|
+
* long calls: the runtime then resets the per-attempt `callTool` wall on
|
|
217
|
+
* every notification (bounded by an absolute total ceiling) instead of
|
|
218
|
+
* killing the call at the fixed 60s wall. Servers that go quiet for
|
|
219
|
+
* longer than the per-attempt wall still time out.
|
|
220
|
+
*/
|
|
221
|
+
longRunning?: boolean;
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
export interface SkillSupportingFile {
|
|
@@ -294,59 +301,11 @@ export interface UiSurfaceLaunchMetadata {
|
|
|
294
301
|
entitlementProductKey?: string;
|
|
295
302
|
}
|
|
296
303
|
|
|
297
|
-
export const AUTH_PROVIDER_KEYS = ["workos"] as const;
|
|
298
|
-
|
|
299
|
-
export type AuthProviderKey = (typeof AUTH_PROVIDER_KEYS)[number];
|
|
300
|
-
|
|
301
|
-
export const AUTH_PROVIDER_CONFIG_FIELD_STORAGE = [
|
|
302
|
-
"metadata",
|
|
303
|
-
"secret-ref",
|
|
304
|
-
] as const;
|
|
305
|
-
|
|
306
|
-
export type AuthProviderConfigFieldStorage =
|
|
307
|
-
(typeof AUTH_PROVIDER_CONFIG_FIELD_STORAGE)[number];
|
|
308
|
-
|
|
309
|
-
export interface AuthProviderConfigField {
|
|
310
|
-
/** Operator-facing input key; values are never present in the manifest. */
|
|
311
|
-
key: string;
|
|
312
|
-
displayName: string;
|
|
313
|
-
required: boolean;
|
|
314
|
-
/**
|
|
315
|
-
* metadata: non-secret config such as issuer/client id.
|
|
316
|
-
* secret-ref: stored only as an operator-managed secret reference.
|
|
317
|
-
*/
|
|
318
|
-
storage: AuthProviderConfigFieldStorage;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export interface AuthProviderPublicOption {
|
|
322
|
-
/**
|
|
323
|
-
* Public-safe option key. For the U1-approved fallback this is `sso`; do not
|
|
324
|
-
* use provider-specific Google/Microsoft keys unless routing evidence exists.
|
|
325
|
-
*/
|
|
326
|
-
key: string;
|
|
327
|
-
displayName: string;
|
|
328
|
-
providerSpecific: boolean;
|
|
329
|
-
recommended?: boolean;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
export interface AuthProviderComponent {
|
|
333
|
-
type: "auth-provider";
|
|
334
|
-
key: string;
|
|
335
|
-
displayName: string;
|
|
336
|
-
provider: AuthProviderKey;
|
|
337
|
-
settingsSurface: string;
|
|
338
|
-
/** Cognito IdP name the validated bridge will use; not a secret. */
|
|
339
|
-
cognitoIdentityProviderName: string;
|
|
340
|
-
configFields: AuthProviderConfigField[];
|
|
341
|
-
publicOptions: AuthProviderPublicOption[];
|
|
342
|
-
}
|
|
343
|
-
|
|
344
304
|
export type PluginComponent =
|
|
345
305
|
| McpServerComponent
|
|
346
306
|
| SkillsComponent
|
|
347
307
|
| InfrastructureComponent
|
|
348
|
-
| UiSurfaceComponent
|
|
349
|
-
| AuthProviderComponent;
|
|
308
|
+
| UiSurfaceComponent;
|
|
350
309
|
|
|
351
310
|
export interface EmailChannelProviderOption {
|
|
352
311
|
key: EmailChannelProviderKey;
|
|
@@ -566,12 +525,6 @@ function validatePluginVersion(value: unknown, pluginKey: string): void {
|
|
|
566
525
|
case "ui-surface":
|
|
567
526
|
validateUiSurfaceComponent(component as UiSurfaceComponent, label);
|
|
568
527
|
break;
|
|
569
|
-
case "auth-provider":
|
|
570
|
-
validateAuthProviderComponent(
|
|
571
|
-
component as AuthProviderComponent,
|
|
572
|
-
label,
|
|
573
|
-
);
|
|
574
|
-
break;
|
|
575
528
|
}
|
|
576
529
|
}
|
|
577
530
|
|
|
@@ -727,6 +680,12 @@ function validateMcpServerComponent(
|
|
|
727
680
|
);
|
|
728
681
|
}
|
|
729
682
|
}
|
|
683
|
+
if (
|
|
684
|
+
component.longRunning !== undefined &&
|
|
685
|
+
typeof component.longRunning !== "boolean"
|
|
686
|
+
) {
|
|
687
|
+
throw new PluginManifestError(`${prefix}.longRunning must be a boolean`);
|
|
688
|
+
}
|
|
730
689
|
const auth = component.auth as Partial<McpServerAuth> | undefined;
|
|
731
690
|
if (!auth || typeof auth !== "object" || Array.isArray(auth)) {
|
|
732
691
|
throw new PluginManifestError(`${prefix}.auth is required`);
|
|
@@ -1443,131 +1402,10 @@ function validateUiSurfaceLaunchMetadata(
|
|
|
1443
1402
|
}
|
|
1444
1403
|
}
|
|
1445
1404
|
|
|
1446
|
-
function validateAuthProviderComponent(
|
|
1447
|
-
component: Partial<AuthProviderComponent>,
|
|
1448
|
-
label: string,
|
|
1449
|
-
): void {
|
|
1450
|
-
const prefix = `${label}: auth-provider "${component.key}"`;
|
|
1451
|
-
requireString(component.displayName, `${prefix}.displayName`);
|
|
1452
|
-
if (
|
|
1453
|
-
typeof component.provider !== "string" ||
|
|
1454
|
-
!(AUTH_PROVIDER_KEYS as readonly string[]).includes(component.provider)
|
|
1455
|
-
) {
|
|
1456
|
-
throw new PluginManifestError(
|
|
1457
|
-
`${prefix}.provider "${String(component.provider)}" is not supported`,
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
requireString(component.settingsSurface, `${prefix}.settingsSurface`);
|
|
1461
|
-
requireString(
|
|
1462
|
-
component.cognitoIdentityProviderName,
|
|
1463
|
-
`${prefix}.cognitoIdentityProviderName`,
|
|
1464
|
-
);
|
|
1465
|
-
|
|
1466
|
-
if (
|
|
1467
|
-
!Array.isArray(component.configFields) ||
|
|
1468
|
-
component.configFields.length === 0
|
|
1469
|
-
) {
|
|
1470
|
-
throw new PluginManifestError(
|
|
1471
|
-
`${prefix}.configFields must be a non-empty array`,
|
|
1472
|
-
);
|
|
1473
|
-
}
|
|
1474
|
-
const seenConfigKeys = new Set<string>();
|
|
1475
|
-
for (const [index, field] of component.configFields.entries()) {
|
|
1476
|
-
const fieldPrefix = `${prefix}.configFields[${index}]`;
|
|
1477
|
-
if (!field || typeof field !== "object" || Array.isArray(field)) {
|
|
1478
|
-
throw new PluginManifestError(`${fieldPrefix} must be an object`);
|
|
1479
|
-
}
|
|
1480
|
-
rejectManifestValueCarrier(
|
|
1481
|
-
field as unknown as Record<string, unknown>,
|
|
1482
|
-
fieldPrefix,
|
|
1483
|
-
);
|
|
1484
|
-
requireString(field.key, `${fieldPrefix}.key`);
|
|
1485
|
-
if (!CREDENTIAL_KEY_RE.test(field.key)) {
|
|
1486
|
-
throw new PluginManifestError(
|
|
1487
|
-
`${fieldPrefix}.key "${field.key}" must match ${CREDENTIAL_KEY_RE.source}`,
|
|
1488
|
-
);
|
|
1489
|
-
}
|
|
1490
|
-
if (seenConfigKeys.has(field.key)) {
|
|
1491
|
-
throw new PluginManifestError(
|
|
1492
|
-
`${prefix}.configFields declares duplicate key "${field.key}"`,
|
|
1493
|
-
);
|
|
1494
|
-
}
|
|
1495
|
-
seenConfigKeys.add(field.key);
|
|
1496
|
-
requireString(field.displayName, `${fieldPrefix}.displayName`);
|
|
1497
|
-
if (typeof field.required !== "boolean") {
|
|
1498
|
-
throw new PluginManifestError(
|
|
1499
|
-
`${fieldPrefix}.required must be a boolean`,
|
|
1500
|
-
);
|
|
1501
|
-
}
|
|
1502
|
-
if (
|
|
1503
|
-
!(AUTH_PROVIDER_CONFIG_FIELD_STORAGE as readonly string[]).includes(
|
|
1504
|
-
field.storage,
|
|
1505
|
-
)
|
|
1506
|
-
) {
|
|
1507
|
-
throw new PluginManifestError(
|
|
1508
|
-
`${fieldPrefix}.storage must be one of ${AUTH_PROVIDER_CONFIG_FIELD_STORAGE.join(", ")}`,
|
|
1509
|
-
);
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
if (
|
|
1514
|
-
!Array.isArray(component.publicOptions) ||
|
|
1515
|
-
component.publicOptions.length === 0
|
|
1516
|
-
) {
|
|
1517
|
-
throw new PluginManifestError(
|
|
1518
|
-
`${prefix}.publicOptions must be a non-empty array`,
|
|
1519
|
-
);
|
|
1520
|
-
}
|
|
1521
|
-
const seenOptionKeys = new Set<string>();
|
|
1522
|
-
let recommendedCount = 0;
|
|
1523
|
-
for (const [index, option] of component.publicOptions.entries()) {
|
|
1524
|
-
const optionPrefix = `${prefix}.publicOptions[${index}]`;
|
|
1525
|
-
if (!option || typeof option !== "object" || Array.isArray(option)) {
|
|
1526
|
-
throw new PluginManifestError(`${optionPrefix} must be an object`);
|
|
1527
|
-
}
|
|
1528
|
-
rejectManifestValueCarrier(
|
|
1529
|
-
option as unknown as Record<string, unknown>,
|
|
1530
|
-
optionPrefix,
|
|
1531
|
-
);
|
|
1532
|
-
requireString(option.key, `${optionPrefix}.key`);
|
|
1533
|
-
if (!SLUG_RE.test(option.key)) {
|
|
1534
|
-
throw new PluginManifestError(
|
|
1535
|
-
`${optionPrefix}.key "${option.key}" must match ${SLUG_RE.source}`,
|
|
1536
|
-
);
|
|
1537
|
-
}
|
|
1538
|
-
if (seenOptionKeys.has(option.key)) {
|
|
1539
|
-
throw new PluginManifestError(
|
|
1540
|
-
`${prefix}.publicOptions declares duplicate key "${option.key}"`,
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1543
|
-
seenOptionKeys.add(option.key);
|
|
1544
|
-
requireString(option.displayName, `${optionPrefix}.displayName`);
|
|
1545
|
-
if (typeof option.providerSpecific !== "boolean") {
|
|
1546
|
-
throw new PluginManifestError(
|
|
1547
|
-
`${optionPrefix}.providerSpecific must be a boolean`,
|
|
1548
|
-
);
|
|
1549
|
-
}
|
|
1550
|
-
if (
|
|
1551
|
-
option.recommended !== undefined &&
|
|
1552
|
-
typeof option.recommended !== "boolean"
|
|
1553
|
-
) {
|
|
1554
|
-
throw new PluginManifestError(
|
|
1555
|
-
`${optionPrefix}.recommended must be a boolean`,
|
|
1556
|
-
);
|
|
1557
|
-
}
|
|
1558
|
-
if (option.recommended === true) recommendedCount += 1;
|
|
1559
|
-
}
|
|
1560
|
-
if (recommendedCount !== 1) {
|
|
1561
|
-
throw new PluginManifestError(
|
|
1562
|
-
`${prefix}.publicOptions must declare exactly one recommended option`,
|
|
1563
|
-
);
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
1405
|
function rejectManifestValueCarrier(
|
|
1568
1406
|
value: Record<string, unknown>,
|
|
1569
1407
|
prefix: string,
|
|
1570
|
-
context = "
|
|
1408
|
+
context = "plugin manifests",
|
|
1571
1409
|
): void {
|
|
1572
1410
|
for (const key of ["value", "secret", "defaultValue", "currentValue"]) {
|
|
1573
1411
|
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
|
@@ -3,27 +3,14 @@
|
|
|
3
3
|
// pnpm --filter @thinkwork/plugin-catalog generate:plugins
|
|
4
4
|
|
|
5
5
|
import { defineFirstPartyPluginPackage } from "../plugin-package";
|
|
6
|
-
import { companyDataPluginPackage as rawCompanyDataPluginPackage } from "@thinkwork/plugin-company-data";
|
|
7
|
-
import { companyEtlPluginPackage as rawCompanyEtlPluginPackage } from "@thinkwork/plugin-company-etl";
|
|
8
6
|
import { emailChannelPluginPackage as rawEmailChannelPluginPackage } from "@thinkwork/plugin-email-channel";
|
|
9
|
-
import { lastmilePluginPackage as rawLastmilePluginPackage } from "@thinkwork/plugin-lastmile";
|
|
10
7
|
import { n8nPluginPackage as rawN8nPluginPackage } from "@thinkwork/plugin-n8n";
|
|
11
8
|
import { sendgridPluginPackage as rawSendgridPluginPackage } from "@thinkwork/plugin-sendgrid";
|
|
12
9
|
import { twentyPluginPackage as rawTwentyPluginPackage } from "@thinkwork/plugin-twenty";
|
|
13
|
-
import { workosAuthPluginPackage as rawWorkosAuthPluginPackage } from "@thinkwork/plugin-workos-auth";
|
|
14
10
|
|
|
15
|
-
export const companyDataPluginPackage = defineFirstPartyPluginPackage(
|
|
16
|
-
rawCompanyDataPluginPackage,
|
|
17
|
-
);
|
|
18
|
-
export const companyEtlPluginPackage = defineFirstPartyPluginPackage(
|
|
19
|
-
rawCompanyEtlPluginPackage,
|
|
20
|
-
);
|
|
21
11
|
export const emailChannelPluginPackage = defineFirstPartyPluginPackage(
|
|
22
12
|
rawEmailChannelPluginPackage,
|
|
23
13
|
);
|
|
24
|
-
export const lastmilePluginPackage = defineFirstPartyPluginPackage(
|
|
25
|
-
rawLastmilePluginPackage,
|
|
26
|
-
);
|
|
27
14
|
export const n8nPluginPackage = defineFirstPartyPluginPackage(
|
|
28
15
|
rawN8nPluginPackage,
|
|
29
16
|
);
|
|
@@ -33,28 +20,17 @@ export const sendgridPluginPackage = defineFirstPartyPluginPackage(
|
|
|
33
20
|
export const twentyPluginPackage = defineFirstPartyPluginPackage(
|
|
34
21
|
rawTwentyPluginPackage,
|
|
35
22
|
);
|
|
36
|
-
export const workosAuthPluginPackage = defineFirstPartyPluginPackage(
|
|
37
|
-
rawWorkosAuthPluginPackage,
|
|
38
|
-
);
|
|
39
23
|
|
|
40
|
-
export const companyDataManifest = companyDataPluginPackage.manifest;
|
|
41
|
-
export const companyEtlManifest = companyEtlPluginPackage.manifest;
|
|
42
24
|
export const emailChannelManifest = emailChannelPluginPackage.manifest;
|
|
43
|
-
export const lastmileManifest = lastmilePluginPackage.manifest;
|
|
44
25
|
export const n8nManifest = n8nPluginPackage.manifest;
|
|
45
26
|
export const sendgridManifest = sendgridPluginPackage.manifest;
|
|
46
27
|
export const twentyManifest = twentyPluginPackage.manifest;
|
|
47
|
-
export const workosAuthManifest = workosAuthPluginPackage.manifest;
|
|
48
28
|
|
|
49
29
|
export const firstPartyPluginPackages = [
|
|
50
|
-
companyDataPluginPackage,
|
|
51
|
-
companyEtlPluginPackage,
|
|
52
30
|
emailChannelPluginPackage,
|
|
53
|
-
lastmilePluginPackage,
|
|
54
31
|
n8nPluginPackage,
|
|
55
32
|
sendgridPluginPackage,
|
|
56
33
|
twentyPluginPackage,
|
|
57
|
-
workosAuthPluginPackage,
|
|
58
34
|
] as const;
|
|
59
35
|
|
|
60
36
|
/**
|
|
@@ -211,7 +211,7 @@ describe("n8n integrated app contract", () => {
|
|
|
211
211
|
const docsPage = await readFile(
|
|
212
212
|
fileURLToPath(
|
|
213
213
|
new URL(
|
|
214
|
-
"../../../docs/
|
|
214
|
+
"../../../docs/reference/applications/n8n.mdx",
|
|
215
215
|
import.meta.url,
|
|
216
216
|
),
|
|
217
217
|
),
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# TEI LastMile → Twenty sync — Lambda deploy runbook
|
|
2
|
+
|
|
3
|
+
Deploy the nightly LastMile → Twenty delta sync as an AWS Lambda in **TEI's
|
|
4
|
+
account** (the same account as `n8n.lastmile-tei.com` and the LastMile RDS), and
|
|
5
|
+
invoke it nightly from n8n. The function reuses the exact code proven against
|
|
6
|
+
TEI's live Twenty; a full delta run is ~90s, so it fits Lambda's 15-minute
|
|
7
|
+
ceiling with wide headroom.
|
|
8
|
+
|
|
9
|
+
**Properties:** idempotent (upsert by `sourceId` + deletion mirror), reads
|
|
10
|
+
LastMile **read-only**, never provisions members, needs **no AWS credentials**
|
|
11
|
+
of its own (the ~4 CRM attachments Twenty can't ingest are reported, not fatal).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Build the deployment zip
|
|
16
|
+
|
|
17
|
+
From the repo root:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pnpm install
|
|
21
|
+
bash plugins/twenty/scripts/build-lambda.sh
|
|
22
|
+
# → plugins/twenty/dist/tei-lastmile-sync-lambda.zip (handler: index.handler)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The zip is self-contained (pg, bcryptjs, aws-sdk all inlined) — no `npm install`
|
|
26
|
+
in the account.
|
|
27
|
+
|
|
28
|
+
## 2. Create the function
|
|
29
|
+
|
|
30
|
+
- **Runtime:** Node.js 22.x
|
|
31
|
+
- **Handler:** `index.handler`
|
|
32
|
+
- **Architecture:** arm64 or x86_64 (either)
|
|
33
|
+
- **Timeout:** 300s (run is ~90s; leaves headroom for a busier delta)
|
|
34
|
+
- **Memory:** 512 MB
|
|
35
|
+
- **Code:** upload `tei-lastmile-sync-lambda.zip`
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
aws lambda create-function \
|
|
39
|
+
--function-name tei-lastmile-sync \
|
|
40
|
+
--runtime nodejs22.x --handler index.handler \
|
|
41
|
+
--timeout 300 --memory-size 512 \
|
|
42
|
+
--role arn:aws:iam::<ACCOUNT_ID>:role/<lambda-exec-role> \
|
|
43
|
+
--zip-file fileb://plugins/twenty/dist/tei-lastmile-sync-lambda.zip
|
|
44
|
+
# later updates: aws lambda update-function-code --function-name tei-lastmile-sync \
|
|
45
|
+
# --zip-file fileb://plugins/twenty/dist/tei-lastmile-sync-lambda.zip
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## 3. Configuration (the three env vars)
|
|
49
|
+
|
|
50
|
+
| Variable | Value |
|
|
51
|
+
| ----------------------- | ---------------------------------------------------- |
|
|
52
|
+
| `TWENTY_PUBLIC_URL` | `https://crm.tei.thinkwork.ai` |
|
|
53
|
+
| `TWENTY_API_KEY` | Twenty workspace API key (Settings → API & Webhooks) |
|
|
54
|
+
| `LASTMILE_DATABASE_URL` | read-only DSN for the `dispatch` database |
|
|
55
|
+
|
|
56
|
+
Set them directly as Lambda environment variables, or (preferred) store the two
|
|
57
|
+
secrets in Secrets Manager and resolve them at start. If you use Secrets
|
|
58
|
+
Manager, add `secretsmanager:GetSecretValue` to the execution role.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
aws lambda update-function-configuration --function-name tei-lastmile-sync \
|
|
62
|
+
--environment "Variables={TWENTY_PUBLIC_URL=https://crm.tei.thinkwork.ai,TWENTY_API_KEY=***,LASTMILE_DATABASE_URL=***}"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 4. Networking — the one thing that must be right
|
|
66
|
+
|
|
67
|
+
The function must reach **both** the LastMile RDS **and** `crm.tei.thinkwork.ai`.
|
|
68
|
+
Mirror wherever n8n already connects to the RDS:
|
|
69
|
+
|
|
70
|
+
- **In-VPC (cleanest):** attach the Lambda to the RDS's VPC/subnets, give it a
|
|
71
|
+
security group allowed to reach the RDS on 5432, and ensure the subnets have a
|
|
72
|
+
**NAT gateway** so it can still reach `crm.tei.thinkwork.ai` over HTTPS.
|
|
73
|
+
- **No-VPC (public):** the RDS is a public endpoint — the function reaches it and
|
|
74
|
+
Twenty over the internet, but the **RDS security group must admit the
|
|
75
|
+
function's egress IP** (the account's NAT/EIP). This is the same allowlisting
|
|
76
|
+
n8n already has.
|
|
77
|
+
|
|
78
|
+
Execution role needs at least `AWSLambdaBasicExecutionRole` (CloudWatch Logs),
|
|
79
|
+
plus `AWSLambdaVPCAccessExecutionRole` if you attach it to a VPC.
|
|
80
|
+
|
|
81
|
+
## 5. Smoke test before scheduling
|
|
82
|
+
|
|
83
|
+
Always dry-run first (reads + reports, writes nothing):
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
aws lambda invoke --function-name tei-lastmile-sync \
|
|
87
|
+
--payload '{"mode":"dry-run"}' --cli-binary-format raw-in-base64-out out.json
|
|
88
|
+
cat out.json # → {"ok":true,"failed":0,"report":{...parity...}}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
If `report.mode` is `dry-run` and the entity counts look right, you're wired
|
|
92
|
+
correctly. Then run once for real: `--payload '{"mode":"apply"}'` (or `'{}'` —
|
|
93
|
+
apply is the default).
|
|
94
|
+
|
|
95
|
+
## 6. Wire n8n
|
|
96
|
+
|
|
97
|
+
1. **Schedule Trigger** node — cron `0 3 * * *` (03:00 America/Chicago), or your
|
|
98
|
+
preferred window.
|
|
99
|
+
2. **AWS Lambda** node → _Invoke_, region `us-east-1`, function
|
|
100
|
+
`tei-lastmile-sync`, invocation type `RequestResponse`. Leave the payload
|
|
101
|
+
empty (defaults to apply) or send `{"mode":"apply"}`.
|
|
102
|
+
3. (Optional) an **IF** node on `{{$json.ok}}` / `{{$json.failed}}` to post a
|
|
103
|
+
Slack/email alert when a run reports soft failures.
|
|
104
|
+
|
|
105
|
+
n8n's AWS credential needs `lambda:InvokeFunction` on the function ARN.
|
|
106
|
+
|
|
107
|
+
## 7. Operational invariants (human-owned)
|
|
108
|
+
|
|
109
|
+
- Keep Twenty's **"Create company when adding a new person"** workflow
|
|
110
|
+
**deactivated** — the API key can't toggle it, and it mislinks new people.
|
|
111
|
+
- **Rotate** the shared `TWENTY_REP_PASSWORD` once the validation window closes.
|
|
112
|
+
- A **cold re-seed** (empty Twenty) creates thousands of records at Twenty's rate
|
|
113
|
+
limit and can exceed 15 min — run that from the CLI/bundle, not this Lambda.
|
|
114
|
+
Steady-state nightly deltas are the Lambda's job.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build a self-contained AWS Lambda deployment zip for the LastMile → Twenty
|
|
3
|
+
# nightly sync. The bundle inlines every dependency (pg, bcryptjs, aws-sdk),
|
|
4
|
+
# so the target account needs no `npm install` — just upload the zip.
|
|
5
|
+
#
|
|
6
|
+
# Usage: bash scripts/build-lambda.sh
|
|
7
|
+
# Output: plugins/twenty/dist/tei-lastmile-sync-lambda.zip (handler: index.handler)
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
cd "$(dirname "$0")/.." # -> plugins/twenty
|
|
11
|
+
OUT_DIR="dist/tei-lastmile-sync-lambda"
|
|
12
|
+
ZIP="dist/tei-lastmile-sync-lambda.zip"
|
|
13
|
+
|
|
14
|
+
rm -rf "$OUT_DIR" "$ZIP"
|
|
15
|
+
mkdir -p "$OUT_DIR"
|
|
16
|
+
|
|
17
|
+
# CJS + .js so Lambda's Node runtime resolves `index.handler` with no wrapper.
|
|
18
|
+
npx esbuild scripts/lambda-handler.ts \
|
|
19
|
+
--bundle --platform=node --format=cjs --target=node20 \
|
|
20
|
+
--outfile="$OUT_DIR/index.js"
|
|
21
|
+
|
|
22
|
+
# The repo's plugins/twenty/package.json is "type":"module", which would make
|
|
23
|
+
# Node treat the CJS index.js as ESM. Ship a CommonJS marker inside the zip so
|
|
24
|
+
# the handler loads correctly regardless of the deploy environment.
|
|
25
|
+
printf '{\n "type": "commonjs"\n}\n' >"$OUT_DIR/package.json"
|
|
26
|
+
|
|
27
|
+
(cd "$OUT_DIR" && zip -q -r "../../$ZIP" index.js package.json)
|
|
28
|
+
echo "built $(cd "$(dirname "$ZIP")" && pwd)/$(basename "$ZIP")"
|
|
29
|
+
echo " handler: index.handler size: $(du -h "$ZIP" | cut -f1)"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* One-off: make the Opportunity's Products table show Product, Quantity, and
|
|
4
|
+
* Amount.
|
|
5
|
+
*
|
|
6
|
+
* Twenty gives a new custom object a default TABLE view whose columns are
|
|
7
|
+
* `name` plus the audit fields (createdAt, createdBy, updatedAt, updatedBy).
|
|
8
|
+
* So the Products section rendered a row per line with no product, no quantity,
|
|
9
|
+
* and no amount — the data was all there, just never surfaced.
|
|
10
|
+
*
|
|
11
|
+
* This sets the opportunityProduct table view to:
|
|
12
|
+
* Product | Quantity | Amount | Line
|
|
13
|
+
* and hides the audit columns. It also orders the record-page field widget the
|
|
14
|
+
* same way.
|
|
15
|
+
*
|
|
16
|
+
* Idempotent: re-running reconciles the view to the same state.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* npx tsx scripts/configure-product-views.ts # dry-run
|
|
20
|
+
* npx tsx scripts/configure-product-views.ts --apply
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import process from "node:process";
|
|
24
|
+
|
|
25
|
+
import { fetchObjectMetadata } from "./lib/schema-ensure";
|
|
26
|
+
import { TwentyClient, normalizeBaseUrl } from "./lib/twenty-client";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Exactly what Eric asked for: "Each line needs to be Product Name, Quantity and
|
|
30
|
+
* Amount." `name` is the record's label identifier (it holds the product name,
|
|
31
|
+
* or "Line 2" for the 173 lines whose brand did not map), so it leads. Twenty
|
|
32
|
+
* will not let the label identifier be hidden, which is also why it stays.
|
|
33
|
+
*/
|
|
34
|
+
const VISIBLE_COLUMNS = ["name", "quantity", "amount"] as const;
|
|
35
|
+
|
|
36
|
+
/** Noise on a line item: audit trail, internals, and the product relation whose
|
|
37
|
+
* value already reads out through `name`. */
|
|
38
|
+
const HIDDEN_COLUMNS = [
|
|
39
|
+
"createdAt",
|
|
40
|
+
"createdBy",
|
|
41
|
+
"updatedAt",
|
|
42
|
+
"updatedBy",
|
|
43
|
+
"sourceId",
|
|
44
|
+
"sourceHash",
|
|
45
|
+
"isMobil",
|
|
46
|
+
"opportunity",
|
|
47
|
+
"lineNumber",
|
|
48
|
+
"product",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
interface ViewField {
|
|
52
|
+
id: string;
|
|
53
|
+
fieldMetadataId: string;
|
|
54
|
+
isVisible: boolean;
|
|
55
|
+
position: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function requireEnv(name: string): string {
|
|
59
|
+
const value = process.env[name];
|
|
60
|
+
if (!value) throw new Error(`Missing required environment variable: ${name}`);
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function log(message: string): void {
|
|
65
|
+
process.stderr.write(`${message}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main(): Promise<void> {
|
|
69
|
+
const apply = process.argv.slice(2).includes("--apply");
|
|
70
|
+
const client = new TwentyClient({
|
|
71
|
+
baseUrl: normalizeBaseUrl(requireEnv("TWENTY_PUBLIC_URL")),
|
|
72
|
+
authToken: requireEnv("TWENTY_API_KEY"),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const objects = await fetchObjectMetadata(client);
|
|
76
|
+
const line = objects.get("opportunityProduct");
|
|
77
|
+
if (!line) throw new Error('Twenty object "opportunityProduct" not found.');
|
|
78
|
+
const fieldIdByName = new Map(
|
|
79
|
+
[...line.fields.values()].map((field) => [field.name, field.id]),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const views = await client.requestWithRetry<{
|
|
83
|
+
getViews: Array<{ id: string; name: string; type: string }>;
|
|
84
|
+
}>(
|
|
85
|
+
"/metadata",
|
|
86
|
+
`query ConfigViews($objectMetadataId: String!) {
|
|
87
|
+
getViews(objectMetadataId: $objectMetadataId) { id name type }
|
|
88
|
+
}`,
|
|
89
|
+
{ objectMetadataId: line.id },
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const report: Record<string, unknown> = {
|
|
93
|
+
mode: apply ? "apply" : "dry-run",
|
|
94
|
+
views: views.getViews.map((view) => `${view.type}:${view.name}`),
|
|
95
|
+
};
|
|
96
|
+
const changes: string[] = [];
|
|
97
|
+
|
|
98
|
+
for (const view of views.getViews) {
|
|
99
|
+
const fields = await client.requestWithRetry<{
|
|
100
|
+
getViewFields: ViewField[];
|
|
101
|
+
}>(
|
|
102
|
+
"/metadata",
|
|
103
|
+
`query ConfigViewFields($viewId: String!) {
|
|
104
|
+
getViewFields(viewId: $viewId) { id fieldMetadataId isVisible position }
|
|
105
|
+
}`,
|
|
106
|
+
{ viewId: view.id },
|
|
107
|
+
);
|
|
108
|
+
const existingByFieldId = new Map(
|
|
109
|
+
fields.getViewFields.map((field) => [field.fieldMetadataId, field]),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Wanted columns, in order.
|
|
113
|
+
for (const [index, name] of VISIBLE_COLUMNS.entries()) {
|
|
114
|
+
const fieldMetadataId = fieldIdByName.get(name);
|
|
115
|
+
if (!fieldMetadataId) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`opportunityProduct.${name} not found; run restructure-products first.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const existing = existingByFieldId.get(fieldMetadataId);
|
|
121
|
+
if (existing?.isVisible && existing.position === index) continue;
|
|
122
|
+
|
|
123
|
+
changes.push(`${view.type}: show ${name} at ${index}`);
|
|
124
|
+
if (!apply) continue;
|
|
125
|
+
|
|
126
|
+
if (existing) {
|
|
127
|
+
await client.requestOnce(
|
|
128
|
+
"/metadata",
|
|
129
|
+
`mutation ConfigUpdateViewField($input: UpdateViewFieldInput!) {
|
|
130
|
+
updateViewField(input: $input) { id }
|
|
131
|
+
}`,
|
|
132
|
+
{
|
|
133
|
+
input: {
|
|
134
|
+
id: existing.id,
|
|
135
|
+
update: { isVisible: true, position: index },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
);
|
|
139
|
+
} else {
|
|
140
|
+
await client.requestOnce(
|
|
141
|
+
"/metadata",
|
|
142
|
+
`mutation ConfigCreateViewField($input: CreateViewFieldInput!) {
|
|
143
|
+
createViewField(input: $input) { id }
|
|
144
|
+
}`,
|
|
145
|
+
{
|
|
146
|
+
input: {
|
|
147
|
+
viewId: view.id,
|
|
148
|
+
fieldMetadataId,
|
|
149
|
+
isVisible: true,
|
|
150
|
+
position: index,
|
|
151
|
+
size: 150,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Everything else on the line is noise in a table of deal lines.
|
|
159
|
+
for (const name of HIDDEN_COLUMNS) {
|
|
160
|
+
const fieldMetadataId = fieldIdByName.get(name);
|
|
161
|
+
if (!fieldMetadataId) continue;
|
|
162
|
+
const existing = existingByFieldId.get(fieldMetadataId);
|
|
163
|
+
if (!existing || !existing.isVisible) continue;
|
|
164
|
+
changes.push(`${view.type}: hide ${name}`);
|
|
165
|
+
if (!apply) continue;
|
|
166
|
+
await client.requestOnce(
|
|
167
|
+
"/metadata",
|
|
168
|
+
`mutation ConfigHideViewField($input: UpdateViewFieldInput!) {
|
|
169
|
+
updateViewField(input: $input) { id }
|
|
170
|
+
}`,
|
|
171
|
+
{ input: { id: existing.id, update: { isVisible: false } } },
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The opportunity's `products` relation is left as-is (visible chips). An
|
|
177
|
+
// earlier version hid it and/or replaced it with a denormalized text field;
|
|
178
|
+
// both were wrong. The line records (Product / Quantity / Amount) are the
|
|
179
|
+
// correct structured model. A real inline COLUMNAR table on the record page
|
|
180
|
+
// is not something stock Twenty renders for a one-to-many relation — that
|
|
181
|
+
// needs a custom Twenty-app widget, scoped separately.
|
|
182
|
+
|
|
183
|
+
report.changes =
|
|
184
|
+
changes.length > 0 ? changes : "none — views already correct";
|
|
185
|
+
if (apply && changes.length > 0)
|
|
186
|
+
log(`applied ${changes.length} view changes`);
|
|
187
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main().catch((error) => {
|
|
191
|
+
process.stderr.write(
|
|
192
|
+
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
193
|
+
);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|