deepspace 0.18.0 → 0.19.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/dist/cli.js CHANGED
@@ -342,7 +342,7 @@ function defineDeepspaceCommand(def) {
342
342
  ...def.args ?? {},
343
343
  json: {
344
344
  type: "boolean",
345
- description: "Emit a single-line JSON result for scripts/agents",
345
+ description: def.jsonDescription ?? "Emit a single-line JSON result for scripts/agents",
346
346
  default: false
347
347
  }
348
348
  };
@@ -2412,7 +2412,7 @@ function lintSchema(schema) {
2412
2412
  if (schema.ownerField) {
2413
2413
  const col = schema.columns.find((c) => c.name === schema.ownerField);
2414
2414
  const spoofableRoles = Object.entries(schema.permissions).filter(
2415
- ([role, permissions]) => role !== "owner" && role !== "admin" && permissions.create === true
2415
+ ([role, permissions]) => role !== "owner" && role !== "admin" && permissions.create === true && permissions.update !== "unclaimed-or-own"
2416
2416
  ).map(([role]) => role);
2417
2417
  if (col && !col.userBound && spoofableRoles.length > 0) {
2418
2418
  warnings.push(
@@ -3045,7 +3045,7 @@ function normalizeTestAccountScope(value) {
3045
3045
  }
3046
3046
  function loadAllTestAccounts(scope = currentTestAccountScope()) {
3047
3047
  const store = readTestAccountStore();
3048
- return mergeTestAccounts(store.scopes[scope] ?? [], store.unscoped);
3048
+ return store.scopes[scope] ?? [];
3049
3049
  }
3050
3050
  function reconcileTestAccountStore(remote, scope = currentTestAccountScope()) {
3051
3051
  const result = reconcileTestAccountScopes(readTestAccountStore(), remote, scope);
@@ -3054,24 +3054,20 @@ function reconcileTestAccountStore(remote, scope = currentTestAccountScope()) {
3054
3054
  }
3055
3055
  function reconcileTestAccountScopes(store, remote, scope) {
3056
3056
  const scoped = store.scopes[scope] ?? [];
3057
- const current = reconcileTestAccounts(scoped, remote);
3058
- const migrated = reconcileTestAccounts(store.unscoped, remote);
3059
- const accounts = mergeTestAccounts(current, migrated);
3057
+ const accounts = reconcileTestAccounts(scoped, remote);
3060
3058
  return {
3061
3059
  accounts,
3062
- removed: Math.max(0, scoped.length - current.length),
3060
+ removed: Math.max(0, scoped.length - accounts.length),
3063
3061
  store: {
3064
3062
  version: 2,
3065
- scopes: { ...store.scopes, [scope]: accounts },
3066
- unscoped: store.unscoped.filter((account) => !remote.some((item) => accountMatchesRemote(account, item)))
3063
+ scopes: { ...store.scopes, [scope]: accounts }
3067
3064
  }
3068
3065
  };
3069
3066
  }
3070
3067
  function reconcileTestAccounts(local, remote) {
3071
3068
  const localById = new Map(local.filter((account) => account.id).map((account) => [account.id, account]));
3072
- const legacyByEmail = new Map(local.filter((account) => !account.id).map((account) => [account.email, account]));
3073
3069
  return remote.flatMap((account) => {
3074
- const stored = localById.get(account.id) ?? legacyByEmail.get(account.email);
3070
+ const stored = localById.get(account.id);
3075
3071
  return stored ? [{ ...stored, ...account }] : [];
3076
3072
  });
3077
3073
  }
@@ -3082,10 +3078,7 @@ function upsertTestAccount(account, scope = currentTestAccountScope()) {
3082
3078
  );
3083
3079
  saveTestAccountStore({
3084
3080
  version: 2,
3085
- scopes: { ...store.scopes, [scope]: [...accounts, account] },
3086
- unscoped: store.unscoped.filter(
3087
- (stored) => stored.id !== account.id && stored.email !== account.email
3088
- )
3081
+ scopes: { ...store.scopes, [scope]: [...accounts, account] }
3089
3082
  });
3090
3083
  }
3091
3084
  function removeTestAccounts(ids, emails = [], scope = currentTestAccountScope()) {
@@ -3095,41 +3088,31 @@ function removeTestAccounts(ids, emails = [], scope = currentTestAccountScope())
3095
3088
  const store = readTestAccountStore();
3096
3089
  saveTestAccountStore({
3097
3090
  version: 2,
3098
- scopes: { ...store.scopes, [scope]: (store.scopes[scope] ?? []).filter(keep) },
3099
- unscoped: store.unscoped.filter(keep)
3091
+ scopes: { ...store.scopes, [scope]: (store.scopes[scope] ?? []).filter(keep) }
3100
3092
  });
3101
3093
  }
3102
3094
  function readTestAccountStore() {
3103
- if (!existsSync11(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {}, unscoped: [] };
3095
+ if (!existsSync11(TEST_ACCOUNTS_PATH)) return { version: 2, scopes: {} };
3104
3096
  try {
3105
3097
  const raw = JSON.parse(readFileSync13(TEST_ACCOUNTS_PATH, "utf-8"));
3106
- if (Array.isArray(raw)) return { version: 2, scopes: {}, unscoped: validTestAccounts(raw) };
3107
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
3108
- return { version: 2, scopes: {}, unscoped: [] };
3098
+ if (Array.isArray(raw)) return { version: 2, scopes: {} };
3099
+ if (!raw || typeof raw !== "object") {
3100
+ return { version: 2, scopes: {} };
3109
3101
  }
3110
3102
  const candidate = raw;
3111
3103
  const scopes = candidate.scopes && typeof candidate.scopes === "object" && !Array.isArray(candidate.scopes) ? Object.fromEntries(Object.entries(candidate.scopes).map(([key, value]) => [key, validTestAccounts(value)])) : {};
3112
- return { version: 2, scopes, unscoped: validTestAccounts(candidate.unscoped) };
3104
+ return { version: 2, scopes };
3113
3105
  } catch {
3114
- return { version: 2, scopes: {}, unscoped: [] };
3106
+ return { version: 2, scopes: {} };
3115
3107
  }
3116
3108
  }
3117
3109
  function validTestAccounts(value) {
3118
3110
  if (!Array.isArray(value)) return [];
3119
- return value.filter((item) => Boolean(
3120
- item && typeof item === "object" && typeof item.email === "string" && typeof item.password === "string"
3121
- ));
3122
- }
3123
- function mergeTestAccounts(...groups) {
3124
- const accounts = /* @__PURE__ */ new Map();
3125
- for (const account of groups.flat()) {
3126
- const key = account.id ? `id:${account.id}` : `email:${account.email}`;
3127
- if (!accounts.has(key)) accounts.set(key, account);
3128
- }
3129
- return [...accounts.values()];
3130
- }
3131
- function accountMatchesRemote(local, remote) {
3132
- return local.id ? local.id === remote.id : local.email === remote.email;
3111
+ return value.filter(
3112
+ (item) => Boolean(
3113
+ item && typeof item === "object" && typeof item.id === "string" && typeof item.email === "string" && typeof item.password === "string"
3114
+ )
3115
+ );
3133
3116
  }
3134
3117
  function saveTestAccountStore(store) {
3135
3118
  mkdirSync5(join13(homedir3(), ".deepspace"), { recursive: true });
@@ -3187,11 +3170,24 @@ async function syncTestAccountStore() {
3187
3170
 
3188
3171
  // src/cli/commands/test.ts
3189
3172
  var DEPLOY_URL2 = process.env.DEEPSPACE_DEPLOY_URL ?? PLATFORM_URLS.deploy;
3173
+ var PLAYWRIGHT_OUTPUT_DIR = ".deepspace/test-results";
3174
+ function playwrightTestArgs(testFiles) {
3175
+ return [
3176
+ "playwright",
3177
+ "test",
3178
+ "--config",
3179
+ "tests/playwright.config.ts",
3180
+ "--output",
3181
+ PLAYWRIGHT_OUTPUT_DIR,
3182
+ ...testFiles
3183
+ ];
3184
+ }
3190
3185
  var test_default = defineDeepspaceCommand({
3191
3186
  meta: {
3192
3187
  name: "test",
3193
3188
  description: "Run tests for your DeepSpace app"
3194
3189
  },
3190
+ jsonDescription: "Stream test output; emit the final JSON result on the last line",
3195
3191
  args: {
3196
3192
  suite: {
3197
3193
  type: "positional",
@@ -3324,15 +3320,14 @@ function runPlaywright(appDir, testFiles, port, wranglerEnv) {
3324
3320
  return 1;
3325
3321
  }
3326
3322
  try {
3327
- const result = spawnSync4(
3328
- "npx",
3329
- ["playwright", "test", "--config", "tests/playwright.config.ts", ...testFiles],
3330
- {
3331
- cwd: appDir,
3332
- stdio: "inherit",
3333
- env: wranglerViteEnv(process.env, wranglerConfig, { DEEPSPACE_PORT: String(port) })
3334
- }
3335
- );
3323
+ const result = spawnSync4("npx", playwrightTestArgs(testFiles), {
3324
+ cwd: appDir,
3325
+ stdio: "inherit",
3326
+ env: wranglerViteEnv(process.env, wranglerConfig, {
3327
+ DEEPSPACE_PORT: String(port),
3328
+ PLAYWRIGHT_HTML_OUTPUT_DIR: ".deepspace/playwright-report"
3329
+ })
3330
+ });
3336
3331
  return result.status ?? 1;
3337
3332
  } finally {
3338
3333
  wranglerConfig.cleanup();
@@ -3850,7 +3845,7 @@ function describeFilesFailure(status3, body) {
3850
3845
  import * as p6 from "@clack/prompts";
3851
3846
  import { execSync as execSync2 } from "child_process";
3852
3847
  import { createHash as createHash2 } from "crypto";
3853
- import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4 } from "fs";
3848
+ import { chmodSync as chmodSync2, existsSync as existsSync14, lstatSync as lstatSync2, readFileSync as readFileSync17, readdirSync as readdirSync4 } from "fs";
3854
3849
  import { dirname as dirname6, join as join16, resolve as resolve11 } from "path";
3855
3850
 
3856
3851
  // src/documentation/deploy.ts
@@ -3885,6 +3880,7 @@ import { join as join15 } from "path";
3885
3880
  var APP_MIGRATION_ID_RE = /^[a-z0-9][a-z0-9._-]{0,127}$/;
3886
3881
  var MAX_APP_MIGRATION_IDS = 256;
3887
3882
  var WORKER_OWNED_NOT_FOUND_MIGRATION_ID = "2026-08-worker-owned-not-found";
3883
+ var SECURE_ROOM_BOUNDARIES_MIGRATION_ID = "2026-08-secure-room-boundaries";
3888
3884
  function validateAppMigrationIds(value) {
3889
3885
  if (!Array.isArray(value)) {
3890
3886
  return { valid: false, reason: "app migrations must be a JSON string array" };
@@ -3918,6 +3914,12 @@ var SOURCE_MIGRATIONS = [
3918
3914
  description: "Let the worker answer misses, so deleted files 404 instead of serving HTML",
3919
3915
  transform: migrateWorkerOwnedNotFound,
3920
3916
  findBlockers: findWorkerOwnedNotFoundBlockers
3917
+ },
3918
+ {
3919
+ id: SECURE_ROOM_BOUNDARIES_MIGRATION_ID,
3920
+ description: "Secure room identity, job mutations, and production debug routes",
3921
+ transform: migrateSecureRoomBoundaries,
3922
+ findBlockers: findSecureRoomBoundaryBlockers
3921
3923
  }
3922
3924
  ];
3923
3925
  var APP_MIGRATION_DEFINITIONS = SOURCE_MIGRATIONS.map(
@@ -4061,6 +4063,248 @@ function isSourcePath(repoPath, appPrefix) {
4061
4063
  if (repoPath.endsWith("wrangler.toml")) return true;
4062
4064
  return /\.(?:[cm]?[jt]sx?)$/.test(repoPath);
4063
4065
  }
4066
+ var LEGACY_AUTHENTICATED_ROOM_HELPER = `const IDENTITY_QUERY_PARAMS = ['userId', 'userName', 'userEmail', 'userImageUrl', 'role'] as const
4067
+
4068
+ function authenticatedRoomUrl(
4069
+ requestUrl: string,
4070
+ auth: VerifyResult | null,
4071
+ extraParams?: (auth: VerifyResult) => Record<string, string>,
4072
+ ): URL {
4073
+ const doUrl = new URL(requestUrl)
4074
+ doUrl.searchParams.delete('token')
4075
+ for (const key of IDENTITY_QUERY_PARAMS) {
4076
+ doUrl.searchParams.delete(key)
4077
+ }
4078
+
4079
+ if (!auth) return doUrl
4080
+
4081
+ doUrl.searchParams.set('userId', auth.userId)
4082
+ if (auth.claims.name) doUrl.searchParams.set('userName', auth.claims.name)
4083
+ if (auth.claims.email) doUrl.searchParams.set('userEmail', auth.claims.email)
4084
+ if (auth.claims.image) doUrl.searchParams.set('userImageUrl', auth.claims.image)
4085
+ if (extraParams) {
4086
+ for (const [key, value] of Object.entries(extraParams(auth))) {
4087
+ doUrl.searchParams.set(key, value)
4088
+ }
4089
+ }
4090
+ return doUrl
4091
+ }`;
4092
+ var IDENTITY_ROUTE_REPLACEMENTS = [
4093
+ [
4094
+ `import { verifyJwt } from 'deepspace/worker'`,
4095
+ `import { authenticatedRoomRequest, verifyJwt } from 'deepspace/worker'`,
4096
+ 1
4097
+ ],
4098
+ [LEGACY_AUTHENTICATED_ROOM_HELPER, "", 1],
4099
+ [
4100
+ `function wsRoute(
4101
+ doNamespace: (env: Env) => DurableObjectNamespace,
4102
+ extraParams?: (auth: VerifyResult) => Record<string, string>,
4103
+ ) {`,
4104
+ `function wsRoute(
4105
+ doNamespace: (env: Env) => DurableObjectNamespace,
4106
+ extraIdentity?: (auth: VerifyResult) => { role?: string },
4107
+ ) {`,
4108
+ 1
4109
+ ],
4110
+ [
4111
+ ` const doUrl = authenticatedRoomUrl(c.req.url, auth, extraParams)`,
4112
+ ` const roomRequest = authenticatedRoomRequest(
4113
+ c.req.raw,
4114
+ auth,
4115
+ auth ? extraIdentity?.(auth) : undefined,
4116
+ )`,
4117
+ 1
4118
+ ],
4119
+ [
4120
+ ` const doUrl = authenticatedRoomUrl(c.req.url, auth, () => ({ role }))`,
4121
+ ` const roomRequest = authenticatedRoomRequest(c.req.raw, auth, { role })`,
4122
+ 1
4123
+ ],
4124
+ [
4125
+ ` return stub.fetch(new Request(doUrl.toString(), c.req.raw))`,
4126
+ ` return stub.fetch(roomRequest)`,
4127
+ 2
4128
+ ]
4129
+ ];
4130
+ function occurrences(source, pattern) {
4131
+ return source.split(pattern).length - 1;
4132
+ }
4133
+ var SECURE_JOB_ROOM_CONSTRUCTOR = ` constructor(state: DurableObjectState, env: Env) {
4134
+ super(state, env, {
4135
+ authorizeWrite: async (user) => {
4136
+ if (user.userId.startsWith('anon-')) return false
4137
+ const role = await resolveAppRole(env, user.userId)
4138
+ return role === 'member' || role === 'admin'
4139
+ },
4140
+ })
4141
+ }`;
4142
+ var APP_JOB_ROOM_CLASS = `export class AppJobRoom extends JobRoom<Env> {`;
4143
+ var SECURE_APP_JOB_ROOM = `${APP_JOB_ROOM_CLASS}
4144
+ ${SECURE_JOB_ROOM_CONSTRUCTOR}`;
4145
+ function hasSecureAppJobRoom(source) {
4146
+ return occurrences(source, "class AppJobRoom") === 1 && source.includes(SECURE_APP_JOB_ROOM);
4147
+ }
4148
+ var WORKER_JOB_REPLACEMENTS = [
4149
+ [
4150
+ `import type { DOBindings, DOManifest, Job, JobContext } from 'deepspace/worker'`,
4151
+ `import { resolveAppRole } from 'deepspace/worker'
4152
+ import type { DOBindings, DOManifest, Job, JobContext } from 'deepspace/worker'`
4153
+ ],
4154
+ [
4155
+ `export class AppJobRoom extends JobRoom<Env> {
4156
+ constructor(state: DurableObjectState, env: Env) {
4157
+ super(state, env)
4158
+ }`,
4159
+ `export class AppJobRoom extends JobRoom<Env> {
4160
+ ${SECURE_JOB_ROOM_CONSTRUCTOR}`
4161
+ ]
4162
+ ];
4163
+ var HTTP_ROLE_IMPORT = `import { resolveAppRole } from 'deepspace/worker'`;
4164
+ var HTTP_ROLE_IMPORT_SEAM = `import type { AppContext, Env } from '../../worker.js'`;
4165
+ var DEBUG_ROUTE_SEAM = ` app.all('/api/debug/*', async (c) => {
4166
+ if (c.env.ALLOW_DEBUG_ROUTES !== 'true') {
4167
+ return c.notFound()
4168
+ }
4169
+ const stub = c.env.RECORD_ROOMS.get(`;
4170
+ var DOUBLE_QUOTED_DEBUG_ROUTE_SEAM = DEBUG_ROUTE_SEAM.replace(
4171
+ "app.all('/api/debug/*'",
4172
+ 'app.all("/api/debug/*"'
4173
+ );
4174
+ var SECURE_DEBUG_ROUTE_SEAM = ` app.all('/api/debug/*', async (c) => {
4175
+ if (c.env.ALLOW_DEBUG_ROUTES !== 'true') {
4176
+ return c.notFound()
4177
+ }
4178
+ const auth = await resolveAuth(c.req.raw, c.env)
4179
+ if (!auth) return c.json({ error: 'unauthorized' }, 401)
4180
+ if ((await resolveAppRole(c.env, auth.userId)) !== 'admin') {
4181
+ return c.json({ error: 'forbidden' }, 403)
4182
+ }
4183
+ const stub = c.env.RECORD_ROOMS.get(`;
4184
+ var DOUBLE_QUOTED_SECURE_DEBUG_ROUTE_SEAM = SECURE_DEBUG_ROUTE_SEAM.replace(
4185
+ "app.all('/api/debug/*'",
4186
+ 'app.all("/api/debug/*"'
4187
+ );
4188
+ var DEBUG_ROUTE_REPLACEMENTS = [
4189
+ [DEBUG_ROUTE_SEAM, SECURE_DEBUG_ROUTE_SEAM],
4190
+ [DOUBLE_QUOTED_DEBUG_ROUTE_SEAM, DOUBLE_QUOTED_SECURE_DEBUG_ROUTE_SEAM]
4191
+ ];
4192
+ var DEBUG_ROUTE_PATTERN = /\bapp\.all\(\s*(['"])\/api\/debug\/\*\1\s*,/;
4193
+ function debugRouteOccurrences(source) {
4194
+ return source.match(new RegExp(DEBUG_ROUTE_PATTERN.source, "g"))?.length ?? 0;
4195
+ }
4196
+ function secureDebugRouteOccurrences(source) {
4197
+ return DEBUG_ROUTE_REPLACEMENTS.reduce(
4198
+ (count, [, secure]) => count + occurrences(source, secure),
4199
+ 0
4200
+ );
4201
+ }
4202
+ function hasSecureDebugRoute(source) {
4203
+ return debugRouteOccurrences(source) === 1 && secureDebugRouteOccurrences(source) === 1 && occurrences(source, HTTP_ROLE_IMPORT) === 1;
4204
+ }
4205
+ function migrateSecureRoomBoundaries(source, file) {
4206
+ if (file.endsWith("src/server/realtime-routes.ts")) {
4207
+ return migrateRealtimeRoomBoundaries(source);
4208
+ }
4209
+ if (file.endsWith("worker.ts")) return migrateWorkerJobAuthorization(source);
4210
+ if (file.endsWith("src/server/http-routes.ts")) return migrateDebugRouteAuthorization(source);
4211
+ return { source, replacements: 0 };
4212
+ }
4213
+ function migrateRealtimeRoomBoundaries(source) {
4214
+ let migrated = source;
4215
+ let replacements = 0;
4216
+ if (!migrated.includes("authenticatedRoomRequest(")) {
4217
+ if (IDENTITY_ROUTE_REPLACEMENTS.some(
4218
+ ([before, , expected]) => occurrences(migrated, before) !== expected
4219
+ )) {
4220
+ return { source, replacements: 0 };
4221
+ }
4222
+ for (const [before, after, expected] of IDENTITY_ROUTE_REPLACEMENTS) {
4223
+ migrated = migrated.replaceAll(before, after);
4224
+ replacements += expected;
4225
+ }
4226
+ }
4227
+ return { source: migrated, replacements };
4228
+ }
4229
+ function migrateWorkerJobAuthorization(source) {
4230
+ if (!source.includes("class AppJobRoom")) return { source, replacements: 0 };
4231
+ if (hasSecureAppJobRoom(source)) return { source, replacements: 0 };
4232
+ if (WORKER_JOB_REPLACEMENTS.some(([before]) => occurrences(source, before) !== 1)) {
4233
+ return { source, replacements: 0 };
4234
+ }
4235
+ let migrated = source;
4236
+ for (const [before, after] of WORKER_JOB_REPLACEMENTS) migrated = migrated.replace(before, after);
4237
+ return { source: migrated, replacements: WORKER_JOB_REPLACEMENTS.length };
4238
+ }
4239
+ function migrateDebugRouteAuthorization(source) {
4240
+ if (debugRouteOccurrences(source) === 0 || hasSecureDebugRoute(source)) {
4241
+ return { source, replacements: 0 };
4242
+ }
4243
+ const matchingRoutes = DEBUG_ROUTE_REPLACEMENTS.filter(
4244
+ ([before2]) => occurrences(source, before2) === 1
4245
+ );
4246
+ if (debugRouteOccurrences(source) !== 1 || matchingRoutes.length !== 1 || occurrences(source, HTTP_ROLE_IMPORT) !== 0 || occurrences(source, HTTP_ROLE_IMPORT_SEAM) !== 1) {
4247
+ return { source, replacements: 0 };
4248
+ }
4249
+ const [before, after] = matchingRoutes[0];
4250
+ return {
4251
+ source: source.replace(HTTP_ROLE_IMPORT_SEAM, `${HTTP_ROLE_IMPORT_SEAM}
4252
+ ${HTTP_ROLE_IMPORT}`).replace(before, after),
4253
+ replacements: 2
4254
+ };
4255
+ }
4256
+ function findSecureRoomBoundaryBlockers(source, file) {
4257
+ if (file.endsWith("worker.ts") && source.includes("class AppJobRoom")) {
4258
+ if (!hasSecureAppJobRoom(source)) {
4259
+ return [
4260
+ roomBoundaryBlocker(
4261
+ file,
4262
+ source,
4263
+ "class AppJobRoom",
4264
+ "Configure AppJobRoom with authorizeWrite using the current users.role."
4265
+ )
4266
+ ];
4267
+ }
4268
+ return [];
4269
+ }
4270
+ if (file.endsWith("src/server/http-routes.ts") && debugRouteOccurrences(source) > 0) {
4271
+ if (!hasSecureDebugRoute(source)) {
4272
+ const route = source.match(DEBUG_ROUTE_PATTERN)?.[0] ?? "/api/debug/*";
4273
+ return [
4274
+ roomBoundaryBlocker(
4275
+ file,
4276
+ source,
4277
+ route,
4278
+ "Require verified owner/admin access inside the enabled debug-route handler."
4279
+ )
4280
+ ];
4281
+ }
4282
+ return [];
4283
+ }
4284
+ if (!file.endsWith("src/server/realtime-routes.ts")) return [];
4285
+ const lines = source.split("\n");
4286
+ let index = lines.findIndex(
4287
+ (line) => line.includes("authenticatedRoomUrl") || /searchParams\.(?:set|append)\(\s*['"](?:userId|userName|userEmail|userImageUrl|role)['"]/.test(
4288
+ line
4289
+ )
4290
+ );
4291
+ if (index < 0 && /\bauth\.(?:userId|claims)\b/.test(source)) {
4292
+ index = lines.findIndex((line) => /searchParams\.(?:set|append)\s*\(/.test(line));
4293
+ }
4294
+ if (index < 0) return [];
4295
+ return [
4296
+ {
4297
+ migrationId: SECURE_ROOM_BOUNDARIES_MIGRATION_ID,
4298
+ file,
4299
+ line: index + 1,
4300
+ message: "This customized room proxy still places verified user identity in its internal URL. Replace that forwarding code with authenticatedRoomRequest() from deepspace/worker before the Durable Object fetch."
4301
+ }
4302
+ ];
4303
+ }
4304
+ function roomBoundaryBlocker(file, source, seam, message) {
4305
+ const line = source.slice(0, source.indexOf(seam)).split("\n").length;
4306
+ return { migrationId: SECURE_ROOM_BOUNDARIES_MIGRATION_ID, file, line, message };
4307
+ }
4064
4308
  function migrateWorkerOwnedNotFound(source, file) {
4065
4309
  let replacements = 0;
4066
4310
  if (file.endsWith("wrangler.toml")) {
@@ -4126,6 +4370,16 @@ function findWorkerOwnedNotFoundBlockers(source, file) {
4126
4370
 
4127
4371
  // src/cli/commands/deploy/build.ts
4128
4372
  var RESERVED_RUN_WORKER_FIRST2 = new Set(RESERVED_RUN_WORKER_FIRST);
4373
+ function secureBuildDevVars(workerDir) {
4374
+ const path = join16(workerDir, ".dev.vars");
4375
+ if (!existsSync14(path)) return false;
4376
+ const stat = lstatSync2(path);
4377
+ if (!stat.isFile() || stat.isSymbolicLink()) {
4378
+ throw new Error(`Refusing unsafe build secret path: ${path}`);
4379
+ }
4380
+ chmodSync2(path, 384);
4381
+ return true;
4382
+ }
4129
4383
  function declaredCompatibilityDate(config) {
4130
4384
  const declared = config.compatibility_date;
4131
4385
  return typeof declared === "string" && /^\d{4}-\d{2}-\d{2}$/.test(declared) ? declared : null;
@@ -4182,6 +4436,11 @@ async function buildDeployBundle(options) {
4182
4436
  );
4183
4437
  }
4184
4438
  const workerDir = dirname6(outputWranglerPath);
4439
+ try {
4440
+ secureBuildDevVars(workerDir);
4441
+ } catch (error) {
4442
+ output.die(errorMessage(error), "build_output_unsafe");
4443
+ }
4185
4444
  const workerBundlePath = join16(workerDir, outputConfig.main);
4186
4445
  const clientDir = outputConfig.assets?.directory ? resolve11(workerDir, outputConfig.assets.directory) : null;
4187
4446
  if (!existsSync14(workerBundlePath)) {
@@ -4455,7 +4714,7 @@ async function syncSubscriptionPlans(appDir, appName, token, nonInteractive) {
4455
4714
  spinner2.stop(`Synced ${plans.length} plan${plans.length === 1 ? "" : "s"} to Stripe`);
4456
4715
  if (body.connectMissing) {
4457
4716
  p7.log.warn(
4458
- "Stripe Connect not yet set up. Customers can subscribe and earnings will queue in your platform balance until you finish onboarding at /earnings on the dashboard. The first payout after you connect will sweep everything that accumulated in the meantime."
4717
+ "Stripe Connect is not ready. Paid checkout will remain unavailable until the app owner finishes onboarding at /earnings on the dashboard."
4459
4718
  );
4460
4719
  }
4461
4720
  const changes = (body.planChanges ?? []).filter((change) => change.affectedSubscribers > 0);
@@ -5096,7 +5355,16 @@ function selectGitHubRemote(cwd, options = {}) {
5096
5355
  return matching.find((remote) => remote.name === "origin") ?? (matching.length === 1 ? matching[0] : null);
5097
5356
  }
5098
5357
  function remotePublicRefs(cwd, remote) {
5099
- const output = runGit(cwd, ["ls-remote", "--refs", remote]).stdout.toString("utf-8");
5358
+ let output;
5359
+ try {
5360
+ output = runGit(cwd, ["ls-remote", "--refs", remote]).stdout.toString("utf-8");
5361
+ } catch (error) {
5362
+ if (!isGitCredentialFailure(error)) throw error;
5363
+ throw new GitError(
5364
+ "Git could not authenticate the remote. In a container, forward an SSH agent or use an ephemeral Git credential helper, then retry. Do not put tokens in remote URLs.",
5365
+ "github_credentials_required"
5366
+ );
5367
+ }
5100
5368
  return output.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
5101
5369
  const [oid, name] = line.split(/\s+/, 2);
5102
5370
  return { oid, name };
@@ -5104,6 +5372,12 @@ function remotePublicRefs(cwd, remote) {
5104
5372
  (ref) => /^[0-9a-f]{40}$/.test(ref.oid) && (ref.name.startsWith("refs/heads/") || ref.name.startsWith("refs/tags/"))
5105
5373
  );
5106
5374
  }
5375
+ function isGitCredentialFailure(error) {
5376
+ if (!(error instanceof Error)) return false;
5377
+ return /authentication failed|could not read username|permission denied \(publickey\)|repository not found|terminal prompts disabled/i.test(
5378
+ error.message
5379
+ );
5380
+ }
5107
5381
  function remoteBranchOid(cwd, remote, branch) {
5108
5382
  return remotePublicRefs(cwd, remote).find((ref) => ref.name === `refs/heads/${branch}`)?.oid ?? null;
5109
5383
  }
@@ -5145,6 +5419,25 @@ function setAppSource(deployUrl, token, appId, input) {
5145
5419
  }
5146
5420
 
5147
5421
  // src/cli/commands/deploy/repository.ts
5422
+ function preflightDeployRepository(options) {
5423
+ const { appDir, push, source } = options;
5424
+ if (!push || source?.provider === "github") return null;
5425
+ try {
5426
+ assertSyncableRepo(appDir);
5427
+ if (!source && listGitHubRemotes(appDir).length > 0) {
5428
+ return {
5429
+ code: "source_unclaimed",
5430
+ error: "This app has a GitHub remote but no claimed source. Choose once with `deepspace app source github` (manual GitHub ownership) or `deepspace app source deepspace` (packaged DeepSpace source), then deploy again."
5431
+ };
5432
+ }
5433
+ const branch = currentBranch(appDir);
5434
+ if (!isWorkTreeClean(appDir)) return dirtyWorktreeRefusal(branch);
5435
+ if (!branch) return detachedHeadRefusal();
5436
+ return null;
5437
+ } catch (error) {
5438
+ return deployRepositoryFailure(error, appDir);
5439
+ }
5440
+ }
5148
5441
  async function syncDeployRepository(options) {
5149
5442
  const { deployUrl, appDir, appId, token, push, ignoreStale, output, sourceState } = options;
5150
5443
  let commitOid = null;
@@ -5165,21 +5458,12 @@ async function syncDeployRepository(options) {
5165
5458
  return { commitOid, recoverable, deployKey, source, sourceRevision, baseReleaseId };
5166
5459
  }
5167
5460
  try {
5168
- assertSyncableRepo(appDir);
5169
- if (!source && listGitHubRemotes(appDir).length > 0) {
5170
- output.die(
5171
- "This app has a GitHub remote but no claimed source. Choose once with `deepspace app source github` (manual GitHub ownership) or `deepspace app source deepspace` (packaged DeepSpace source), then deploy again.",
5172
- "source_unclaimed"
5173
- );
5174
- }
5461
+ const preflight = preflightDeployRepository({ appDir, push, source });
5462
+ if (preflight) output.die(preflight.error, preflight.code);
5175
5463
  const sourceRemote = spaceRemoteName();
5176
5464
  ensureSpaceRemote(appDir, appId, sourceRemote);
5177
5465
  const branch = currentBranch(appDir);
5178
5466
  const workspaceBranchId = workspaceIdFromBranch(branch);
5179
- if (!isWorkTreeClean(appDir)) {
5180
- const refusal = dirtyWorktreeRefusal(branch);
5181
- output.die(refusal.error, refusal.code);
5182
- }
5183
5467
  if (!branch) {
5184
5468
  const refusal = detachedHeadRefusal();
5185
5469
  output.die(refusal.error, refusal.code);
@@ -5281,12 +5565,7 @@ function classifyRemoteState(appDir, token, branch, localTip, remote) {
5281
5565
  let remoteTip = null;
5282
5566
  const remoteRef = `refs/remotes/${remote}/${branch}`;
5283
5567
  try {
5284
- runGitRemote(appDir, token, [
5285
- "fetch",
5286
- "--quiet",
5287
- remote,
5288
- `+refs/heads/${branch}:${remoteRef}`
5289
- ]);
5568
+ runGitRemote(appDir, token, ["fetch", "--quiet", remote, `+refs/heads/${branch}:${remoteRef}`]);
5290
5569
  remoteTip = resolveCommit(appDir, remoteRef);
5291
5570
  return {
5292
5571
  remoteTip,
@@ -5860,6 +6139,14 @@ var deploy_default = defineCommand3({
5860
6139
  output
5861
6140
  });
5862
6141
  const sourceState = await getAppSource(DEPLOY_URL3, token, appId);
6142
+ const repositoryPreflight = preflightDeployRepository({
6143
+ appDir,
6144
+ push: args.push !== false,
6145
+ source: sourceState.source
6146
+ });
6147
+ if (repositoryPreflight) {
6148
+ output.die(repositoryPreflight.error, repositoryPreflight.code);
6149
+ }
5863
6150
  const spinner2 = createSpinner();
5864
6151
  const bundle = await buildDeployBundle({
5865
6152
  appDir,
@@ -6322,11 +6609,17 @@ function parseSince(input, now = Date.now()) {
6322
6609
  } else {
6323
6610
  ts = Date.parse(trimmed);
6324
6611
  if (Number.isNaN(ts)) {
6325
- throw new InputError(`Invalid --since "${input}" \u2014 use 30s, 15m, 2h, 7d, or an ISO timestamp.`, "invalid_since");
6612
+ throw new InputError(
6613
+ `Invalid --since "${input}" \u2014 use 30s, 15m, 2h, 7d, or an ISO timestamp.`,
6614
+ "invalid_since"
6615
+ );
6326
6616
  }
6327
6617
  }
6328
6618
  if (now - ts > RETENTION_MS) {
6329
- throw new InputError("Logs are retained for 7 days \u2014 --since can reach back at most 7d.", "since_out_of_range");
6619
+ throw new InputError(
6620
+ "Logs are retained for 7 days \u2014 --since can reach back at most 7d.",
6621
+ "since_out_of_range"
6622
+ );
6330
6623
  }
6331
6624
  return ts;
6332
6625
  }
@@ -6457,13 +6750,19 @@ var logs_default = defineCommand4({
6457
6750
  const token = await ensureToken();
6458
6751
  const appId = await resolveAppTarget(DEPLOY_URL6, token, args.app, { wranglerEnv: envArg });
6459
6752
  if (args.level && !LOG_LEVELS.includes(args.level)) {
6460
- throw new InputError(`Invalid --level "${args.level}". Use: ${LOG_LEVELS.join(", ")}`, "invalid_level");
6753
+ throw new InputError(
6754
+ `Invalid --level "${args.level}". Use: ${LOG_LEVELS.join(", ")}`,
6755
+ "invalid_level"
6756
+ );
6461
6757
  }
6462
6758
  let limit;
6463
6759
  if (args.limit !== void 0) {
6464
6760
  limit = Number(args.limit);
6465
6761
  if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
6466
- throw new InputError("Invalid --limit \u2014 expected an integer between 1 and 500.", "invalid_limit");
6762
+ throw new InputError(
6763
+ "Invalid --limit \u2014 expected an integer between 1 and 500.",
6764
+ "invalid_limit"
6765
+ );
6467
6766
  }
6468
6767
  }
6469
6768
  const initialSince = args.since ? parseSince(args.since) : Date.now() - DEFAULT_WINDOW_MS;
@@ -6479,7 +6778,9 @@ var logs_default = defineCommand4({
6479
6778
  };
6480
6779
  const print = (events) => {
6481
6780
  for (const e of events) {
6482
- process.stdout.write(args.json ? JSON.stringify(e) + "\n" : formatEvent(e, stdoutColor()) + "\n");
6781
+ process.stdout.write(
6782
+ args.json ? JSON.stringify(e) + "\n" : formatEvent(e, stdoutColor()) + "\n"
6783
+ );
6483
6784
  }
6484
6785
  };
6485
6786
  const first = await fetchPage(initialSince, args.follow ? followLimit : limit, token);
@@ -6487,15 +6788,25 @@ var logs_default = defineCommand4({
6487
6788
  if (!args.follow) {
6488
6789
  if (first.events.length === 0) {
6489
6790
  if (args.json) {
6490
- process.stdout.write(JSON.stringify({ type: "meta", count: 0, window: windowLabel }) + "\n");
6791
+ process.stdout.write(
6792
+ JSON.stringify({ type: "meta", count: 0, window: windowLabel }) + "\n"
6793
+ );
6491
6794
  } else {
6492
6795
  console.log(
6493
6796
  `No logs in the last ${windowLabel} for ${appId}. New logs can take up to a minute to appear; retention is 7 days.`
6494
6797
  );
6495
6798
  }
6496
6799
  } else if (first.truncated) {
6497
- if (args.json) process.stdout.write(JSON.stringify({ type: "meta", truncated: true }) + "\n");
6498
- else console.error(`(showing the newest ${first.events.length} events \u2014 narrow with --since/--level/--search)`);
6800
+ if (args.json)
6801
+ process.stdout.write(JSON.stringify({ type: "meta", truncated: true }) + "\n");
6802
+ else if (args.search)
6803
+ console.error(
6804
+ `(showing the newest ${first.events.length} matches \u2014 search scans at most the newest 500 level-filtered events per fetch; narrow with --since/--level or raise --limit)`
6805
+ );
6806
+ else
6807
+ console.error(
6808
+ `(showing the newest ${first.events.length} events \u2014 narrow with --since/--level)`
6809
+ );
6499
6810
  }
6500
6811
  return;
6501
6812
  }
@@ -6515,7 +6826,11 @@ var logs_default = defineCommand4({
6515
6826
  process.stdout.write(JSON.stringify({ type: "meta", truncated: true }) + "\n");
6516
6827
  } else {
6517
6828
  console.error(
6518
- paint("33", `(more than ${followLimit} events in the initial ${windowLabel} window \u2014 some older events not shown; narrow with --since/--level/--search)`, stderrColor())
6829
+ paint(
6830
+ "33",
6831
+ args.search ? `(search scanned only the newest ${MAX_LIMIT} level-filtered events in the initial ${windowLabel} window \u2014 older matches may be omitted; narrow with --since/--level)` : `(more than ${followLimit} events in the initial ${windowLabel} window \u2014 some older events not shown; narrow with --since/--level)`,
6832
+ stderrColor()
6833
+ )
6519
6834
  );
6520
6835
  }
6521
6836
  }
@@ -6529,21 +6844,27 @@ var logs_default = defineCommand4({
6529
6844
  if (err instanceof ApiError && [401, 403, 404].includes(err.status)) throw err;
6530
6845
  backoff = Math.min(backoff ? backoff * 2 : MIN_POLL_MS, 3e4);
6531
6846
  const msg = err instanceof Error ? err.message : String(err);
6532
- console.error(paint("2", `(logs fetch failed: ${msg} \u2014 retrying in ${backoff / 1e3}s)`, stderrColor()));
6847
+ console.error(
6848
+ paint("2", `(logs fetch failed: ${msg} \u2014 retrying in ${backoff / 1e3}s)`, stderrColor())
6849
+ );
6533
6850
  continue;
6534
6851
  }
6535
6852
  backoff = 0;
6536
6853
  interval = Math.max(page.pollIntervalMs || 0, MIN_POLL_MS);
6537
6854
  const fresh = seen.fresh(page.events);
6538
6855
  print(fresh);
6539
- const realDrop = fresh.length >= followLimit;
6856
+ const realDrop = fresh.length >= followLimit || Boolean(args.search && page.truncated);
6540
6857
  if (realDrop && !warnedTruncated) {
6541
6858
  warnedTruncated = true;
6542
6859
  if (args.json) {
6543
6860
  process.stdout.write(JSON.stringify({ type: "meta", truncated: true }) + "\n");
6544
6861
  } else {
6545
6862
  console.error(
6546
- paint("33", `(burst exceeded ${followLimit} events/poll \u2014 some may be dropped; narrow with --level/--search)`, stderrColor())
6863
+ paint(
6864
+ "33",
6865
+ args.search ? `(search scanned only the newest ${MAX_LIMIT} level-filtered events this poll \u2014 older matches may be omitted; narrow with --level)` : `(burst exceeded ${followLimit} events/poll \u2014 some may be dropped; narrow with --level)`,
6866
+ stderrColor()
6867
+ )
6547
6868
  );
6548
6869
  }
6549
6870
  } else if (!realDrop) {
@@ -9368,7 +9689,7 @@ function ok(json, data, human) {
9368
9689
  function warnIfDebugRoutesEnabled(secrets) {
9369
9690
  if (secrets.ALLOW_DEBUG_ROUTES !== "true") return false;
9370
9691
  console.warn(
9371
- "\n\u26A0 ALLOW_DEBUG_ROUTES=true exposes an UNAUTHENTICATED debug API on the deployed app \u2014\n anyone who can reach /api/debug/* can read and mutate any record. Redeploy to apply;\n `deepspace secrets delete ALLOW_DEBUG_ROUTES` (then redeploy) turns it back off."
9692
+ "\n\u26A0 ALLOW_DEBUG_ROUTES=true enables the owner/admin debug API on the deployed app \u2014\n those users can read and mutate any app record through /api/debug/*. Redeploy to apply;\n `deepspace secrets delete ALLOW_DEBUG_ROUTES` (then redeploy) turns it back off."
9372
9693
  );
9373
9694
  return true;
9374
9695
  }
@@ -11137,6 +11458,11 @@ function appDirInWorktree(appDir, worktreeRoot) {
11137
11458
  const appRelative = relative2(realpathSync6(repoToplevel(appDir)), realpathSync6(appDir));
11138
11459
  return appRelative ? join22(worktreeRoot, appRelative) : worktreeRoot;
11139
11460
  }
11461
+ function primaryAppDir(appDir) {
11462
+ const primary = listWorktrees(appDir)[0];
11463
+ if (!primary) throw new Error("Git did not report a primary worktree");
11464
+ return appDirInWorktree(appDir, primary.path);
11465
+ }
11140
11466
  function materializeWorkspaceWorktree(appDir, worktreeRoot, branch, startPoint, workspaceId, options = {}) {
11141
11467
  const createdOid = resolveCommit(appDir, startPoint);
11142
11468
  if (!createdOid) throw new Error(`Could not resolve workspace start point ${startPoint}`);
@@ -12163,6 +12489,11 @@ function conflictMarkerFiles(diffCheckOutput) {
12163
12489
  }
12164
12490
  return [...files];
12165
12491
  }
12492
+ function pullAfterLandAction(primaryDir, intoBranch, landedOid) {
12493
+ if (currentBranch(primaryDir) !== intoBranch) return null;
12494
+ if (resolveCommit(primaryDir, "HEAD") === landedOid) return null;
12495
+ return { cwd: primaryDir, argv: ["deepspace", "pull"] };
12496
+ }
12166
12497
  function landResumeArgv(args) {
12167
12498
  const argv = ["deepspace", "workspace", "land"];
12168
12499
  const workspace = args.workspace?.trim();
@@ -12441,6 +12772,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12441
12772
  );
12442
12773
  }
12443
12774
  spinner2?.stop(`Landed ${id} into ${intoBranch} at ${landedOid.slice(0, 10)}.`);
12775
+ const primaryDir = primaryAppDir(appDir);
12444
12776
  const cleanup = keepWorktree ? null : cleanupWorkspaceLocal(appDir, id, intoBranch, { expectedBranchOid: landedOid });
12445
12777
  const inOwnWorktree = !cleanup && inOwnLinkedWorktree(appDir, id);
12446
12778
  const retainedWorktree = cleanup?.worktreeRetained ?? (keepWorktree || inOwnWorktree ? appDir : null);
@@ -12458,7 +12790,7 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12458
12790
  p30.log.info(`Retained checkout: ${retainedWorktree}`);
12459
12791
  }
12460
12792
  if (cleanup?.error) {
12461
- const action = cleanupAction({
12793
+ const action2 = cleanupAction({
12462
12794
  worktreeDir: cleanup.worktreeDir,
12463
12795
  branch: cleanup.branch
12464
12796
  });
@@ -12469,10 +12801,11 @@ var landWorkspaceCommand = defineDeepspaceCommand({
12469
12801
  branch: cleanup.branch
12470
12802
  }),
12471
12803
  "cleanup_incomplete",
12472
- { actionRequired: true, action, extra: data }
12804
+ { actionRequired: true, action: action2, extra: data }
12473
12805
  );
12474
12806
  }
12475
- return { data };
12807
+ const action = pullAfterLandAction(primaryDir, intoBranch, landedOid);
12808
+ return { data, ...action ? { action } : {} };
12476
12809
  }
12477
12810
  });
12478
12811
 
@@ -12912,7 +13245,7 @@ var source_default = defineDeepspaceCommand({
12912
13245
  args: {
12913
13246
  provider: {
12914
13247
  type: "positional",
12915
- description: "Source provider: github or deepspace (omit to show)",
13248
+ description: "Source authority: deepspace is platform-hosted and commit-first; github deploys the local working tree without Git sync (omit to show)",
12916
13249
  required: false
12917
13250
  },
12918
13251
  app: {