@xfey/tutti 0.1.119 → 0.1.121

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 (113) hide show
  1. package/dist/approvals/decision-command.d.ts +2 -2
  2. package/dist/collaboration-state/clarification-snapshot.d.ts +9 -9
  3. package/dist/collaboration-state/task-compile-context.d.ts +1 -1
  4. package/dist/control-plane/workflows/openai.d.ts +3 -4
  5. package/dist/control-plane/workflows/openai.js +3 -2
  6. package/dist/providers/openai/app-server/invocation-coordinator.d.ts +1 -0
  7. package/dist/providers/openai/app-server/invocation-coordinator.js +4 -1
  8. package/dist/providers/openai/app-server/invocation-deadline.d.ts +3 -0
  9. package/dist/providers/openai/app-server/invocation-deadline.js +15 -0
  10. package/dist/providers/openai/app-server/read-only-procedure.d.ts +4 -2
  11. package/dist/providers/openai/app-server/read-only-procedure.js +16 -0
  12. package/dist/providers/openai/app-server/workspace-write-run.d.ts +4 -2
  13. package/dist/providers/openai/app-server/workspace-write-run.js +16 -0
  14. package/dist/providers/openai/chat-assistant.d.ts +3 -4
  15. package/dist/providers/openai/chat-assistant.js +2 -1
  16. package/dist/providers/openai/execution-access.d.ts +17 -0
  17. package/dist/providers/openai/execution-access.js +13 -0
  18. package/dist/providers/openai/sdk-procedure-runner.d.ts +2 -2
  19. package/dist/providers/openai/sdk-procedure-runner.js +17 -0
  20. package/dist/run-pipeline/artifact-applicability.d.ts +3 -4
  21. package/dist/run-pipeline/artifact-applicability.js +2 -1
  22. package/dist/run-pipeline/openai.d.ts +3 -4
  23. package/dist/run-pipeline/openai.js +3 -2
  24. package/dist/run-pipeline/task-run-invocation.js +6 -12
  25. package/dist/server-shell/cli/cli.js +3 -0
  26. package/dist/server-shell/cli/host-lifecycle.js +1 -0
  27. package/dist/server-shell/cli/host-server-runtime.d.ts +1 -0
  28. package/dist/server-shell/cli/host-server-runtime.js +83 -34
  29. package/dist/server-shell/cli/launch-command.js +1 -1
  30. package/dist/server-shell/cli/launch.d.ts +7 -3
  31. package/dist/server-shell/cli/launch.js +23 -4
  32. package/dist/server-shell/cli/machine-local.d.ts +9 -0
  33. package/dist/server-shell/cli/machine-local.js +60 -6
  34. package/dist/server-shell/cli/project-resolver.d.ts +2 -2
  35. package/dist/server-shell/cli/project-resolver.js +6 -3
  36. package/dist/server-shell/cli/relay-registration.js +38 -1
  37. package/dist/server-shell/http/routes/local-control.d.ts +1 -1
  38. package/dist/server-shell/http/routes/local-control.js +2 -46
  39. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +45 -137
  40. package/dist/server-shell/http/routes/project-api/openapi.d.ts +45 -137
  41. package/dist/server-shell/http/routes/project-api/schemas.d.ts +24 -136
  42. package/dist/server-shell/http/routes/project-api/schemas.js +4 -58
  43. package/dist/server-shell/http/routes/project-api/types.d.ts +3 -0
  44. package/dist/server-shell/local-console/managed-project-routes.d.ts +9 -0
  45. package/dist/server-shell/local-console/managed-project-routes.js +120 -0
  46. package/dist/server-shell/local-console/managed-project-service.d.ts +141 -0
  47. package/dist/server-shell/local-console/managed-project-service.js +158 -0
  48. package/dist/server-shell/local-console/operation-coordinator.d.ts +1 -1
  49. package/dist/server-shell/local-console/package-update-hosts.js +2 -1
  50. package/dist/server-shell/local-console/project-service.d.ts +4 -1
  51. package/dist/server-shell/local-console/project-service.js +62 -14
  52. package/dist/server-shell/local-console/server.js +22 -2
  53. package/dist/server-shell/managed-access/execution-binding.d.ts +9 -0
  54. package/dist/server-shell/managed-access/execution-binding.js +22 -0
  55. package/dist/server-shell/managed-access/existing-project.d.ts +15 -0
  56. package/dist/server-shell/managed-access/existing-project.js +41 -0
  57. package/dist/server-shell/managed-access/legacy-records.d.ts +2 -0
  58. package/dist/server-shell/managed-access/legacy-records.js +24 -0
  59. package/dist/server-shell/managed-access/model-control-client.d.ts +65 -0
  60. package/dist/server-shell/managed-access/model-control-client.js +112 -0
  61. package/dist/server-shell/managed-access/private-store.d.ts +19 -0
  62. package/dist/server-shell/managed-access/private-store.js +275 -0
  63. package/dist/server-shell/managed-access/project-provider.d.ts +18 -0
  64. package/dist/server-shell/managed-access/project-provider.js +30 -0
  65. package/dist/server-shell/managed-access/records.d.ts +103 -0
  66. package/dist/server-shell/managed-access/records.js +74 -0
  67. package/dist/server-shell/managed-access/runners.d.ts +11 -0
  68. package/dist/server-shell/managed-access/runners.js +103 -0
  69. package/dist/server-shell/managed-access/runtime-records.d.ts +106 -0
  70. package/dist/server-shell/managed-access/runtime-records.js +65 -0
  71. package/dist/server-shell/managed-access/runtime.d.ts +48 -0
  72. package/dist/server-shell/managed-access/runtime.js +300 -0
  73. package/dist/server-shell/managed-access/state.d.ts +53 -0
  74. package/dist/server-shell/managed-access/state.js +292 -0
  75. package/dist/server-shell/managed-access/transport.d.ts +45 -0
  76. package/dist/server-shell/managed-access/transport.js +39 -0
  77. package/dist/server-shell/managed-access/workspace.d.ts +10 -0
  78. package/dist/server-shell/managed-access/workspace.js +69 -0
  79. package/dist/server-shell/smoke/e2e-client.d.ts +8 -3
  80. package/dist/server-shell/smoke/e2e-client.js +11 -11
  81. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +4 -1
  82. package/node_modules/@tutti/relay-client/dist/host-control.js +26 -3
  83. package/node_modules/@tutti/relay-client/dist/index.d.ts +1 -0
  84. package/node_modules/@tutti/relay-client/dist/index.js +1 -0
  85. package/node_modules/@tutti/relay-client/dist/project-authority.d.ts +70 -0
  86. package/node_modules/@tutti/relay-client/dist/project-authority.js +139 -0
  87. package/node_modules/@tutti/shared/dist/ids/index.d.ts +7 -0
  88. package/node_modules/@tutti/shared/dist/ids/index.js +7 -0
  89. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -0
  90. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -0
  91. package/node_modules/@tutti/shared/dist/schemas/api/managed-account.d.ts +34 -0
  92. package/node_modules/@tutti/shared/dist/schemas/api/managed-account.js +40 -0
  93. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.d.ts +7 -4
  94. package/node_modules/@tutti/shared/dist/schemas/api/provider-config.js +5 -0
  95. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +3 -0
  96. package/node_modules/@tutti/shared/dist/schemas/internal/creation-validation.d.ts +26 -0
  97. package/node_modules/@tutti/shared/dist/schemas/internal/creation-validation.js +40 -0
  98. package/node_modules/@tutti/shared/dist/schemas/internal/index.d.ts +5 -0
  99. package/node_modules/@tutti/shared/dist/schemas/internal/index.js +5 -0
  100. package/node_modules/@tutti/shared/dist/schemas/internal/managed-account.d.ts +12 -0
  101. package/node_modules/@tutti/shared/dist/schemas/internal/managed-account.js +13 -0
  102. package/node_modules/@tutti/shared/dist/schemas/internal/managed-models.d.ts +17 -0
  103. package/node_modules/@tutti/shared/dist/schemas/internal/managed-models.js +17 -0
  104. package/node_modules/@tutti/shared/dist/schemas/internal/managed-runtime.d.ts +113 -0
  105. package/node_modules/@tutti/shared/dist/schemas/internal/managed-runtime.js +84 -0
  106. package/node_modules/@tutti/shared/dist/schemas/internal/project-authority.d.ts +114 -0
  107. package/node_modules/@tutti/shared/dist/schemas/internal/project-authority.js +102 -0
  108. package/package.json +1 -1
  109. package/web/assets/{homepage-motion-scene-B6lCCHBq.js → homepage-motion-scene-WfLyzcCG.js} +1 -1
  110. package/web/assets/{index-DsYi7AhN.css → index-ATsYiBZs.css} +1 -1
  111. package/web/assets/index-o9YCtzEd.js +70 -0
  112. package/web/index.html +2 -2
  113. package/web/assets/index-DepsYMLl.js +0 -70
@@ -0,0 +1,292 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { existsSync, realpathSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { Value } from "@sinclair/typebox/value";
5
+ import { createIdempotencyKey, createPrefixedId, createProjectId, ID_PREFIXES, isPrefixedId, } from "@tutti/shared/ids";
6
+ import { HostDelegationProjectionSchema, PROJECT_TRIAL_POLICY, } from "@tutti/shared/schemas/internal";
7
+ import { ensureManagedPrivateRoot, isWithin, ManagedLocalStateError, ManagedPrivateStore, } from "./private-store.js";
8
+ import { parseInstallation, parseJournal, NativeProjectPointerSchema, } from "./records.js";
9
+ const DRAFT_TTL_MS = 10 * 60_000;
10
+ const secret = () => randomBytes(32).toString("base64url");
11
+ const hash = (value) => `sha256:${createHash("sha256").update(value).digest("hex")}`;
12
+ export function managedRelayOrigin(input) {
13
+ try {
14
+ const url = new URL(input);
15
+ if (url.username ||
16
+ url.password ||
17
+ url.search ||
18
+ url.hash ||
19
+ url.pathname !== "/" ||
20
+ !(url.protocol === "https:" ||
21
+ (url.protocol === "http:" && ["127.0.0.1", "[::1]"].includes(url.hostname))))
22
+ throw new Error();
23
+ return url.origin;
24
+ }
25
+ catch {
26
+ throw new ManagedLocalStateError("managed_relay_origin_invalid");
27
+ }
28
+ }
29
+ function originDirectory(tuttiHome, relayUrl) {
30
+ return join(tuttiHome, "credentials", "managed", createHash("sha256").update(managedRelayOrigin(relayUrl)).digest("hex"));
31
+ }
32
+ // BYOK reads do not create a managed installation, journal or mutex.
33
+ export function findNativeManagedProject(options) {
34
+ if (!isPrefixedId(options.projectId, ID_PREFIXES.project))
35
+ throw new ManagedLocalStateError("managed_project_id_invalid");
36
+ let origin;
37
+ try {
38
+ origin = new URL(options.relayUrl).origin;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ if (!existsSync(join(originDirectory(options.tuttiHome, origin), `project-${options.projectId}.json`)))
44
+ return null;
45
+ const state = new NativeManagedState(options);
46
+ return { state, journal: state.project(options.projectId) };
47
+ }
48
+ export class NativeManagedState {
49
+ origin;
50
+ files;
51
+ constructor(options) {
52
+ this.origin = managedRelayOrigin(options.relayUrl);
53
+ this.files = new ManagedPrivateStore(ensureManagedPrivateRoot(options.tuttiHome, createHash("sha256").update(this.origin).digest("hex")));
54
+ }
55
+ installation(now = new Date()) {
56
+ return this.files.transaction(() => {
57
+ const saved = this.files.read("installation.json");
58
+ if (saved !== null)
59
+ return parseInstallation(saved, this.origin);
60
+ const value = {
61
+ version: 1,
62
+ relay_origin: this.origin,
63
+ installation_ref: createPrefixedId(ID_PREFIXES.hostInstallation),
64
+ secret: secret(),
65
+ created_at: now.toISOString(),
66
+ };
67
+ this.files.write("installation.json", value, true);
68
+ return parseInstallation(this.files.read("installation.json"), this.origin);
69
+ });
70
+ }
71
+ createDraft(options) {
72
+ const now = options.now ?? new Date();
73
+ let workspace;
74
+ try {
75
+ workspace = realpathSync(options.workspaceRoot);
76
+ }
77
+ catch {
78
+ throw new ManagedLocalStateError("managed_workspace_unavailable");
79
+ }
80
+ if (isWithin(workspace, this.files.root) || isWithin(this.files.root, workspace))
81
+ throw new ManagedLocalStateError("managed_state_in_workspace");
82
+ const installation = this.installation(now);
83
+ const requestRef = options.requestRef ?? createIdempotencyKey();
84
+ const name = this.journalName(requestRef);
85
+ return this.files.transaction(() => {
86
+ const saved = this.files.read(name);
87
+ if (saved !== null) {
88
+ const record = this.readJournal(requestRef);
89
+ if (record.workspace_root !== workspace ||
90
+ record.display_name !== options.displayName ||
91
+ record.description !== options.description ||
92
+ record.surface !== options.surface ||
93
+ (options.projectId !== undefined && record.project_id !== options.projectId) ||
94
+ record.existing_relay_project_ref !== options.existing?.relayProjectRef ||
95
+ record.previous_request_ref !== options.existing?.previousRequestRef ||
96
+ (options.existing !== undefined &&
97
+ record.host_registration_secret !== options.existing.hostSecret) ||
98
+ record.installation_ref !== installation.installation_ref)
99
+ throw new ManagedLocalStateError("managed_creation_mismatch");
100
+ return record;
101
+ }
102
+ const record = parseJournal({
103
+ version: 2,
104
+ request_ref: requestRef,
105
+ project_id: options.projectId ?? createProjectId(),
106
+ relay_origin: this.origin,
107
+ installation_ref: installation.installation_ref,
108
+ workspace_root: workspace,
109
+ display_name: options.displayName,
110
+ ...(options.description === undefined ? {} : { description: options.description }),
111
+ surface: options.surface,
112
+ ...(options.existing === undefined
113
+ ? {}
114
+ : {
115
+ existing_relay_project_ref: options.existing.relayProjectRef,
116
+ ...(options.existing.previousRequestRef === undefined
117
+ ? {}
118
+ : { previous_request_ref: options.existing.previousRequestRef }),
119
+ }),
120
+ handoff_token: secret(),
121
+ host_registration_secret: options.existing?.hostSecret ?? secret(),
122
+ created_at: now.toISOString(),
123
+ expires_at: new Date(now.getTime() + DRAFT_TTL_MS).toISOString(),
124
+ state: "awaiting_authorization",
125
+ }, this.origin, requestRef);
126
+ this.files.write(name, record, true);
127
+ return record;
128
+ });
129
+ }
130
+ read(requestRef) {
131
+ return this.files.transaction(() => this.readJournal(requestRef));
132
+ }
133
+ // Called only inside the private-store mutex. Upgrade metadata atomically;
134
+ // never rotate proofs, extend consent or restore revoked runtime credentials.
135
+ readJournal(requestRef) {
136
+ const name = this.journalName(requestRef);
137
+ const saved = this.files.read(name);
138
+ const row = parseJournal(saved, this.origin, requestRef);
139
+ if (saved && typeof saved === "object" && "version" in saved && saved.version === 1)
140
+ this.files.write(name, row);
141
+ return row;
142
+ }
143
+ attachProject(requestRef, now = new Date()) {
144
+ this.bootstrap(requestRef, now);
145
+ return this.files.transaction(() => {
146
+ const row = this.readJournal(requestRef);
147
+ const name = `project-${row.project_id}.json`;
148
+ const old = this.files.read(name);
149
+ if (old !== null &&
150
+ (!Value.Check(NativeProjectPointerSchema, old) ||
151
+ old.project_id !== row.project_id ||
152
+ (old.request_ref !== requestRef && old.request_ref !== row.previous_request_ref)))
153
+ throw new ManagedLocalStateError("managed_project_binding_conflict");
154
+ if (old === null ||
155
+ (Value.Check(NativeProjectPointerSchema, old) && old.request_ref !== requestRef))
156
+ this.files.write(name, { version: 1, project_id: row.project_id, request_ref: requestRef }, old === null);
157
+ return row;
158
+ });
159
+ }
160
+ project(projectId) {
161
+ if (!isPrefixedId(projectId, ID_PREFIXES.project))
162
+ throw new ManagedLocalStateError("managed_project_id_invalid");
163
+ return this.files.transaction(() => {
164
+ const pointer = this.files.read(`project-${projectId}.json`);
165
+ if (!Value.Check(NativeProjectPointerSchema, pointer) || pointer.project_id !== projectId)
166
+ throw new ManagedLocalStateError("managed_project_binding_invalid");
167
+ const row = this.readJournal(pointer.request_ref);
168
+ if (row.project_id !== projectId || (row.state !== "validated" && row.state !== "registered"))
169
+ throw new ManagedLocalStateError("managed_project_binding_invalid");
170
+ return row;
171
+ });
172
+ }
173
+ preparationBody(requestRef) {
174
+ const row = this.read(requestRef), installation = this.installation();
175
+ if (row.installation_ref !== installation.installation_ref)
176
+ throw new ManagedLocalStateError("managed_installation_changed");
177
+ return {
178
+ request_ref: row.request_ref,
179
+ project_id: row.project_id,
180
+ surface: row.surface,
181
+ ...(row.existing_relay_project_ref === undefined
182
+ ? {}
183
+ : { existing_relay_project_ref: row.existing_relay_project_ref }),
184
+ installation_ref: row.installation_ref,
185
+ installation_secret_hash: hash(installation.secret),
186
+ handoff_token_hash: hash(row.handoff_token),
187
+ host_registration_secret_hash: hash(row.host_registration_secret),
188
+ trial_consent: PROJECT_TRIAL_POLICY,
189
+ expires_before: row.expires_at,
190
+ };
191
+ }
192
+ proof(requestRef) {
193
+ const row = this.read(requestRef), installation = this.installation();
194
+ if (row.installation_ref !== installation.installation_ref)
195
+ throw new ManagedLocalStateError("managed_installation_changed");
196
+ return {
197
+ request_ref: row.request_ref,
198
+ project_id: row.project_id,
199
+ installation_ref: row.installation_ref,
200
+ installation_secret: installation.secret,
201
+ handoff_token: row.handoff_token,
202
+ host_registration_secret: row.host_registration_secret,
203
+ };
204
+ }
205
+ // Only native, Relay-verified responses may call these transitions. Browser input is not evidence.
206
+ prepared(requestRef, receipt, now = new Date()) {
207
+ return this.update(requestRef, (row) => {
208
+ if (row.project_id !== receipt.project_id ||
209
+ row.installation_ref !== receipt.installation_ref ||
210
+ row.surface !== receipt.surface ||
211
+ !Number.isFinite(Date.parse(receipt.expires_at)) ||
212
+ Date.parse(receipt.expires_at) > Date.parse(row.expires_at))
213
+ throw new ManagedLocalStateError("managed_preparation_mismatch");
214
+ if (row.state !== "awaiting_authorization") {
215
+ if (row.creation_ref !== receipt.creation_ref)
216
+ throw new ManagedLocalStateError("managed_preparation_mismatch");
217
+ return row;
218
+ }
219
+ if (receipt.expires_at <= now.toISOString())
220
+ throw new ManagedLocalStateError("managed_preparation_expired");
221
+ return {
222
+ ...row,
223
+ state: "prepared",
224
+ creation_ref: receipt.creation_ref,
225
+ expires_at: receipt.expires_at,
226
+ };
227
+ });
228
+ }
229
+ validation(requestRef, outcome) {
230
+ return this.update(requestRef, (row) => {
231
+ if (row.state === "registered" || row.state === "validated")
232
+ return row;
233
+ if (row.state === "validation_failed") {
234
+ if (outcome !== row.state)
235
+ throw new ManagedLocalStateError("managed_validation_conflict");
236
+ return row;
237
+ }
238
+ if (row.state !== "prepared" && row.state !== "validation_unknown")
239
+ throw new ManagedLocalStateError("managed_creation_not_prepared");
240
+ return { ...row, state: outcome };
241
+ });
242
+ }
243
+ bootstrap(requestRef, now = new Date()) {
244
+ const row = this.read(requestRef), installation = this.installation();
245
+ if ((row.state !== "validated" && row.state !== "registered") ||
246
+ (row.state !== "registered" && row.expires_at <= now.toISOString()))
247
+ throw new ManagedLocalStateError("managed_creation_not_validated");
248
+ if (row.installation_ref !== installation.installation_ref)
249
+ throw new ManagedLocalStateError("managed_installation_changed");
250
+ return {
251
+ kind: "managed_project_authority_v2",
252
+ creation_ref: row.creation_ref,
253
+ installation_ref: installation.installation_ref,
254
+ installation_secret: installation.secret,
255
+ handoff_token: row.handoff_token,
256
+ };
257
+ }
258
+ registered(requestRef, authority) {
259
+ return this.update(requestRef, (row) => {
260
+ if (!Value.Check(HostDelegationProjectionSchema, authority) ||
261
+ authority.installation_ref !== row.installation_ref ||
262
+ (row.existing_relay_project_ref !== undefined &&
263
+ authority.relay_project_ref !== row.existing_relay_project_ref))
264
+ throw new ManagedLocalStateError("managed_registration_mismatch");
265
+ if (row.state === "registered") {
266
+ if (row.authority.delegation_ref !== authority.delegation_ref ||
267
+ row.authority.grant_ref !== authority.grant_ref ||
268
+ row.authority.relay_project_ref !== authority.relay_project_ref)
269
+ throw new ManagedLocalStateError("managed_registration_mismatch");
270
+ return row;
271
+ }
272
+ if (row.state !== "validated")
273
+ throw new ManagedLocalStateError("managed_creation_not_validated");
274
+ return { ...row, state: "registered", authority };
275
+ });
276
+ }
277
+ update(requestRef, change) {
278
+ return this.files.transaction(() => {
279
+ const name = this.journalName(requestRef);
280
+ const row = parseJournal(this.files.read(name), this.origin, requestRef);
281
+ const updated = parseJournal(change(row), this.origin, requestRef);
282
+ this.files.write(name, updated);
283
+ return updated;
284
+ });
285
+ }
286
+ journalName(requestRef) {
287
+ if (!isPrefixedId(requestRef, ID_PREFIXES.idempotency))
288
+ throw new ManagedLocalStateError("managed_request_ref_invalid");
289
+ return `${requestRef}.json`;
290
+ }
291
+ }
292
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1,45 @@
1
+ import type { NativeManagedState } from "./state.js";
2
+ export declare function reconcileNativePreparation(options: {
3
+ state: NativeManagedState;
4
+ requestRef: string;
5
+ fetchImpl?: typeof fetch;
6
+ now?: Date;
7
+ signal?: AbortSignal;
8
+ }): Promise<{
9
+ authority?: {
10
+ state: "active";
11
+ delegation_ref: string;
12
+ grant_ref: string;
13
+ installation_ref: string;
14
+ relay_project_ref: string;
15
+ expires_at: null;
16
+ funding_kind: "trial";
17
+ model_access: "runtime_authority_required" | "not_enabled";
18
+ };
19
+ account_ref: string;
20
+ request_ref: string;
21
+ creation: {
22
+ relay_project_ref?: string;
23
+ state: "revoked" | "expired" | "prepared" | "activated";
24
+ installation_ref: string;
25
+ surface: "standalone" | "feishu";
26
+ expires_at: string;
27
+ project_id: string;
28
+ creation_ref: string;
29
+ };
30
+ }>;
31
+ export declare function validateNativePreparation(options: {
32
+ state: NativeManagedState;
33
+ requestRef: string;
34
+ fetchImpl?: typeof fetch;
35
+ signal?: AbortSignal;
36
+ }): Promise<{
37
+ state: "failed" | "unknown" | "started" | "succeeded";
38
+ installation_ref: string;
39
+ reason_code: "validation_started" | "validation_succeeded" | "validation_route_unavailable" | "validation_http_rejected" | "validation_response_invalid" | "validation_transport_unknown" | "validation_interrupted";
40
+ project_id: string;
41
+ creation_ref: string;
42
+ route_ref: string;
43
+ http_status: number | null;
44
+ }>;
45
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { readNativeCreationAuthority, validateNativeCreationAuthority } from "@tutti/relay-client";
2
+ import { ManagedLocalStateError } from "./private-store.js";
3
+ // The browser never supplies a trusted receipt or account identity to the native journal.
4
+ export async function reconcileNativePreparation(options) {
5
+ const result = await readNativeCreationAuthority({
6
+ relayUrl: options.state.origin,
7
+ body: options.state.proof(options.requestRef),
8
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
9
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
10
+ });
11
+ if (result.creation.state === "expired" || result.creation.state === "revoked")
12
+ throw new ManagedLocalStateError(`managed_preparation_${result.creation.state}`);
13
+ options.state.prepared(options.requestRef, result.creation, options.now ?? new Date());
14
+ if (result.authority !== undefined)
15
+ options.state.registered(options.requestRef, result.authority);
16
+ return result;
17
+ }
18
+ export async function validateNativePreparation(options) {
19
+ const result = await validateNativeCreationAuthority({
20
+ relayUrl: options.state.origin,
21
+ body: options.state.proof(options.requestRef),
22
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
23
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
24
+ });
25
+ const row = options.state.read(options.requestRef);
26
+ if (!("creation_ref" in row) ||
27
+ row.creation_ref !== result.creation_ref ||
28
+ row.project_id !== result.project_id ||
29
+ row.installation_ref !== result.installation_ref)
30
+ throw new ManagedLocalStateError("managed_validation_mismatch");
31
+ if (result.state === "succeeded")
32
+ options.state.validation(options.requestRef, "validated");
33
+ else if (result.state === "unknown" || result.state === "started")
34
+ options.state.validation(options.requestRef, "validation_unknown");
35
+ else
36
+ options.state.validation(options.requestRef, "validation_failed");
37
+ return result;
38
+ }
39
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1,10 @@
1
+ import type { NativeManagedState } from "./state.js";
2
+ export declare function acquireManagedWorkspaceLease(tuttiHome: string, workspaceRoot: string): () => void;
3
+ export declare function materializeManagedWorkspace(options: {
4
+ state: NativeManagedState;
5
+ requestRef: string;
6
+ tuttiHome: string;
7
+ environment: NodeJS.ProcessEnv;
8
+ now?: () => Date;
9
+ }): Promise<import("../cli/launch.js").LaunchPreparationResult>;
10
+ //# sourceMappingURL=workspace.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { createHash } from "node:crypto";
2
+ import { realpathSync } from "node:fs";
3
+ import { prepareLaunchProject, resolveLaunchLocalContext } from "../cli/launch.js";
4
+ import { readExistingManagedTarget } from "./existing-project.js";
5
+ import { ensureManagedPrivateRoot, ManagedLocalStateError, ManagedPrivateStore, } from "./private-store.js";
6
+ // A workspace can be selected by different drafts and Relay origins. Serialize by
7
+ // canonical folder in a machine-wide namespace, not by project or creation ID.
8
+ export function acquireManagedWorkspaceLease(tuttiHome, workspaceRoot) {
9
+ const files = new ManagedPrivateStore(ensureManagedPrivateRoot(tuttiHome, createHash("sha256").update("tutti-managed-workspace-locks-v1").digest("hex")));
10
+ return files.acquireExecutionLease(`workspace-${createHash("sha256").update(realpathSync(workspaceRoot)).digest("hex")}`);
11
+ }
12
+ // Native-only materialization after verified validation. Does not start a Host or
13
+ // write a placeholder BYOK credential. Resuming the same draft reuses its identity.
14
+ export async function materializeManagedWorkspace(options) {
15
+ const now = options.now ?? (() => new Date());
16
+ options.state.bootstrap(options.requestRef, now());
17
+ const row = options.state.read(options.requestRef);
18
+ const unlock = acquireManagedWorkspaceLease(options.tuttiHome, row.workspace_root);
19
+ try {
20
+ let workspacePath = row.workspace_root;
21
+ if (row.existing_relay_project_ref !== undefined) {
22
+ const target = readExistingManagedTarget({
23
+ tuttiHome: options.tuttiHome,
24
+ relayOrigin: options.state.origin,
25
+ projectId: row.project_id,
26
+ workspaceRoot: row.workspace_root,
27
+ surface: row.surface,
28
+ requireStopped: row.state !== "registered",
29
+ });
30
+ if (target.relayProjectRef !== row.existing_relay_project_ref ||
31
+ target.hostSecret !== row.host_registration_secret)
32
+ throw new ManagedLocalStateError("managed_existing_project_mismatch");
33
+ workspacePath = target.workspacePath;
34
+ }
35
+ const environment = {
36
+ ...options.environment,
37
+ TUTTI_HOME: options.tuttiHome,
38
+ TUTTI_RELAY_URL: options.state.origin,
39
+ };
40
+ const local = resolveLaunchLocalContext({
41
+ workspacePath,
42
+ env: environment,
43
+ });
44
+ if (realpathSync(local.workspace_root) !== row.workspace_root ||
45
+ (local.existing_identity.kind === "present" &&
46
+ local.existing_identity.project_id !== row.project_id))
47
+ throw new ManagedLocalStateError("managed_workspace_identity_changed");
48
+ // Persist the recovery pointer before Git/binding mutation. A crash never leaves
49
+ // an apparently-BYOK project where the managed creation reservation was lost.
50
+ options.state.attachProject(options.requestRef, now());
51
+ return await prepareLaunchProject({
52
+ workspacePath,
53
+ env: environment,
54
+ yes: true,
55
+ projectDisplayName: row.display_name,
56
+ ...(row.description === undefined ? {} : { projectDescription: row.description }),
57
+ projectSurface: row.surface,
58
+ reservedIdentity: {
59
+ projectId: row.project_id,
60
+ hostRegistrationSecret: row.host_registration_secret,
61
+ },
62
+ now,
63
+ });
64
+ }
65
+ finally {
66
+ unlock();
67
+ }
68
+ }
69
+ //# sourceMappingURL=workspace.js.map
@@ -1,5 +1,10 @@
1
- import { type HostProjectApiClient, type LoginPayload, type LoginResponse, type OpenProjectResponse, type RelayApiClient, type TuttiApiFetch, type AcceptJoinResponse, type JoinEntryProjection, type TuttiApiResponse } from "@tutti/api-client";
1
+ import { type HostProjectApiClient, type EmailLoginChallengeProjection, type LoginResponse, type OpenProjectResponse, type RelayApiClient, type TuttiApiFetch, type AcceptJoinResponse, type JoinEntryProjection, type TuttiApiResponse } from "@tutti/api-client";
2
2
  import type { RelayProjectRef } from "@tutti/shared/ids";
3
+ export type SmokeEmailLoginInput = {
4
+ email: string;
5
+ display_name?: string;
6
+ readCode: (challenge: EmailLoginChallengeProjection) => Promise<string>;
7
+ };
3
8
  export type E2ESmokeLogSink = (line: string) => void;
4
9
  export type E2ESmokeClientOptions = {
5
10
  relayBaseUrl: string;
@@ -20,7 +25,7 @@ export type E2ESmokeSession = {
20
25
  };
21
26
  export type LoginJoinAndOpenWorkspaceInput = {
22
27
  joinToken: string;
23
- login?: Partial<LoginPayload>;
28
+ login: SmokeEmailLoginInput;
24
29
  };
25
30
  export type LoginJoinAndOpenWorkspaceResult = {
26
31
  joinEntry: JoinEntryProjection;
@@ -53,7 +58,7 @@ export declare class E2ESmokeCookieJar {
53
58
  private storeSetCookieHeader;
54
59
  }
55
60
  export declare function createE2ESmokeSession(options: E2ESmokeClientOptions): E2ESmokeSession;
56
- export declare function loginRelaySmokeAccount(session: E2ESmokeSession, input?: Partial<LoginPayload>): Promise<LoginResponse>;
61
+ export declare function loginRelaySmokeAccount(session: E2ESmokeSession, input: SmokeEmailLoginInput): Promise<LoginResponse>;
57
62
  export declare function loginJoinAndOpenWorkspace(session: E2ESmokeSession, input: LoginJoinAndOpenWorkspaceInput): Promise<LoginJoinAndOpenWorkspaceResult>;
58
63
  export declare function pollE2ESmoke<TValue>(options: E2ESmokePollOptions<TValue>): Promise<TValue>;
59
64
  //# sourceMappingURL=e2e-client.d.ts.map
@@ -1,10 +1,6 @@
1
1
  import { setTimeout as sleep } from "node:timers/promises";
2
2
  import { createHostProjectApiClientFromRelay, createRelayApiClientFromRelay, } from "@tutti/api-client";
3
3
  import { serializeRuntimeLogLine } from "@tutti/shared/utils";
4
- const DEFAULT_SMOKE_LOGIN_PAYLOAD = {
5
- email: "tutti-smoke@example.invalid",
6
- display_name: "Tutti Smoke",
7
- };
8
4
  const DEFAULT_POLL_TIMEOUT_MS = 30_000;
9
5
  const DEFAULT_POLL_INTERVAL_MS = 500;
10
6
  export class E2ESmokeFlowError extends Error {
@@ -160,12 +156,6 @@ function emitSmokeLog(session, stage, fields = {}) {
160
156
  now: session.now,
161
157
  }));
162
158
  }
163
- function loginPayload(input) {
164
- return {
165
- ...DEFAULT_SMOKE_LOGIN_PAYLOAD,
166
- ...(input ?? {}),
167
- };
168
- }
169
159
  function readRelayProjectRefFromJoin(joinEntry) {
170
160
  if (joinEntry.relay_project !== undefined) {
171
161
  return joinEntry.relay_project.relay_project_ref;
@@ -216,7 +206,17 @@ export function createE2ESmokeSession(options) {
216
206
  };
217
207
  }
218
208
  export async function loginRelaySmokeAccount(session, input) {
219
- const response = await session.relay.login(loginPayload(input), { now: session.now });
209
+ const start = await session.relay.startEmailLogin({ email: input.email }, { now: session.now });
210
+ if (start.result === undefined)
211
+ throw new Error("Email login challenge was not returned");
212
+ const code = await input.readCode(start.result.challenge);
213
+ const response = await session.relay.verifyEmailLogin({
214
+ challenge_ref: start.result.challenge.challenge_ref,
215
+ code,
216
+ }, { now: session.now });
217
+ if (response.result?.account.profile_complete === false && input.display_name !== undefined) {
218
+ await session.relay.completeAccountProfile({ display_name: input.display_name }, { now: session.now });
219
+ }
220
220
  emitSmokeLog(session, "relay_login", {
221
221
  disposition: response.disposition.kind,
222
222
  account_ref: response.result?.account.account_ref,
@@ -1,7 +1,7 @@
1
1
  import { type HostConnectionRef, type IdempotencyKey, type ProjectId, type RelayProjectRef, type RelayUploadRef } from "@tutti/shared/ids";
2
2
  import type { ProjectSurfaceProfile } from "@tutti/shared/domain";
3
3
  import type { RelayUploadPurpose, Sha256Digest } from "@tutti/shared/schemas/api";
4
- import { type FeishuSetupProjection, type PlatformOutboundHandoffResponse, type PlatformOutboundIntent } from "@tutti/shared/schemas/internal";
4
+ import { type HostDelegationProjection, type ManagedProjectBootstrap, type FeishuSetupProjection, type PlatformOutboundHandoffResponse, type PlatformOutboundIntent } from "@tutti/shared/schemas/internal";
5
5
  export type RelayHostControlFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
6
6
  export type RelayProjectProjection = {
7
7
  relay_project_ref: RelayProjectRef;
@@ -25,6 +25,7 @@ export type JoinTokenProjection = {
25
25
  visibility: "visible_once" | "not_recoverable";
26
26
  };
27
27
  export type RegisterRelayHostConnectionPayload = {
28
+ managed_bootstrap?: ManagedProjectBootstrap;
28
29
  project_identity: {
29
30
  source: "git_config";
30
31
  project_id: ProjectId;
@@ -64,6 +65,7 @@ export type RegisterRelayHostConnectionPayload = {
64
65
  };
65
66
  };
66
67
  export type RegisterRelayHostConnectionResult = {
68
+ managed_authority?: HostDelegationProjection;
67
69
  relay_project: RelayProjectProjection;
68
70
  host_connection: HostConnectionProjection;
69
71
  join_token?: JoinTokenProjection;
@@ -152,6 +154,7 @@ export type DeliverRelayPlatformOutboundOptions = {
152
154
  signal?: AbortSignal;
153
155
  };
154
156
  export type RegisterRelayHostConnectionOptions = {
157
+ managedBootstrap?: ManagedProjectBootstrap;
155
158
  relayUrl: string;
156
159
  projectId: ProjectId;
157
160
  hostRegistrationSecret: string;
@@ -1,5 +1,5 @@
1
1
  import { createIdempotencyKey, } from "@tutti/shared/ids";
2
- import { isPlatformOutboundHandoffResponse, } from "@tutti/shared/schemas/internal";
2
+ import { isPlatformOutboundHandoffResponse, isHostDelegationProjection, isManagedProjectBootstrap, } from "@tutti/shared/schemas/internal";
3
3
  export class RelayHostControlClientError extends Error {
4
4
  code;
5
5
  retryable;
@@ -91,6 +91,12 @@ function createRegisterPayload(options) {
91
91
  account_handoff_token: options.accountHandoffToken,
92
92
  };
93
93
  }
94
+ if (options.managedBootstrap !== undefined) {
95
+ if (options.accountHandoffToken !== undefined ||
96
+ !isManagedProjectBootstrap(options.managedBootstrap))
97
+ throw new RelayHostControlClientError("relay_registration_failed", "Managed Host authority input is invalid", false);
98
+ payload.managed_bootstrap = options.managedBootstrap;
99
+ }
94
100
  return payload;
95
101
  }
96
102
  function createRefreshJoinTokenPayload(options) {
@@ -199,6 +205,10 @@ function assertRegistrationResponse(value) {
199
205
  throw new RelayHostControlClientError("relay_registration_failed", "Relay host registration response is invalid", false);
200
206
  }
201
207
  const dispositionKind = stringField(value.disposition, "kind");
208
+ if (isRecord(value.result) &&
209
+ value.result.managed_authority !== undefined &&
210
+ !isHostDelegationProjection(value.result.managed_authority))
211
+ throw new RelayHostControlClientError("relay_registration_failed", "Managed Host authority response is invalid", false);
202
212
  if (dispositionKind !== "registered" && dispositionKind !== "interrupted") {
203
213
  throw new RelayHostControlClientError("relay_registration_failed", "Relay host registration disposition is invalid", false);
204
214
  }
@@ -249,6 +259,7 @@ export async function registerRelayHostConnection(options) {
249
259
  const fetchImpl = options.fetch ?? fetch;
250
260
  const commandId = options.commandId ?? createIdempotencyKey();
251
261
  const issuedAt = options.issuedAt ?? new Date();
262
+ const payload = createRegisterPayload(options);
252
263
  const response = await (async () => {
253
264
  try {
254
265
  return await fetchWithTimeout(fetchImpl, registrationUrl(options.relayUrl), {
@@ -260,7 +271,7 @@ export async function registerRelayHostConnection(options) {
260
271
  body: JSON.stringify({
261
272
  command_id: commandId,
262
273
  issued_at: issuedAt.toISOString(),
263
- payload: createRegisterPayload(options),
274
+ payload,
264
275
  }),
265
276
  }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, options.signal);
266
277
  }
@@ -278,7 +289,19 @@ export async function registerRelayHostConnection(options) {
278
289
  }
279
290
  throw new RelayHostControlClientError(response.status >= 500 ? "relay_unavailable" : "relay_registration_failed", `Relay host registration failed with HTTP ${response.status}`, response.status >= 500, response.status, relayErrorCode);
280
291
  }
281
- return assertRegistrationResponse(await response.json());
292
+ const result = assertRegistrationResponse(await response.json());
293
+ if (options.managedBootstrap !== undefined &&
294
+ result.result !== undefined &&
295
+ result.result.managed_authority === undefined)
296
+ throw new RelayHostControlClientError("relay_registration_failed", "Relay does not support managed Host authority", false);
297
+ if (options.managedBootstrap !== undefined &&
298
+ result.result?.managed_authority !== undefined &&
299
+ (result.result.managed_authority.installation_ref !==
300
+ options.managedBootstrap.installation_ref ||
301
+ result.result.managed_authority.relay_project_ref !==
302
+ result.result.relay_project?.relay_project_ref))
303
+ throw new RelayHostControlClientError("relay_registration_failed", "Managed Host authority response does not match registration", false);
304
+ return result;
282
305
  }
283
306
  export async function refreshRelayJoinToken(options) {
284
307
  const fetchImpl = options.fetch ?? fetch;
@@ -1,4 +1,5 @@
1
1
  export * from "./host-control.js";
2
2
  export * from "./tunnel.js";
3
3
  export * from "./external-references.js";
4
+ export * from "./project-authority.js";
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,5 @@
1
1
  export * from "./host-control.js";
2
2
  export * from "./tunnel.js";
3
3
  export * from "./external-references.js";
4
+ export * from "./project-authority.js";
4
5
  //# sourceMappingURL=index.js.map