thinkwork-cli 0.13.2 → 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-JYZXYQJR.js → api-client-4P4JZOBX.js} +1 -1
- package/dist/{chunk-Z2F6RJFR.js → chunk-4OMDWYZQ.js} +4 -5
- package/dist/{chunk-ILZKG5WV.js → chunk-AD2NIWB5.js} +434 -69
- package/dist/chunk-MWQ7RO76.js +137 -0
- package/dist/{chunk-DVPBI5HM.js → chunk-OLUQ4U2Z.js} +26 -26
- package/dist/{chunk-QOERT44A.js → chunk-YQCYD6NE.js} +57 -44
- package/dist/cli.js +4400 -16431
- 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-CRV46D5B.js → dist-es-2OTNTL4N.js} +4 -4
- package/dist/{dist-es-ENBUC3JP.js → dist-es-2XVAH4BQ.js} +2 -2
- package/dist/{dist-es-DCY4IC3Y.js → dist-es-3DPDUJAH.js} +2 -2
- package/dist/{dist-es-G7PTG75M.js → dist-es-BNZTBBVC.js} +1 -1
- package/dist/{dist-es-GKDBE7E6.js → dist-es-CD2RBDAC.js} +1 -1
- package/dist/{dist-es-R7FGL2IB.js → dist-es-I4FW57PS.js} +1 -1
- package/dist/{dist-es-USORBXZ5.js → dist-es-KVFWJNDY.js} +4 -4
- package/dist/{dist-es-HY3535OI.js → dist-es-PS73DWNL.js} +2 -2
- package/dist/{dist-es-LDE437AM.js → dist-es-S3NZQFGT.js} +1 -1
- package/dist/{dist-es-G54LBMRN.js → dist-es-SGCA4P32.js} +1 -1
- package/dist/{dist-es-Q57SZXZ7.js → dist-es-WBM6J4RA.js} +2 -2
- package/dist/{dist-es-P26FWFLT.js → dist-es-ZGVADYIF.js} +8 -8
- package/dist/drizzle/0227_analyst_reader_role.sql +17 -26
- package/dist/drizzle/0230_analyst_rls.sql +28 -45
- package/dist/drizzle/0261_native_auth_control_plane.sql +7 -3
- 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/{loadSso-DJVB7BXE.js → loadSso-ETCXTR4I.js} +2 -2
- package/dist/{loadSso-JAMNA3WY.js → loadSso-UBD7KUHE.js} +2 -2
- package/dist/plugins/README.md +2 -2
- package/dist/plugins/catalog/package.json +0 -3
- package/dist/plugins/catalog/src/__tests__/build-catalog.test.ts +6 -3
- package/dist/plugins/catalog/src/__tests__/catalog.test.ts +21 -21
- package/dist/plugins/catalog/src/__tests__/contracts.test.ts +102 -25
- package/dist/plugins/catalog/src/__tests__/plugin-package.test.ts +7 -6
- package/dist/plugins/catalog/src/__tests__/plugin-registry.test.ts +0 -15
- package/dist/plugins/catalog/src/contracts.ts +15 -0
- package/dist/plugins/catalog/src/registry/generated-first-party.ts +0 -18
- 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-UGDWTGS4.js → signin-5QZNRSOO.js} +2 -2
- package/dist/{sso-oidc-Q4VXANXY.js → sso-oidc-XK673P54.js} +2 -2
- package/dist/{sts-CWLSE3GH.js → sts-OXI2UALF.js} +6 -134
- package/dist/{sts-DNZLNVOV.js → sts-SQW4NBK3.js} +6 -134
- package/dist/terraform/examples/greenfield/main.tf +233 -175
- package/dist/terraform/examples/greenfield/terraform.tfvars.example +12 -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 +106 -26
- package/dist/terraform/modules/app/agentcore-identity/outputs.tf +6 -6
- package/dist/terraform/modules/app/agentcore-identity/scripts/bootstrap_twenty_oauth_client.sh +216 -99
- package/dist/terraform/modules/app/agentcore-identity/scripts/delete_identity.sh +6 -9
- 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 +22 -7
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_identity.sh +17 -46
- package/dist/terraform/modules/app/agentcore-identity/scripts/reconcile_twenty_provider.mjs +65 -3
- 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 +6 -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/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 +2641 -124
- package/dist/terraform/modules/app/deployment-control-plane/test_runner_bundle.py +2034 -154
- 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 +411 -723
- package/dist/terraform/modules/app/lambda-api/iam-grouped.tf +262 -198
- package/dist/terraform/modules/app/lambda-api/main.tf +51 -13
- package/dist/terraform/modules/app/lambda-api/mcp-oauth.tf +4 -181
- 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 -45
- package/dist/terraform/modules/app/lambda-api/turn-assertion.tf +45 -0
- package/dist/terraform/modules/app/lambda-api/variables.tf +219 -126
- 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 +81 -8
- package/dist/terraform/modules/foundation/cognito/outputs.tf +5 -0
- package/dist/terraform/modules/foundation/cognito/variables.tf +24 -12
- package/dist/terraform/modules/thinkwork/README.md +5 -1
- package/dist/terraform/modules/thinkwork/main.tf +376 -374
- package/dist/terraform/modules/thinkwork/outputs.tf +12 -158
- package/dist/terraform/modules/thinkwork/variables.tf +246 -89
- 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/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 +1 -1
- package/dist/drizzle/0261_managed_harness_multiplayer.sql +0 -313
- package/dist/drizzle/0262_parallel_harness_threads.sql +0 -9
- package/dist/drizzle/0264_harness_tool_execution_ledger.sql +0 -113
- 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/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-gateway/main.tf +0 -305
- package/dist/terraform/modules/app/agentcore-gateway/outputs.tf +0 -44
- package/dist/terraform/modules/app/agentcore-gateway/scripts/delete_gateway.sh +0 -93
- package/dist/terraform/modules/app/agentcore-gateway/scripts/read_gateway.sh +0 -33
- package/dist/terraform/modules/app/agentcore-gateway/scripts/reconcile_gateway.sh +0 -543
- package/dist/terraform/modules/app/agentcore-gateway/variables.tf +0 -70
- package/dist/terraform/modules/app/agentcore-harness/main.tf +0 -330
- package/dist/terraform/modules/app/agentcore-harness/outputs.tf +0 -72
- package/dist/terraform/modules/app/agentcore-harness/scripts/delete_harness.sh +0 -5
- package/dist/terraform/modules/app/agentcore-harness/scripts/harness-lifecycle.mjs +0 -430
- package/dist/terraform/modules/app/agentcore-harness/scripts/harness-readback.mjs +0 -180
- package/dist/terraform/modules/app/agentcore-harness/scripts/harness-tool-contract.mjs +0 -193
- package/dist/terraform/modules/app/agentcore-harness/scripts/prune_harness.sh +0 -5
- package/dist/terraform/modules/app/agentcore-harness/scripts/read_harness.sh +0 -5
- package/dist/terraform/modules/app/agentcore-harness/scripts/reconcile_harness.sh +0 -5
- package/dist/terraform/modules/app/agentcore-harness/variables.tf +0 -96
- 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/app/lambda-api/agentcore-harness-alarms.tf +0 -88
- package/dist/terraform/modules/app/lambda-api/agentcore-identity-alarms.tf +0 -91
- 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
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Resolve LastMile reps to existing Twenty workspace members (plan U4, R6).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
-
| "
|
|
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
|
-
/**
|
|
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,
|
|
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
|
|
187
|
-
//
|
|
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
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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 {
|