@venturewild/workspace 0.3.7 → 0.4.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 (55) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +112 -112
  3. package/package.json +83 -83
  4. package/server/bin/wild-workspace.mjs +1096 -995
  5. package/server/src/account.mjs +114 -114
  6. package/server/src/agent-login.mjs +146 -146
  7. package/server/src/agent-readiness.mjs +200 -200
  8. package/server/src/agent.mjs +468 -468
  9. package/server/src/bazaar/core.mjs +579 -579
  10. package/server/src/bazaar/index.mjs +75 -75
  11. package/server/src/bazaar/mcp-server.mjs +328 -328
  12. package/server/src/bazaar/mock-tickup.mjs +97 -97
  13. package/server/src/bazaar/preview-server.mjs +95 -95
  14. package/server/src/bazaar/seed-recipes/customer-feedback-form/know-how.md +23 -23
  15. package/server/src/bazaar/seed-recipes/customer-feedback-form/recipe.json +24 -24
  16. package/server/src/bazaar/seed-recipes/landing-page-launch/know-how.md +29 -29
  17. package/server/src/bazaar/seed-recipes/landing-page-launch/recipe.json +25 -25
  18. package/server/src/bazaar/seed-recipes/personal-portfolio/know-how.md +21 -21
  19. package/server/src/bazaar/seed-recipes/personal-portfolio/recipe.json +24 -24
  20. package/server/src/bazaar/seed-recipes/receipt-sorter/know-how.md +31 -31
  21. package/server/src/bazaar/seed-recipes/receipt-sorter/recipe.json +25 -25
  22. package/server/src/bazaar/seed-recipes/tickup-hr-matching/know-how.md +79 -79
  23. package/server/src/bazaar/seed-recipes/tickup-hr-matching/recipe.json +32 -32
  24. package/server/src/canvas/core.mjs +446 -421
  25. package/server/src/canvas/index.mjs +42 -42
  26. package/server/src/canvas/mcp-server.mjs +253 -253
  27. package/server/src/canvas-rails.mjs +108 -0
  28. package/server/src/config.mjs +404 -404
  29. package/server/src/daemon-bin.mjs +110 -110
  30. package/server/src/daemon-supervisor.mjs +285 -285
  31. package/server/src/doctor.mjs +375 -375
  32. package/server/src/inbox.mjs +86 -86
  33. package/server/src/index.mjs +2766 -2475
  34. package/server/src/logpaths.mjs +98 -98
  35. package/server/src/observability.mjs +45 -45
  36. package/server/src/operator.mjs +92 -92
  37. package/server/src/pairing.mjs +137 -137
  38. package/server/src/service.mjs +515 -515
  39. package/server/src/session-reporter.mjs +201 -201
  40. package/server/src/settings.mjs +145 -145
  41. package/server/src/share.mjs +182 -182
  42. package/server/src/skills.mjs +213 -213
  43. package/server/src/supervisor.mjs +647 -647
  44. package/server/src/support-consent.mjs +133 -133
  45. package/server/src/sync.mjs +248 -248
  46. package/server/src/transcript.mjs +121 -121
  47. package/server/src/turn-mcp.mjs +46 -46
  48. package/server/src/usage.mjs +405 -405
  49. package/server/src/workspace-registry.mjs +225 -0
  50. package/server/src/workspaces.mjs +111 -0
  51. package/web/dist/assets/index-NXZN2LU2.css +1 -0
  52. package/web/dist/assets/index-PAS8Inwp.js +91 -0
  53. package/web/dist/index.html +2 -2
  54. package/web/dist/assets/index-BxRx8EsD.js +0 -91
  55. package/web/dist/assets/index-DoOPBr3s.css +0 -1
@@ -1,137 +1,137 @@
1
- // In-memory store of pending "sign in this device" requests (Phase 2 device
2
- // approval). A new device calls POST /api/auth/pair/start → we mint a short
3
- // human CODE (the owner reads it off the new device) plus an unguessable
4
- // requestId (the device polls by THIS, never the code). The owner approves from
5
- // a partner session — which must echo the matching code (confused-deputy
6
- // defense) — and we attach a minted device token; the device claims it ONCE.
7
- //
8
- // Deliberately in-memory + ephemeral (5-min TTL): a pending request that
9
- // survived a restart would just be a longer attack window for no UX gain — the
10
- // device simply re-requests. The durable artifact is the minted token
11
- // (persisted via TokenRegistry), not the pairing record.
12
-
13
- import crypto from 'node:crypto';
14
- import { nanoid } from 'nanoid';
15
-
16
- const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
17
- const DEFAULT_MAX_PENDING = 5;
18
-
19
- // 6-digit zero-padded code. Used ONLY owner-side (the approver matches it
20
- // against the code shown on the new device). It is NEVER an auth credential on
21
- // the public poll path — the device authenticates by requestId — so its low
22
- // entropy is not an internet-facing brute-force surface.
23
- function makeCode() {
24
- return String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
25
- }
26
-
27
- export class PairingStore {
28
- constructor({ ttlMs = DEFAULT_TTL_MS, maxPending = DEFAULT_MAX_PENDING, clock = () => Date.now() } = {}) {
29
- this.ttlMs = ttlMs;
30
- this.maxPending = maxPending;
31
- this.clock = clock;
32
- // requestId -> { requestId, code, label, status, createdAt, expiresAt, token, sub, exp }
33
- this.requests = new Map();
34
- }
35
-
36
- // Expire stale pending requests; drop terminal records a while after expiry to
37
- // bound memory. Called on every accessor so the map self-cleans.
38
- prune() {
39
- const now = this.clock();
40
- for (const [id, r] of this.requests) {
41
- if (r.status === 'pending' && r.expiresAt <= now) r.status = 'expired';
42
- if (r.expiresAt + this.ttlMs <= now) this.requests.delete(id);
43
- }
44
- }
45
-
46
- pendingCount() {
47
- this.prune();
48
- let n = 0;
49
- for (const r of this.requests.values()) if (r.status === 'pending') n += 1;
50
- return n;
51
- }
52
-
53
- // Returns { requestId, code, expiresAt } or null if the global pending cap is
54
- // reached (anti-spam: bounds how cluttered the owner's approval list can get).
55
- create({ label } = {}) {
56
- if (this.pendingCount() >= this.maxPending) return null;
57
- const now = this.clock();
58
- const requestId = nanoid(16);
59
- // Codes must be UNIQUE among pending so "owner types the code" resolves to
60
- // exactly one request (collision odds are ~nil at maxPending=5, but be
61
- // deterministic about it).
62
- const pendingCodes = new Set(
63
- [...this.requests.values()].filter((r) => r.status === 'pending').map((r) => r.code),
64
- );
65
- let code = makeCode();
66
- for (let guard = 0; pendingCodes.has(code) && guard < 50; guard += 1) code = makeCode();
67
- const rec = {
68
- requestId,
69
- code,
70
- label: label || 'a device',
71
- status: 'pending',
72
- createdAt: now,
73
- expiresAt: now + this.ttlMs,
74
- token: null,
75
- sub: null,
76
- exp: null,
77
- };
78
- this.requests.set(requestId, rec);
79
- return { requestId, code: rec.code, expiresAt: rec.expiresAt };
80
- }
81
-
82
-
83
- get(requestId) {
84
- this.prune();
85
- return this.requests.get(requestId) || null;
86
- }
87
-
88
- // Pending, non-expired requests for the owner's approval UI (includes the code
89
- // so the owner can visually match it against the new device's screen).
90
- listPending() {
91
- this.prune();
92
- // Includes the code so the owner can match it against the new device's
93
- // screen and approve the right one if more than one is pending. Approval
94
- // itself is by requestId (one tap); the code is the human "is this mine?"
95
- // confirmation, not a typed secret.
96
- return [...this.requests.values()]
97
- .filter((r) => r.status === 'pending')
98
- .map((r) => ({ requestId: r.requestId, code: r.code, label: r.label, createdAt: r.createdAt }));
99
- }
100
-
101
- // Approve a specific pending request (the one the owner tapped, having matched
102
- // its code against the new device). Returns true on success. `tokenInfo` =
103
- // { token, sub, exp } from mintDeviceToken.
104
- approve(requestId, tokenInfo) {
105
- this.prune();
106
- const r = this.requests.get(requestId);
107
- if (!r || r.status !== 'pending') return false;
108
- r.status = 'approved';
109
- r.token = tokenInfo.token;
110
- r.sub = tokenInfo.sub;
111
- r.exp = tokenInfo.exp;
112
- return true;
113
- }
114
-
115
- deny(requestId) {
116
- this.prune();
117
- const r = this.requests.get(requestId);
118
- if (!r || r.status !== 'pending') return false;
119
- r.status = 'denied';
120
- return true;
121
- }
122
-
123
- // One-shot token read: once approved, the device gets the token exactly once;
124
- // a replayed poll yields { status:'claimed' } with no token.
125
- claim(requestId) {
126
- this.prune();
127
- const r = this.requests.get(requestId);
128
- if (!r) return { status: 'expired' };
129
- if (r.status === 'approved' && r.token) {
130
- const token = r.token;
131
- r.status = 'claimed';
132
- r.token = null;
133
- return { status: 'approved', token };
134
- }
135
- return { status: r.status };
136
- }
137
- }
1
+ // In-memory store of pending "sign in this device" requests (Phase 2 device
2
+ // approval). A new device calls POST /api/auth/pair/start → we mint a short
3
+ // human CODE (the owner reads it off the new device) plus an unguessable
4
+ // requestId (the device polls by THIS, never the code). The owner approves from
5
+ // a partner session — which must echo the matching code (confused-deputy
6
+ // defense) — and we attach a minted device token; the device claims it ONCE.
7
+ //
8
+ // Deliberately in-memory + ephemeral (5-min TTL): a pending request that
9
+ // survived a restart would just be a longer attack window for no UX gain — the
10
+ // device simply re-requests. The durable artifact is the minted token
11
+ // (persisted via TokenRegistry), not the pairing record.
12
+
13
+ import crypto from 'node:crypto';
14
+ import { nanoid } from 'nanoid';
15
+
16
+ const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
17
+ const DEFAULT_MAX_PENDING = 5;
18
+
19
+ // 6-digit zero-padded code. Used ONLY owner-side (the approver matches it
20
+ // against the code shown on the new device). It is NEVER an auth credential on
21
+ // the public poll path — the device authenticates by requestId — so its low
22
+ // entropy is not an internet-facing brute-force surface.
23
+ function makeCode() {
24
+ return String(crypto.randomInt(0, 1_000_000)).padStart(6, '0');
25
+ }
26
+
27
+ export class PairingStore {
28
+ constructor({ ttlMs = DEFAULT_TTL_MS, maxPending = DEFAULT_MAX_PENDING, clock = () => Date.now() } = {}) {
29
+ this.ttlMs = ttlMs;
30
+ this.maxPending = maxPending;
31
+ this.clock = clock;
32
+ // requestId -> { requestId, code, label, status, createdAt, expiresAt, token, sub, exp }
33
+ this.requests = new Map();
34
+ }
35
+
36
+ // Expire stale pending requests; drop terminal records a while after expiry to
37
+ // bound memory. Called on every accessor so the map self-cleans.
38
+ prune() {
39
+ const now = this.clock();
40
+ for (const [id, r] of this.requests) {
41
+ if (r.status === 'pending' && r.expiresAt <= now) r.status = 'expired';
42
+ if (r.expiresAt + this.ttlMs <= now) this.requests.delete(id);
43
+ }
44
+ }
45
+
46
+ pendingCount() {
47
+ this.prune();
48
+ let n = 0;
49
+ for (const r of this.requests.values()) if (r.status === 'pending') n += 1;
50
+ return n;
51
+ }
52
+
53
+ // Returns { requestId, code, expiresAt } or null if the global pending cap is
54
+ // reached (anti-spam: bounds how cluttered the owner's approval list can get).
55
+ create({ label } = {}) {
56
+ if (this.pendingCount() >= this.maxPending) return null;
57
+ const now = this.clock();
58
+ const requestId = nanoid(16);
59
+ // Codes must be UNIQUE among pending so "owner types the code" resolves to
60
+ // exactly one request (collision odds are ~nil at maxPending=5, but be
61
+ // deterministic about it).
62
+ const pendingCodes = new Set(
63
+ [...this.requests.values()].filter((r) => r.status === 'pending').map((r) => r.code),
64
+ );
65
+ let code = makeCode();
66
+ for (let guard = 0; pendingCodes.has(code) && guard < 50; guard += 1) code = makeCode();
67
+ const rec = {
68
+ requestId,
69
+ code,
70
+ label: label || 'a device',
71
+ status: 'pending',
72
+ createdAt: now,
73
+ expiresAt: now + this.ttlMs,
74
+ token: null,
75
+ sub: null,
76
+ exp: null,
77
+ };
78
+ this.requests.set(requestId, rec);
79
+ return { requestId, code: rec.code, expiresAt: rec.expiresAt };
80
+ }
81
+
82
+
83
+ get(requestId) {
84
+ this.prune();
85
+ return this.requests.get(requestId) || null;
86
+ }
87
+
88
+ // Pending, non-expired requests for the owner's approval UI (includes the code
89
+ // so the owner can visually match it against the new device's screen).
90
+ listPending() {
91
+ this.prune();
92
+ // Includes the code so the owner can match it against the new device's
93
+ // screen and approve the right one if more than one is pending. Approval
94
+ // itself is by requestId (one tap); the code is the human "is this mine?"
95
+ // confirmation, not a typed secret.
96
+ return [...this.requests.values()]
97
+ .filter((r) => r.status === 'pending')
98
+ .map((r) => ({ requestId: r.requestId, code: r.code, label: r.label, createdAt: r.createdAt }));
99
+ }
100
+
101
+ // Approve a specific pending request (the one the owner tapped, having matched
102
+ // its code against the new device). Returns true on success. `tokenInfo` =
103
+ // { token, sub, exp } from mintDeviceToken.
104
+ approve(requestId, tokenInfo) {
105
+ this.prune();
106
+ const r = this.requests.get(requestId);
107
+ if (!r || r.status !== 'pending') return false;
108
+ r.status = 'approved';
109
+ r.token = tokenInfo.token;
110
+ r.sub = tokenInfo.sub;
111
+ r.exp = tokenInfo.exp;
112
+ return true;
113
+ }
114
+
115
+ deny(requestId) {
116
+ this.prune();
117
+ const r = this.requests.get(requestId);
118
+ if (!r || r.status !== 'pending') return false;
119
+ r.status = 'denied';
120
+ return true;
121
+ }
122
+
123
+ // One-shot token read: once approved, the device gets the token exactly once;
124
+ // a replayed poll yields { status:'claimed' } with no token.
125
+ claim(requestId) {
126
+ this.prune();
127
+ const r = this.requests.get(requestId);
128
+ if (!r) return { status: 'expired' };
129
+ if (r.status === 'approved' && r.token) {
130
+ const token = r.token;
131
+ r.status = 'claimed';
132
+ r.token = null;
133
+ return { status: 'approved', token };
134
+ }
135
+ return { status: r.status };
136
+ }
137
+ }