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.
Files changed (262) hide show
  1. package/README.md +3 -14
  2. package/dist/{api-client-4DZ266J5.js → api-client-4P4JZOBX.js} +3 -1
  3. package/dist/bowser-6Z6JFAIR.js +2822 -0
  4. package/dist/{chunk-Y2WHJECR.js → chunk-4OMDWYZQ.js} +5 -44
  5. package/dist/chunk-AD2NIWB5.js +6516 -0
  6. package/dist/chunk-DV3MRBR5.js +43 -0
  7. package/dist/chunk-EI5ZX7CN.js +522 -0
  8. package/dist/chunk-EUJ7X6YD.js +126 -0
  9. package/dist/chunk-GG5WKBOX.js +436 -0
  10. package/dist/chunk-I64EZSU5.js +358 -0
  11. package/dist/chunk-KMIJ5KGT.js +128 -0
  12. package/dist/chunk-MWQ7RO76.js +137 -0
  13. package/dist/chunk-OCPHU34C.js +996 -0
  14. package/dist/chunk-OLUQ4U2Z.js +1728 -0
  15. package/dist/chunk-XYM7PE2A.js +2264 -0
  16. package/dist/chunk-YQCYD6NE.js +3299 -0
  17. package/dist/cli.js +49156 -31257
  18. package/dist/commands/enterprise/templates/deploy-repo/scripts/apply-release.mjs +0 -1
  19. package/dist/commands/enterprise/templates/deploy-repo/scripts/smoke.mjs +0 -1
  20. package/dist/commands/enterprise/templates/deploy-repo/terraform/main.tf +0 -14
  21. package/dist/dist-es-2OTNTL4N.js +315 -0
  22. package/dist/dist-es-2XVAH4BQ.js +14726 -0
  23. package/dist/dist-es-3DPDUJAH.js +2000 -0
  24. package/dist/dist-es-BNZTBBVC.js +176 -0
  25. package/dist/dist-es-CD2RBDAC.js +88 -0
  26. package/dist/dist-es-I4FW57PS.js +88 -0
  27. package/dist/dist-es-KL7Y5NB2.js +377 -0
  28. package/dist/dist-es-KVFWJNDY.js +318 -0
  29. package/dist/dist-es-PS73DWNL.js +70 -0
  30. package/dist/dist-es-S3NZQFGT.js +170 -0
  31. package/dist/dist-es-SGCA4P32.js +48 -0
  32. package/dist/dist-es-WBM6J4RA.js +70 -0
  33. package/dist/dist-es-ZGVADYIF.js +485 -0
  34. package/dist/dist-es-ZKC2QKD2.js +377 -0
  35. package/dist/drizzle/0076_scheduled_jobs_marco_backfill.sql +2 -1
  36. package/dist/drizzle/0079_seed_tenant_customize_catalog.sql +1 -0
  37. package/dist/drizzle/0090_brain_schema_extraction.sql +10 -0
  38. package/dist/drizzle/0132_drop_computer_tables.sql +3 -0
  39. package/dist/drizzle/0160_compliance_event_types_plugins.sql +2 -0
  40. package/dist/drizzle/0214_think137_drop_judge_evidence_roi.sql +1 -0
  41. package/dist/drizzle/0216_drop_agent_loop_versions_goal_worker_policy.sql +1 -0
  42. package/dist/drizzle/0227_analyst_reader_role.sql +17 -20
  43. package/dist/drizzle/0230_analyst_rls.sql +28 -36
  44. package/dist/drizzle/0250_kg_schema_extraction.sql +27 -0
  45. package/dist/drizzle/0251_brain_stragglers.sql +21 -0
  46. package/dist/drizzle/0253_drop_brain_substrate.sql +2 -0
  47. package/dist/drizzle/0257_agents_runtime_allow_harness.sql +24 -0
  48. package/dist/drizzle/0261_native_auth_control_plane.sql +332 -0
  49. package/dist/drizzle/0262_auth_subscription_tickets.sql +59 -0
  50. package/dist/drizzle/0263_agentcore_runtime_identifier.sql +69 -0
  51. package/dist/drizzle/0263_drop_workos_auth_runtime.sql +236 -0
  52. package/dist/drizzle/0264_auth_enrollment_attempt_limits.sql +15 -0
  53. package/dist/drizzle/0265_eval_runs_requester_identity.sql +28 -0
  54. package/dist/drizzle/0265_require_fully_valid_auth_publication.sql +27 -0
  55. package/dist/drizzle/0266_auth_identity_recovery_grants.sql +9 -0
  56. package/dist/drizzle/0266_eval_profiles_runtime_type.sql +12 -0
  57. package/dist/drizzle/0267_ontology_candidate_rejections.sql +53 -0
  58. package/dist/drizzle/0268_identity_crosswalk_routing.sql +182 -0
  59. package/dist/drizzle/0269_ontology_system_map.sql +31 -0
  60. package/dist/drizzle/0270_tool_execution_ledger.sql +115 -0
  61. package/dist/drizzle/0271_ontology_twin_declarations.sql +47 -0
  62. package/dist/drizzle/0272_identity_graph_projection_cursor.sql +23 -0
  63. package/dist/drizzle/0273_twin_materialization_suggestions.sql +29 -0
  64. package/dist/drizzle/0274_tenant_mcp_twin_keys.sql +59 -0
  65. package/dist/drizzle/0275_identity_bulk_rebuild_fence.sql +30 -0
  66. package/dist/drizzle/0276_tool_execution_ledger_allow_cascade_delete.sql +18 -0
  67. package/dist/drizzle/0277_knowledge_base_sources.sql +107 -0
  68. package/dist/drizzle/0278_kb_page_transcription.sql +55 -0
  69. package/dist/drizzle/0279_mcp_servers_off_plugin_provenance.sql +93 -0
  70. package/dist/drizzle/0280_work_item_external_refs_drop_plane_provider.sql +46 -0
  71. package/dist/drizzle/0281_tenant_mcp_twin_keys_expiry_suffix.sql +9 -0
  72. package/dist/drizzle/0282_tenant_mcp_twin_keys_grants.sql +24 -0
  73. package/dist/drizzle/0283_agents_agentcore_runtime_dispatch.sql +13 -0
  74. package/dist/drizzle/0284_user_brain_claims.sql +69 -0
  75. package/dist/drizzle/0285_tenant_mcp_twin_keys_trusted_subsystem.sql +30 -0
  76. package/dist/drizzle/0286_brain_analytics_key.sql +26 -0
  77. package/dist/drizzle/0287_brain_subject.sql +25 -0
  78. package/dist/drizzle/0288_multi_lane_identities.sql +25 -0
  79. package/dist/event-streams-7JNLJEEL.js +894 -0
  80. package/dist/event-streams-KDF5RWR6.js +1515 -0
  81. package/dist/loadSso-ETCXTR4I.js +576 -0
  82. package/dist/loadSso-UBD7KUHE.js +583 -0
  83. package/dist/plugins/README.md +2 -2
  84. package/dist/plugins/catalog/package.json +1 -5
  85. package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -4
  86. package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
  87. package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -94
  88. package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -8
  89. package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -20
  90. package/dist/plugins/catalog/src/contracts.ts +17 -179
  91. package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -24
  92. package/dist/plugins/n8n/test/integrated-app-contract.test.ts +1 -1
  93. package/dist/plugins/twenty/package.json +2 -1
  94. package/dist/plugins/twenty/scripts/LAMBDA_DEPLOY.md +114 -0
  95. package/dist/plugins/twenty/scripts/build-lambda.sh +29 -0
  96. package/dist/plugins/twenty/scripts/configure-product-views.ts +195 -0
  97. package/dist/plugins/twenty/scripts/lambda-handler.ts +49 -0
  98. package/dist/plugins/twenty/scripts/lib/__tests__/load-attachments.test.ts +38 -0
  99. package/dist/plugins/twenty/scripts/lib/__tests__/load-records.test.ts +145 -2
  100. package/dist/plugins/twenty/scripts/lib/__tests__/mappers.test.ts +397 -151
  101. package/dist/plugins/twenty/scripts/lib/__tests__/members-ensure.test.ts +28 -129
  102. package/dist/plugins/twenty/scripts/lib/__tests__/provision-members-db.test.ts +10 -0
  103. package/dist/plugins/twenty/scripts/lib/__tests__/schema-ensure.test.ts +13 -8
  104. package/dist/plugins/twenty/scripts/lib/lastmile-reader.ts +222 -2
  105. package/dist/plugins/twenty/scripts/lib/load-attachments.ts +29 -0
  106. package/dist/plugins/twenty/scripts/lib/load-records.ts +160 -43
  107. package/dist/plugins/twenty/scripts/lib/mappers.ts +368 -152
  108. package/dist/plugins/twenty/scripts/lib/members-ensure.ts +43 -268
  109. package/dist/plugins/twenty/scripts/lib/provision-members-db.ts +57 -0
  110. package/dist/plugins/twenty/scripts/lib/schema-ensure.ts +136 -31
  111. package/dist/plugins/twenty/scripts/migrate-lastmile.ts +254 -152
  112. package/dist/plugins/twenty/scripts/provision-twenty-members.ts +44 -1
  113. package/dist/plugins/twenty/scripts/purge-lastmile-import.ts +178 -0
  114. package/dist/plugins/twenty/scripts/restructure-products.ts +312 -0
  115. package/dist/plugins/twenty/scripts/retag-stage-values.ts +362 -0
  116. package/dist/plugins/twenty/test/manifest.test.ts +2 -4
  117. package/dist/scripts/post-deploy.sh +79 -1
  118. package/dist/signin-5QZNRSOO.js +821 -0
  119. package/dist/sso-oidc-XK673P54.js +837 -0
  120. package/dist/sts-OXI2UALF.js +1113 -0
  121. package/dist/sts-SQW4NBK3.js +1117 -0
  122. package/dist/terraform/examples/greenfield/main.tf +391 -176
  123. package/dist/terraform/examples/greenfield/terraform.tfvars.example +26 -8
  124. package/dist/terraform/modules/app/agentcore-admin/main.tf +47 -0
  125. package/dist/terraform/modules/app/agentcore-code-interpreter/README.md +39 -47
  126. package/dist/terraform/modules/app/agentcore-code-interpreter/main.tf +13 -66
  127. package/dist/terraform/modules/app/agentcore-identity/main.tf +253 -0
  128. package/dist/terraform/modules/app/agentcore-identity/outputs.tf +51 -0
  129. package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +239 -0
  130. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +22 -0
  131. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_twenty_identity.sh +16 -0
  132. package/dist/terraform/modules/app/agentcore-identity/scripts/delete_workload_identity.sh +19 -0
  133. package/dist/terraform/modules/app/agentcore-identity/scripts/ensure_workload_identity.sh +29 -0
  134. package/dist/terraform/modules/app/agentcore-identity/scripts/read_identity.sh +55 -0
  135. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_identity.sh +73 -0
  136. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +45 -0
  137. package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +123 -0
  138. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/after.tf +15 -0
  139. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/before.tf +7 -0
  140. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-after.tf +8 -0
  141. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/identity-before.tf +9 -0
  142. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-after.tf +3 -0
  143. package/dist/terraform/modules/app/agentcore-identity/tests/tainted-marker/thinkwork-before.tf +3 -0
  144. package/dist/terraform/modules/app/agentcore-identity/variables.tf +60 -0
  145. package/dist/terraform/modules/app/agentcore-memory/README.md +46 -9
  146. package/dist/terraform/modules/app/agentcore-memory/main.tf +27 -10
  147. package/dist/terraform/modules/app/agentcore-memory/scripts/create_or_find_memory.sh +156 -24
  148. package/dist/terraform/modules/app/agentcore-pi/README.md +1 -3
  149. package/dist/terraform/modules/app/agentcore-pi/main.tf +42 -52
  150. package/dist/terraform/modules/app/agentcore-pi/outputs.tf +3 -3
  151. package/dist/terraform/modules/app/agentcore-pi/scripts/reconcile_pi_runtime.mjs +175 -0
  152. package/dist/terraform/modules/app/agentcore-pi/variables.tf +35 -28
  153. package/dist/terraform/modules/app/agentcore-platform/main.tf +68 -0
  154. package/dist/terraform/modules/app/agentcore-runtime/main.tf +4 -20
  155. package/dist/terraform/modules/app/appsync-subscriptions/main.tf +127 -21
  156. package/dist/terraform/modules/app/appsync-subscriptions/outputs.tf +0 -6
  157. package/dist/terraform/modules/app/appsync-subscriptions/variables.tf +5 -0
  158. package/dist/terraform/modules/app/capability-broker/main.tf +2 -2
  159. package/dist/terraform/modules/app/capability-broker/variables.tf +11 -0
  160. package/dist/terraform/modules/app/deployment-control-plane/README.md +264 -0
  161. package/dist/terraform/modules/app/deployment-control-plane/buildspec.yml +4 -1
  162. package/dist/terraform/modules/app/deployment-control-plane/main.tf +94 -0
  163. package/dist/terraform/modules/app/deployment-control-plane/outputs.tf +5 -0
  164. package/dist/terraform/modules/app/deployment-control-plane/runner.py +3689 -233
  165. package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2579 -140
  166. package/dist/terraform/modules/app/deployment-control-plane/variables.tf +33 -0
  167. package/dist/terraform/modules/app/lambda-api/auth-state.tf +72 -0
  168. package/dist/terraform/modules/app/lambda-api/chat-latency-observability.tf +466 -0
  169. package/dist/terraform/modules/app/lambda-api/dispatch-dlq.tf +55 -0
  170. package/dist/terraform/modules/app/lambda-api/handlers.tf +531 -619
  171. package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +350 -241
  172. package/dist/terraform/modules/app/lambda-api/main.tf +75 -13
  173. package/dist/terraform/modules/app/lambda-api/memory-alarms.tf +3 -28
  174. package/dist/terraform/modules/app/lambda-api/oauth-secrets.tf +1 -2
  175. package/dist/terraform/modules/app/lambda-api/outputs.tf +17 -10
  176. package/dist/terraform/modules/app/lambda-api/runtime-config.tf +3 -21
  177. package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
  178. package/dist/terraform/modules/app/lambda-api/variables.tf +307 -121
  179. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/README.md +1 -1
  180. package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/main.tf +18 -18
  181. package/dist/terraform/modules/app/sandbox-log-scrubber/README.md +11 -10
  182. package/dist/terraform/modules/app/sandbox-log-scrubber/main.tf +5 -5
  183. package/dist/terraform/modules/app/static-site/main.tf +1 -1
  184. package/dist/terraform/modules/app/www-dns/main.tf +9 -27
  185. package/dist/terraform/modules/app/www-dns/variables.tf +4 -10
  186. package/dist/terraform/modules/data/aurora-postgres/README.md +34 -0
  187. package/dist/terraform/modules/data/aurora-postgres/alarms.tf +82 -0
  188. package/dist/terraform/modules/data/aurora-postgres/main.tf +27 -57
  189. package/dist/terraform/modules/data/aurora-postgres/outputs.tf +0 -9
  190. package/dist/terraform/modules/data/aurora-postgres/variables.tf +41 -0
  191. package/dist/terraform/modules/data/s3-buckets/main.tf +5 -6
  192. package/dist/terraform/modules/foundation/cognito/main.tf +367 -72
  193. package/dist/terraform/modules/foundation/cognito/outputs.tf +47 -0
  194. package/dist/terraform/modules/foundation/cognito/variables.tf +131 -9
  195. package/dist/terraform/modules/thinkwork/README.md +5 -1
  196. package/dist/terraform/modules/thinkwork/main.tf +459 -234
  197. package/dist/terraform/modules/thinkwork/outputs.tf +44 -62
  198. package/dist/terraform/modules/thinkwork/variables.tf +353 -89
  199. package/dist/terraform/schema.graphql +69 -36
  200. package/dist/workspace-defaults/files/INSTRUCTIONS.md +9 -11
  201. package/dist/workspace-defaults/files/MEMORY_GUIDE.md +38 -35
  202. package/dist/workspace-defaults/files/ROUTER.md +1 -1
  203. package/dist/workspace-defaults/files/TOOLS.md +9 -39
  204. package/dist/workspace-defaults/files/USER.md +1 -0
  205. package/dist/workspace-defaults/files/agents/analyst/INSTRUCTIONS.md +6 -3
  206. package/dist/workspace-defaults/files/catalog-skills/brain-brief-builder/SKILL.md +165 -0
  207. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/SKILL.md +75 -0
  208. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/mcp-tooling.md +40 -0
  209. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/validation-and-handoff.md +40 -0
  210. package/dist/workspace-defaults/files/catalog-skills/n8n-workflow-operator/references/workflow-authoring.md +46 -0
  211. package/dist/workspace-defaults/files/skills/artifact-builder/references/crm-dashboard.md +1 -1
  212. package/dist/workspace-defaults/files/skills/document-composer/SKILL.md +22 -3
  213. package/package.json +3 -2
  214. package/dist/drizzle/0261_thread_turns_origin_turn_index.sql +0 -11
  215. package/dist/plugins/company-data/README.md +0 -49
  216. package/dist/plugins/company-data/package.json +0 -21
  217. package/dist/plugins/company-data/src/index.ts +0 -30
  218. package/dist/plugins/company-data/src/manifest.ts +0 -26
  219. package/dist/plugins/company-data/test/manifest.test.ts +0 -116
  220. package/dist/plugins/company-data/tsconfig.json +0 -8
  221. package/dist/plugins/company-etl/README.md +0 -46
  222. package/dist/plugins/company-etl/package.json +0 -21
  223. package/dist/plugins/company-etl/src/index.ts +0 -29
  224. package/dist/plugins/company-etl/src/manifest.ts +0 -26
  225. package/dist/plugins/company-etl/test/manifest.test.ts +0 -108
  226. package/dist/plugins/company-etl/tsconfig.json +0 -8
  227. package/dist/plugins/lastmile/README.md +0 -50
  228. package/dist/plugins/lastmile/package.json +0 -23
  229. package/dist/plugins/lastmile/smoke/lastmile-plugin-smoke.mjs +0 -661
  230. package/dist/plugins/lastmile/src/api/tasks-adapter.ts +0 -572
  231. package/dist/plugins/lastmile/src/discovery.fixture.ts +0 -64
  232. package/dist/plugins/lastmile/src/index.ts +0 -46
  233. package/dist/plugins/lastmile/src/manifest.ts +0 -139
  234. package/dist/plugins/lastmile/test/api/tasks-adapter.test.ts +0 -266
  235. package/dist/plugins/lastmile/test/discovery.test.ts +0 -88
  236. package/dist/plugins/lastmile/tsconfig.json +0 -8
  237. package/dist/plugins/n8n/smoke/n8n-managed-app-smoke.mjs +0 -409
  238. package/dist/plugins/twenty/smoke/twenty-managed-app-smoke.mjs +0 -359
  239. package/dist/plugins/workos-auth/package.json +0 -22
  240. package/dist/plugins/workos-auth/src/index.ts +0 -30
  241. package/dist/plugins/workos-auth/src/manifest.ts +0 -40
  242. package/dist/plugins/workos-auth/src/provider-contract.ts +0 -39
  243. package/dist/plugins/workos-auth/test/manifest.test.ts +0 -94
  244. package/dist/plugins/workos-auth/tsconfig.json +0 -8
  245. package/dist/terraform/modules/app/agentcore-code-interpreter/Dockerfile.sandbox-base +0 -61
  246. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/sitecustomize.py +0 -227
  247. package/dist/terraform/modules/app/agentcore-code-interpreter/sandbox/test_sitecustomize.py +0 -277
  248. package/dist/terraform/modules/app/agentcore-code-interpreter/scripts/build_and_push_sandbox_base.sh +0 -70
  249. package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -101
  250. package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -9
  251. package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -25
  252. package/dist/terraform/modules/app/hindsight-memory/README.md +0 -89
  253. package/dist/terraform/modules/app/hindsight-memory/main.tf +0 -411
  254. package/dist/terraform/modules/data/bedrock-knowledge-base/main.tf +0 -102
  255. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/outputs.tf +0 -0
  256. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/sync-database.py +0 -0
  257. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/scripts/test_sync_database.py +0 -0
  258. /package/dist/{plugins/n8n/terraform → terraform/modules/app}/n8n/variables.tf +0 -0
  259. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/README.md +0 -0
  260. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/main.tf +0 -0
  261. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/outputs.tf +0 -0
  262. /package/dist/{plugins/twenty/terraform → terraform/modules/app}/twenty/variables.tf +0 -0
@@ -1,16 +1,19 @@
1
1
  /**
2
- * Workspace-member provisioning via the invite-token flow (plan U4, KTD4).
2
+ * Resolve LastMile reps to existing Twenty workspace members (plan U4, R6).
3
3
  *
4
- * As the admin user: sendInvitations(emails) read the personal invite tokens
5
- * back signUpInWorkspace(email, password, workspaceId, token) per rep with
6
- * the shared test password (R5, value from env never in the repo). Email
7
- * delivery is irrelevant, which neutralizes TEI's SES sandbox.
4
+ * This module does NOT create members. TEI's Twenty does not serve the auth
5
+ * GraphQL schema over its ALB `sendInvitations`, `signUpInWorkspace`, and
6
+ * `getLoginTokenFromCredentials` all return "Cannot query field"so there is
7
+ * no API path to create a user. Creation lives in
8
+ * `scripts/provision-twenty-members.ts`, which writes to Twenty's Postgres
9
+ * directly (superseding decision S5 in the plan). Run it before the migration.
8
10
  *
9
- * The auth/invitation mutations live on Twenty's core schema, which requires a
10
- * logged-in user token (UserAuthGuard) the workspace API key is not enough.
11
+ * What remains here: list the workspace's members, match reps to them by email
12
+ * (case-insensitive), collapse reps that share an email onto one login, and
13
+ * emit the `repId -> workspaceMemberId` owner map.
11
14
  */
12
15
 
13
- import { TwentyClient, TwentyGraphqlError } from "./twenty-client";
16
+ import type { TwentyClient } from "./twenty-client";
14
17
 
15
18
  export interface RepToProvision {
16
19
  /** LastMile sales_rep.id — the value owner refs on CRM rows point at. */
@@ -58,97 +61,13 @@ export async function listWorkspaceMembers(
58
61
  return members;
59
62
  }
60
63
 
61
- /**
62
- * Obtain an admin user access token via password sign-in on the core schema.
63
- * Twenty's flow is getLoginTokenFromCredentials → getAuthTokensFromLoginToken.
64
- */
65
- export async function adminSignIn(options: {
66
- baseUrl: string;
67
- email: string;
68
- password: string;
69
- fetchImpl?: typeof fetch;
70
- }): Promise<{ accessToken: string; workspaceId: string | null }> {
71
- const fetchImpl = options.fetchImpl ?? fetch;
72
- const origin = options.baseUrl;
73
-
74
- async function core<T>(
75
- query: string,
76
- variables: Record<string, unknown>,
77
- ): Promise<T> {
78
- const response = await fetchImpl(`${options.baseUrl}/graphql`, {
79
- method: "POST",
80
- headers: { "content-type": "application/json" },
81
- body: JSON.stringify({ query, variables }),
82
- });
83
- const body = (await response.json()) as {
84
- data?: T;
85
- errors?: Array<{ message: string }>;
86
- };
87
- if (body.errors?.length) {
88
- throw new TwentyGraphqlError(
89
- `Twenty core auth errors: ${JSON.stringify(body.errors)}`,
90
- {
91
- errors: body.errors,
92
- },
93
- );
94
- }
95
- if (!body.data)
96
- throw new TwentyGraphqlError("Twenty core auth returned no data.");
97
- return body.data;
98
- }
99
-
100
- const loginData = await core<{
101
- getLoginTokenFromCredentials: { loginToken: { token: string } };
102
- }>(
103
- `mutation MigrationLogin($email: String!, $password: String!, $origin: String!) {
104
- getLoginTokenFromCredentials(email: $email, password: $password, origin: $origin) {
105
- loginToken { token }
106
- }
107
- }`,
108
- { email: options.email, password: options.password, origin },
109
- );
110
-
111
- const tokensData = await core<{
112
- getAuthTokensFromLoginToken: {
113
- tokens: { accessOrWorkspaceAgnosticToken: { token: string } };
114
- };
115
- }>(
116
- `mutation MigrationAuthTokens($loginToken: String!, $origin: String!) {
117
- getAuthTokensFromLoginToken(loginToken: $loginToken, origin: $origin) {
118
- tokens { accessOrWorkspaceAgnosticToken { token } }
119
- }
120
- }`,
121
- {
122
- loginToken: loginData.getLoginTokenFromCredentials.loginToken.token,
123
- origin,
124
- },
125
- );
126
-
127
- const accessToken =
128
- tokensData.getAuthTokensFromLoginToken.tokens.accessOrWorkspaceAgnosticToken
129
- .token;
130
- // The workspaceId claim rides inside the JWT payload.
131
- let workspaceId: string | null = null;
132
- try {
133
- const payload = JSON.parse(
134
- Buffer.from(accessToken.split(".")[1], "base64url").toString("utf8"),
135
- ) as { workspaceId?: string };
136
- workspaceId = payload.workspaceId ?? null;
137
- } catch {
138
- workspaceId = null;
139
- }
140
- return { accessToken, workspaceId };
141
- }
142
-
143
64
  export interface MemberProvisionReportRow {
144
65
  repId: string;
145
66
  email: string | null;
146
67
  action:
147
68
  | "existing"
148
- | "invited+signedUp"
149
69
  | "unprovisionable"
150
- | "failed"
151
- | "planned"
70
+ | "missing-member"
152
71
  | "merged-duplicate-email";
153
72
  error?: string;
154
73
  }
@@ -157,34 +76,29 @@ export interface MembersEnsureResult {
157
76
  /** LastMile rep id → Twenty workspaceMemberId, for owner resolution (R6). */
158
77
  ownerMap: Map<string, string>;
159
78
  report: MemberProvisionReportRow[];
160
- /** True when any rep failed to provision (partial-failure exit code, U4). */
79
+ /** Retained for the caller's exit-code contract; this module never fails a rep. */
161
80
  hadFailures: boolean;
81
+ /** Reps with an email but no Twenty member yet — run provision-twenty-members.ts. */
82
+ missingMembers: number;
162
83
  }
163
84
 
164
85
  export interface MembersEnsureOptions {
165
86
  dataClient: TwentyClient;
166
- /** Client authed with the ADMIN USER token; required only when applying. */
167
- adminClient: TwentyClient | null;
168
87
  reps: RepToProvision[];
169
- repPassword: string;
170
- workspaceId: string | null;
171
- dryRun: boolean;
172
88
  }
173
89
 
174
90
  export async function ensureMembers(
175
91
  options: MembersEnsureOptions,
176
92
  ): Promise<MembersEnsureResult> {
177
- const { dataClient, adminClient, reps, repPassword, workspaceId, dryRun } =
178
- options;
93
+ const { dataClient, reps } = options;
179
94
  const report: MemberProvisionReportRow[] = [];
180
95
  const ownerMap = new Map<string, string>();
181
- let hadFailures = false;
182
96
 
183
97
  // Duplicate emails across reps collapse into ONE login: email is the login
184
98
  // identity (Twenty cannot hold two members with one email), so every rep id
185
99
  // sharing the email maps to the same workspace member. Observed live: a
186
- // "house" sales_rep row reusing a real rep's email. The merge is reported
187
- // per secondary rep so it is visible, never silent.
100
+ // "house" sales_rep row reusing a real rep's email, and Sal Carrizales
101
+ // duplicated. The merge is reported per secondary rep, never silent.
188
102
  const primaryRepByEmail = new Map<string, RepToProvision>();
189
103
  const mergedReps: RepToProvision[] = [];
190
104
  for (const rep of reps) {
@@ -193,189 +107,50 @@ export async function ensureMembers(
193
107
  if (!primary) primaryRepByEmail.set(rep.email, rep);
194
108
  else mergedReps.push(rep);
195
109
  }
110
+ const mergedRepIds = new Set(mergedReps.map((rep) => rep.repId));
196
111
 
197
112
  const existingMembers = await listWorkspaceMembers(dataClient);
198
113
  const membersByEmail = new Map(
199
114
  existingMembers.map((member) => [member.userEmail.toLowerCase(), member]),
200
115
  );
201
116
 
202
- const mergedRepIds = new Set(mergedReps.map((rep) => rep.repId));
203
- const toInvite: RepToProvision[] = [];
204
117
  for (const rep of reps) {
205
118
  if (!rep.email) {
206
119
  report.push({ repId: rep.repId, email: null, action: "unprovisionable" });
207
120
  continue;
208
121
  }
209
- if (mergedRepIds.has(rep.repId)) continue; // resolved after primaries below
122
+ if (mergedRepIds.has(rep.repId)) continue; // resolved from its primary below
210
123
  const existing = membersByEmail.get(rep.email);
211
124
  if (existing) {
212
125
  ownerMap.set(rep.repId, existing.id);
213
126
  report.push({ repId: rep.repId, email: rep.email, action: "existing" });
214
127
  continue;
215
128
  }
216
- toInvite.push(rep);
217
- }
218
-
219
- const resolveMergedReps = () => {
220
- for (const rep of mergedReps) {
221
- const primary = primaryRepByEmail.get(
222
- rep.email as string,
223
- ) as RepToProvision;
224
- const memberId = ownerMap.get(primary.repId);
225
- if (memberId) ownerMap.set(rep.repId, memberId);
226
- report.push({
227
- repId: rep.repId,
228
- email: rep.email,
229
- action: "merged-duplicate-email",
230
- });
231
- }
232
- };
233
-
234
- if (dryRun) {
235
- for (const rep of toInvite) {
236
- report.push({ repId: rep.repId, email: rep.email, action: "planned" });
237
- }
238
- resolveMergedReps();
239
- return { ownerMap, report, hadFailures };
129
+ // No member for this rep: provision-twenty-members.ts has not run, or the
130
+ // rep is new since it did. Their records load with a null owner and heal on
131
+ // the next run once the member exists.
132
+ report.push({
133
+ repId: rep.repId,
134
+ email: rep.email,
135
+ action: "missing-member",
136
+ });
240
137
  }
241
138
 
242
- if (toInvite.length > 0) {
243
- if (!adminClient) {
244
- throw new Error(
245
- "Provisioning new members requires TWENTY_ADMIN_EMAIL/TWENTY_ADMIN_PASSWORD (admin user token).",
246
- );
247
- }
248
- if (!workspaceId) {
249
- throw new Error(
250
- "workspaceId could not be resolved from the admin session; set TWENTY_WORKSPACE_ID.",
251
- );
252
- }
253
-
254
- await adminClient.requestOnce(
255
- "/graphql",
256
- `mutation MigrationInvite($emails: [String!]!) {
257
- sendInvitations(emails: $emails) {
258
- success
259
- result { id email }
260
- }
261
- }`,
262
- { emails: toInvite.map((rep) => rep.email) },
263
- );
264
-
265
- const invitationsData = await adminClient.requestWithRetry<{
266
- findWorkspaceInvitations: Array<{ id: string; email: string }>;
267
- }>(
268
- "/graphql",
269
- `query MigrationInvitations {
270
- findWorkspaceInvitations { id email }
271
- }`,
272
- );
273
-
274
- // The personal invite token is read back per invitation.
275
- for (const rep of toInvite) {
276
- try {
277
- const invitation = invitationsData.findWorkspaceInvitations.find(
278
- (candidate) => candidate.email.toLowerCase() === rep.email,
279
- );
280
- if (!invitation) {
281
- throw new Error(
282
- `no invitation found for ${rep.email} after sendInvitations`,
283
- );
284
- }
285
- const tokenData = await adminClient.requestWithRetry<{
286
- getWorkspaceInvitationToken: { token: string } | string;
287
- }>(
288
- "/graphql",
289
- `query MigrationInviteToken($invitationId: String!) {
290
- getWorkspaceInvitationToken(invitationId: $invitationId)
291
- }`,
292
- { invitationId: invitation.id },
293
- );
294
- const rawToken = tokenData.getWorkspaceInvitationToken;
295
- const inviteToken =
296
- typeof rawToken === "string" ? rawToken : rawToken.token;
297
-
298
- await signUpRepInWorkspace({
299
- baseUrl: dataClient.endpoint("/graphql").replace(/\/graphql$/, ""),
300
- email: rep.email as string,
301
- password: repPassword,
302
- workspaceId,
303
- inviteToken,
304
- });
305
- // Membership materializes after signup; resolve the member id.
306
- const refreshed = await listWorkspaceMembers(dataClient);
307
- const member = refreshed.find(
308
- (candidate) => candidate.userEmail.toLowerCase() === rep.email,
309
- );
310
- if (!member)
311
- throw new Error(`member for ${rep.email} not visible after signup`);
312
- ownerMap.set(rep.repId, member.id);
313
- report.push({
314
- repId: rep.repId,
315
- email: rep.email,
316
- action: "invited+signedUp",
317
- });
318
- } catch (error) {
319
- hadFailures = true;
320
- report.push({
321
- repId: rep.repId,
322
- email: rep.email,
323
- action: "failed",
324
- error: error instanceof Error ? error.message : String(error),
325
- });
326
- }
327
- }
139
+ for (const rep of mergedReps) {
140
+ const primary = primaryRepByEmail.get(
141
+ rep.email as string,
142
+ ) as RepToProvision;
143
+ const memberId = ownerMap.get(primary.repId);
144
+ if (memberId) ownerMap.set(rep.repId, memberId);
145
+ report.push({
146
+ repId: rep.repId,
147
+ email: rep.email,
148
+ action: "merged-duplicate-email",
149
+ });
328
150
  }
329
151
 
330
- resolveMergedReps();
331
- return { ownerMap, report, hadFailures };
332
- }
333
-
334
- async function signUpRepInWorkspace(options: {
335
- baseUrl: string;
336
- email: string;
337
- password: string;
338
- workspaceId: string;
339
- inviteToken: string;
340
- fetchImpl?: typeof fetch;
341
- }): Promise<void> {
342
- const fetchImpl = options.fetchImpl ?? fetch;
343
- const response = await fetchImpl(`${options.baseUrl}/graphql`, {
344
- method: "POST",
345
- headers: { "content-type": "application/json" },
346
- body: JSON.stringify({
347
- query: `mutation MigrationSignUp(
348
- $email: String!
349
- $password: String!
350
- $workspaceId: String!
351
- $workspacePersonalInviteToken: String
352
- ) {
353
- signUpInWorkspace(
354
- email: $email
355
- password: $password
356
- workspaceId: { id: $workspaceId }
357
- workspacePersonalInviteToken: $workspacePersonalInviteToken
358
- ) {
359
- loginToken { token }
360
- }
361
- }`,
362
- variables: {
363
- email: options.email,
364
- password: options.password,
365
- workspaceId: options.workspaceId,
366
- workspacePersonalInviteToken: options.inviteToken,
367
- },
368
- }),
369
- });
370
- const body = (await response.json()) as {
371
- errors?: Array<{ message: string }>;
372
- };
373
- if (body.errors?.length) {
374
- throw new TwentyGraphqlError(
375
- `signUpInWorkspace failed: ${JSON.stringify(body.errors)}`,
376
- {
377
- errors: body.errors,
378
- },
379
- );
380
- }
152
+ const missing = report.filter(
153
+ (row) => row.action === "missing-member",
154
+ ).length;
155
+ return { ownerMap, report, hadFailures: false, missingMembers: missing };
381
156
  }
@@ -181,6 +181,63 @@ export async function provisionMembers(
181
181
  };
182
182
  }
183
183
 
184
+ export interface RotateResult {
185
+ rotated: number;
186
+ notFound: string[];
187
+ }
188
+
189
+ /**
190
+ * Rotate the shared validation-window password (plan R5). Every rep account
191
+ * this migration created gets a fresh bcrypt hash; nothing else is touched.
192
+ *
193
+ * Required on cutover, on abort, and on an over-long validation window alike —
194
+ * the shared credential must never outlive the phase that needed it. Pass a
195
+ * per-rep password map to give each rep a distinct one, or a single password to
196
+ * re-share (still shared: prefer the map, or follow with a forced reset).
197
+ */
198
+ export async function rotateMemberPasswords(options: {
199
+ databaseUrl: string;
200
+ /** Emails of the accounts to rotate — the migration's provisioned reps. */
201
+ emails: readonly string[];
202
+ passwordFor: (email: string) => string;
203
+ dryRun: boolean;
204
+ log?: (message: string) => void;
205
+ }): Promise<RotateResult> {
206
+ const log = options.log ?? (() => {});
207
+ const pool = new pg.Pool({ connectionString: options.databaseUrl, max: 2 });
208
+ const notFound: string[] = [];
209
+ let rotated = 0;
210
+ try {
211
+ for (const rawEmail of options.emails) {
212
+ const email = rawEmail.toLowerCase();
213
+ const existing = await pool.query<{ id: string }>(
214
+ `select id from core."user" where lower(email) = $1 and "deletedAt" is null`,
215
+ [email],
216
+ );
217
+ if (existing.rows.length === 0) {
218
+ notFound.push(email);
219
+ continue;
220
+ }
221
+ if (options.dryRun) {
222
+ rotated += 1;
223
+ continue;
224
+ }
225
+ // Hash per user: rotation is the moment the shared secret dies, so the
226
+ // salts must not be shared either.
227
+ const hash = hashPassword(options.passwordFor(email));
228
+ await pool.query(
229
+ `update core."user" set "passwordHash" = $2, "updatedAt" = now() where id = $1`,
230
+ [existing.rows[0].id, hash],
231
+ );
232
+ rotated += 1;
233
+ log(`rotated ${email}`);
234
+ }
235
+ } finally {
236
+ await pool.end();
237
+ }
238
+ return { rotated, notFound };
239
+ }
240
+
184
241
  /** Workspace schema names are `workspace_<id>`; reject anything else rather
185
242
  * than interpolate an arbitrary identifier into SQL. */
186
243
  export function quoteIdent(schema: string): string {