timeback 0.2.2-beta.20260401002024 → 0.2.2-beta.20260402000431

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 (3) hide show
  1. package/dist/cli.js +310 -121
  2. package/package.json +1 -1
  3. package/schema.json +79 -112
package/dist/cli.js CHANGED
@@ -26823,7 +26823,7 @@ function date4(params) {
26823
26823
 
26824
26824
  // ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
26825
26825
  config(en_default());
26826
- // ../clients/core/dist/chunk-jqy7m30q.js
26826
+ // ../clients/core/dist/chunk-bq65zn6q.js
26827
26827
  var log3 = createScopedLogger("edubridge");
26828
26828
  function normalizeBoolean(value) {
26829
26829
  if (typeof value === "boolean")
@@ -26995,6 +26995,13 @@ var StringTimebackGrade = exports_external.string().transform((value, ctx) => {
26995
26995
  return exports_external.NEVER;
26996
26996
  }
26997
26997
  const stripped = raw.replace(/\bgrade\b/g, "").replace(/(\d+)(st|nd|rd|th)\b/g, "$1").trim();
26998
+ if (stripped === "") {
26999
+ ctx.addIssue({
27000
+ code: "custom",
27001
+ message: "must be a valid Timeback grade"
27002
+ });
27003
+ return exports_external.NEVER;
27004
+ }
26998
27005
  if (stripped === "pre-k" || stripped === "pk") {
26999
27006
  return -1;
27000
27007
  }
@@ -33251,6 +33258,13 @@ var StringTimebackGrade2 = exports_external.string().transform((value, ctx) => {
33251
33258
  return exports_external.NEVER;
33252
33259
  }
33253
33260
  const stripped = raw.replace(/\bgrade\b/g, "").replace(/(\d+)(st|nd|rd|th)\b/g, "$1").trim();
33261
+ if (stripped === "") {
33262
+ ctx.addIssue({
33263
+ code: "custom",
33264
+ message: "must be a valid Timeback grade"
33265
+ });
33266
+ return exports_external.NEVER;
33267
+ }
33254
33268
  if (stripped === "pre-k" || stripped === "pk") {
33255
33269
  return -1;
33256
33270
  }
@@ -35111,8 +35125,13 @@ var CredentialsSchema = exports_external.object({
35111
35125
  clientSecret: NonEmptyString2,
35112
35126
  email: exports_external.email("Valid email is required").optional()
35113
35127
  });
35114
- var ClientIdSchema = exports_external.string().min(1, "Client ID is required").regex(/^[a-z0-9]+$/, "Client ID must contain only lowercase letters and numbers");
35115
- var ClientSecretSchema = exports_external.string().min(1, "Client secret is required").regex(/^[a-z0-9]+$/, "Client secret must contain only lowercase letters and numbers").max(53, "Client secret must be less than 53 characters");
35128
+ var StoredCredentialsSchema = exports_external.object({
35129
+ clientId: NonEmptyString2.optional(),
35130
+ clientSecret: NonEmptyString2.optional(),
35131
+ email: exports_external.email("Valid email is required").optional()
35132
+ }).refine((value) => value.email || value.clientId && value.clientSecret, "Saved credentials must include an email or a client ID/client secret pair").refine((value) => Boolean(value.clientId) === Boolean(value.clientSecret), "Saved credentials must include both client ID and client secret");
35133
+ var ClientIdSchema = exports_external.string().min(1, "Client ID is required").max(128, "Client ID must be at most 128 characters").regex(/^[\w+]+$/, "Client ID must contain only letters, digits, underscores, or +");
35134
+ var ClientSecretSchema = exports_external.string().min(24, "Client secret must be at least 24 characters").max(64, "Client secret must be at most 64 characters").regex(/^[\w+]+$/, "Client secret must contain only letters, digits, underscores, or +");
35116
35135
  var MasteryTrackCredentialsSchema = exports_external.object({
35117
35136
  apiKey: NonEmptyString2,
35118
35137
  email: exports_external.email("Valid email is required")
@@ -36344,24 +36363,41 @@ async function writeCredentialsStore(store) {
36344
36363
  await writeFile(CREDENTIALS_FILE, JSON.stringify(cleaned, null, 2), { mode: 384 });
36345
36364
  return true;
36346
36365
  }
36347
- async function getSavedCredentials(environment) {
36348
- const store = await readCredentialsStore();
36366
+ function getStoredCredentialsFromStore(store, environment) {
36349
36367
  const namespaced = store.timeback?.[environment];
36350
36368
  if (namespaced) {
36351
- const result = CredentialsSchema.safeParse(namespaced);
36369
+ const result = StoredCredentialsSchema.safeParse(namespaced);
36352
36370
  if (result.success) {
36353
36371
  return result.data;
36354
36372
  }
36355
36373
  }
36356
36374
  const legacy = store[environment];
36357
36375
  if (legacy) {
36358
- const result = CredentialsSchema.safeParse(legacy);
36376
+ const result = StoredCredentialsSchema.safeParse(legacy);
36359
36377
  if (result.success) {
36360
36378
  return result.data;
36361
36379
  }
36362
36380
  }
36363
36381
  return null;
36364
36382
  }
36383
+ async function getStoredCredentials(environment) {
36384
+ const store = await readCredentialsStore();
36385
+ return getStoredCredentialsFromStore(store, environment);
36386
+ }
36387
+ function toCredentials(stored) {
36388
+ if (stored?.clientId && stored.clientSecret) {
36389
+ return {
36390
+ clientId: stored.clientId,
36391
+ clientSecret: stored.clientSecret,
36392
+ email: stored.email
36393
+ };
36394
+ }
36395
+ return null;
36396
+ }
36397
+ async function getSavedCredentials(environment) {
36398
+ const stored = await getStoredCredentials(environment);
36399
+ return toCredentials(stored);
36400
+ }
36365
36401
  async function saveCredentials(environment, credentials) {
36366
36402
  const store = await readCredentialsStore();
36367
36403
  store.timeback = store.timeback ?? {};
@@ -36369,6 +36405,44 @@ async function saveCredentials(environment, credentials) {
36369
36405
  delete store[environment];
36370
36406
  return writeCredentialsStore(store);
36371
36407
  }
36408
+ async function saveCredentialEmail(environment, email3) {
36409
+ const store = await readCredentialsStore();
36410
+ const existing = getStoredCredentialsFromStore(store, environment);
36411
+ if (email3) {
36412
+ store.timeback = store.timeback ?? {};
36413
+ store.timeback[environment] = {
36414
+ ...existing ?? {},
36415
+ email: email3
36416
+ };
36417
+ } else if (existing?.clientId && existing.clientSecret) {
36418
+ store.timeback = store.timeback ?? {};
36419
+ store.timeback[environment] = {
36420
+ clientId: existing.clientId,
36421
+ clientSecret: existing.clientSecret
36422
+ };
36423
+ } else {
36424
+ delete store.timeback?.[environment];
36425
+ if (store.timeback && Object.keys(store.timeback).length === 0) {
36426
+ delete store.timeback;
36427
+ }
36428
+ }
36429
+ delete store[environment];
36430
+ return writeCredentialsStore(store);
36431
+ }
36432
+ async function getStoredCredentialEmail(environment) {
36433
+ const stored = await getStoredCredentials(environment);
36434
+ return stored?.email;
36435
+ }
36436
+ async function getEnvironmentsWithStoredEmail() {
36437
+ const store = await readCredentialsStore();
36438
+ const environments = [];
36439
+ for (const env2 of ENVIRONMENTS) {
36440
+ if (getStoredCredentialsFromStore(store, env2)?.email) {
36441
+ environments.push(env2);
36442
+ }
36443
+ }
36444
+ return environments;
36445
+ }
36372
36446
  async function clearCredentials(environment) {
36373
36447
  const store = await readCredentialsStore();
36374
36448
  if (environment) {
@@ -36389,18 +36463,23 @@ async function getConfiguredEnvironments() {
36389
36463
  const store = await readCredentialsStore();
36390
36464
  const configured = [];
36391
36465
  for (const env2 of ENVIRONMENTS) {
36392
- const namespaced = store.timeback?.[env2];
36393
- if (namespaced && CredentialsSchema.safeParse(namespaced).success) {
36394
- configured.push(env2);
36395
- continue;
36396
- }
36397
- const legacy = store[env2];
36398
- if (legacy && CredentialsSchema.safeParse(legacy).success) {
36466
+ if (toCredentials(getStoredCredentialsFromStore(store, env2))) {
36399
36467
  configured.push(env2);
36400
36468
  }
36401
36469
  }
36402
36470
  return configured;
36403
36471
  }
36472
+ async function getAvailableEnvironments() {
36473
+ const available = await getConfiguredEnvironments();
36474
+ if (getEnvCredentials()) {
36475
+ for (const env2 of ENVIRONMENTS) {
36476
+ if (!available.includes(env2)) {
36477
+ available.push(env2);
36478
+ }
36479
+ }
36480
+ }
36481
+ return available;
36482
+ }
36404
36483
  function getEnvCredentials() {
36405
36484
  const clientId = process.env.TIMEBACK_API_CLIENT_ID ?? process.env.TIMEBACK_CLIENT_ID;
36406
36485
  const clientSecret = process.env.TIMEBACK_API_CLIENT_SECRET ?? process.env.TIMEBACK_CLIENT_SECRET;
@@ -36414,10 +36493,12 @@ function getEnvCredentials() {
36414
36493
  }
36415
36494
  async function loadCredentials(environment) {
36416
36495
  const envCreds = getEnvCredentials();
36496
+ const storedCreds = await getStoredCredentials(environment);
36417
36497
  if (envCreds) {
36418
- return { success: true, credentials: envCreds, source: "env" };
36498
+ const merged = storedCreds?.email ? { ...envCreds, email: storedCreds.email } : envCreds;
36499
+ return { success: true, credentials: merged, source: "env" };
36419
36500
  }
36420
- const savedCreds = await getSavedCredentials(environment);
36501
+ const savedCreds = toCredentials(storedCreds);
36421
36502
  if (savedCreds) {
36422
36503
  return { success: true, credentials: savedCreds, source: "saved" };
36423
36504
  }
@@ -39302,7 +39383,7 @@ You need to configure your Timeback API credentials first.`, "Credentials Setup"
39302
39383
  }
39303
39384
  async function runImportFlow(opts = {}) {
39304
39385
  const { format: format2 = true } = opts;
39305
- let configuredEnvs = await getConfiguredEnvironments();
39386
+ let configuredEnvs = await getAvailableEnvironments();
39306
39387
  if (configuredEnvs.length === 0) {
39307
39388
  const setupResult = await setupCredentials();
39308
39389
  if (setupResult === "cancelled") {
@@ -39311,7 +39392,7 @@ async function runImportFlow(opts = {}) {
39311
39392
  if (setupResult === "error") {
39312
39393
  return { success: false, error: "Credential validation failed" };
39313
39394
  }
39314
- configuredEnvs = await getConfiguredEnvironments();
39395
+ configuredEnvs = await getAvailableEnvironments();
39315
39396
  if (configuredEnvs.length === 0) {
39316
39397
  return { success: false, error: "Failed to configure credentials" };
39317
39398
  }
@@ -40652,6 +40733,13 @@ var StringTimebackGrade3 = exports_external.string().transform((value, ctx) => {
40652
40733
  return exports_external.NEVER;
40653
40734
  }
40654
40735
  const stripped = raw.replace(/\bgrade\b/g, "").replace(/(\d+)(st|nd|rd|th)\b/g, "$1").trim();
40736
+ if (stripped === "") {
40737
+ ctx.addIssue({
40738
+ code: "custom",
40739
+ message: "must be a valid Timeback grade"
40740
+ });
40741
+ return exports_external.NEVER;
40742
+ }
40655
40743
  if (stripped === "pre-k" || stripped === "pk") {
40656
40744
  return -1;
40657
40745
  }
@@ -47224,11 +47312,15 @@ function exitCancelled(isOverwriting, exitOnComplete) {
47224
47312
  // src/commands/credentials/email.ts
47225
47313
  async function updateEmail(options = {}) {
47226
47314
  const { exitOnComplete = true, env: envOption } = options;
47227
- const configuredEnvs = [];
47228
- for (const env2 of ENVIRONMENTS) {
47229
- const creds = await getSavedCredentials(env2);
47230
- if (creds)
47231
- configuredEnvs.push(env2);
47315
+ const emailEnvs = await getEnvironmentsWithStoredEmail();
47316
+ const requestedEnv = envOption && ENVIRONMENTS.includes(envOption) ? envOption : undefined;
47317
+ let configuredEnvs;
47318
+ if (requestedEnv) {
47319
+ const hasStoredEmail = await getStoredCredentialEmail(requestedEnv);
47320
+ const hasUsableCredentials = (await loadCredentials(requestedEnv)).success;
47321
+ configuredEnvs = hasStoredEmail || hasUsableCredentials ? [requestedEnv] : [];
47322
+ } else {
47323
+ configuredEnvs = [...new Set([...await getAvailableEnvironments(), ...emailEnvs])];
47232
47324
  }
47233
47325
  if (configuredEnvs.length === 0) {
47234
47326
  M2.warn("No credentials configured");
@@ -47258,15 +47350,8 @@ async function updateEmail(options = {}) {
47258
47350
  }
47259
47351
  targetEnv = selected;
47260
47352
  }
47261
- const currentCreds = await getSavedCredentials(targetEnv);
47262
- if (!currentCreds) {
47263
- M2.error("Credentials not found");
47264
- outro.error("Credentials not found");
47265
- if (exitOnComplete)
47266
- process.exit(0);
47267
- return false;
47268
- }
47269
- const currentEmail = currentCreds.email;
47353
+ const currentCredsResult = await loadCredentials(targetEnv);
47354
+ const currentEmail = currentCredsResult.success ? currentCredsResult.credentials.email : await getStoredCredentialEmail(targetEnv);
47270
47355
  const message = currentEmail ? `Email ${dim(`(current: ${currentEmail})`)}` : `Email ${dim("(not set)")}`;
47271
47356
  const email3 = await he({
47272
47357
  message,
@@ -47295,6 +47380,14 @@ async function updateEmail(options = {}) {
47295
47380
  return true;
47296
47381
  }
47297
47382
  if (normalizedEmail) {
47383
+ if (!currentCredsResult.success) {
47384
+ M2.error("Credentials required to set email");
47385
+ outro.error("Credentials required to set email");
47386
+ if (exitOnComplete)
47387
+ process.exit(0);
47388
+ return false;
47389
+ }
47390
+ const currentCreds = currentCredsResult.credentials;
47298
47391
  const s = Y2();
47299
47392
  s.start("Validating email...");
47300
47393
  const result = await validateEmailWithTimeback(targetEnv, currentCreds.clientId, currentCreds.clientSecret, normalizedEmail);
@@ -47315,10 +47408,7 @@ async function updateEmail(options = {}) {
47315
47408
  email: normalizedEmail
47316
47409
  });
47317
47410
  if (accountCreated) {
47318
- await saveCredentials(targetEnv, {
47319
- ...currentCreds,
47320
- email: normalizedEmail
47321
- });
47411
+ await saveCredentialEmail(targetEnv, normalizedEmail);
47322
47412
  M2.success(`Email saved for ${targetEnv}`);
47323
47413
  outro.success();
47324
47414
  } else {
@@ -47328,20 +47418,14 @@ async function updateEmail(options = {}) {
47328
47418
  process.exit(0);
47329
47419
  return accountCreated;
47330
47420
  }
47331
- await saveCredentials(targetEnv, {
47332
- ...currentCreds,
47333
- email: normalizedEmail
47334
- });
47421
+ await saveCredentialEmail(targetEnv, normalizedEmail);
47335
47422
  s.stop(green(`Email updated for ${targetEnv}`));
47336
47423
  outro.success();
47337
47424
  if (exitOnComplete)
47338
47425
  process.exit(0);
47339
47426
  return true;
47340
47427
  }
47341
- await saveCredentials(targetEnv, {
47342
- ...currentCreds,
47343
- email: undefined
47344
- });
47428
+ await saveCredentialEmail(targetEnv, undefined);
47345
47429
  outro.success(`Email cleared for ${targetEnv}`);
47346
47430
  if (exitOnComplete)
47347
47431
  process.exit(0);
@@ -47359,13 +47443,19 @@ async function listCredentials(options = {}) {
47359
47443
  const { exitOnComplete = true } = options;
47360
47444
  const timebackLines = [];
47361
47445
  const masteryTrackLines = [];
47446
+ const envCreds = getEnvCredentials();
47362
47447
  let hasAnyTimeback = false;
47363
47448
  let hasAnyMasteryTrack = false;
47449
+ let hasSavedTimeback = false;
47450
+ const hasEnvTimeback = Boolean(envCreds);
47364
47451
  for (const env2 of ENVIRONMENTS) {
47365
47452
  const creds = await getSavedCredentials(env2);
47366
- if (creds) {
47453
+ if (creds && hasEnvTimeback) {
47454
+ timebackLines.push(`${dim("●")} ${env2}: ${creds.clientId}`);
47455
+ hasSavedTimeback = true;
47456
+ } else if (creds) {
47367
47457
  timebackLines.push(`${green("●")} ${env2}: ${creds.clientId}`);
47368
- hasAnyTimeback = true;
47458
+ hasSavedTimeback = true;
47369
47459
  } else {
47370
47460
  timebackLines.push(`${dim("○")} ${env2}: not configured`);
47371
47461
  }
@@ -47377,12 +47467,26 @@ async function listCredentials(options = {}) {
47377
47467
  masteryTrackLines.push(`${dim("○")} ${env2}: not configured`);
47378
47468
  }
47379
47469
  }
47470
+ hasAnyTimeback = hasSavedTimeback || hasEnvTimeback;
47471
+ if (envCreds) {
47472
+ timebackLines.push(`${green("●")} env: ${envCreds.clientId} ${yellow("[override]")}`);
47473
+ }
47380
47474
  Me(timebackLines.join(`
47381
47475
  `), "Timeback");
47382
47476
  Me(masteryTrackLines.join(`
47383
47477
  `), "MasteryTrack");
47384
47478
  if (hasAnyTimeback || hasAnyMasteryTrack) {
47385
- outro.info(`Credentials stored in ${blueBright(underline(tildify(getCredentialsPath())))}`);
47479
+ const details = [];
47480
+ if (hasSavedTimeback || hasAnyMasteryTrack) {
47481
+ details.push(`Credentials stored in ${blueBright(underline(tildify(getCredentialsPath())))}`);
47482
+ }
47483
+ if (hasEnvTimeback) {
47484
+ details.push("Timeback env credentials are active");
47485
+ }
47486
+ if (details.length > 0) {
47487
+ outro.info(details.join(`
47488
+ `));
47489
+ }
47386
47490
  } else {
47387
47491
  const shouldAdd = await ye({
47388
47492
  message: "No credentials configured. Add now?"
@@ -47424,7 +47528,7 @@ async function removeCredentials(options = {}) {
47424
47528
  configuredEnvs.push(env2);
47425
47529
  }
47426
47530
  if (configuredEnvs.length === 0) {
47427
- M2.warn(provider === "masterytrack" ? "No MasteryTrack credentials configured" : "No credentials configured");
47531
+ M2.warn(provider === "masterytrack" ? "No saved MasteryTrack credentials configured" : "No saved credentials configured");
47428
47532
  if (exitOnComplete)
47429
47533
  process.exit(0);
47430
47534
  return;
@@ -48243,7 +48347,7 @@ You need to configure your Timeback API credentials first.`, "Credentials Setup"
48243
48347
  return "ok";
48244
48348
  }
48245
48349
  async function ensureConfiguredEnvironments(targetEnv) {
48246
- let configuredEnvs = await getConfiguredEnvironments();
48350
+ let configuredEnvs = await getAvailableEnvironments();
48247
48351
  const needsSetup = targetEnv ? !configuredEnvs.includes(targetEnv) : configuredEnvs.length === 0;
48248
48352
  if (needsSetup) {
48249
48353
  const setupResult = await setupCredentials2(targetEnv);
@@ -48253,7 +48357,7 @@ async function ensureConfiguredEnvironments(targetEnv) {
48253
48357
  if (setupResult === "error") {
48254
48358
  return { status: "error" };
48255
48359
  }
48256
- configuredEnvs = await getConfiguredEnvironments();
48360
+ configuredEnvs = await getAvailableEnvironments();
48257
48361
  const stillMissing = targetEnv ? !configuredEnvs.includes(targetEnv) : configuredEnvs.length === 0;
48258
48362
  if (stillMissing) {
48259
48363
  return { status: "error" };
@@ -52598,8 +52702,8 @@ async function installAndDisplay(options) {
52598
52702
 
52599
52703
  // src/commands/init/lib/postInit.ts
52600
52704
  async function ensureSyncEmail(targetEnv) {
52601
- const creds = await getSavedCredentials(targetEnv);
52602
- if (creds && !creds.email) {
52705
+ const result = await loadCredentials(targetEnv);
52706
+ if (result.success && !result.credentials.email) {
52603
52707
  return await updateEmail({ exitOnComplete: false, env: targetEnv });
52604
52708
  }
52605
52709
  return true;
@@ -76464,6 +76568,13 @@ var StringTimebackGrade4 = exports_external2.string().transform((value, ctx) =>
76464
76568
  return exports_external2.NEVER;
76465
76569
  }
76466
76570
  const stripped = raw.replace(/\bgrade\b/g, "").replace(/(\d+)(st|nd|rd|th)\b/g, "$1").trim();
76571
+ if (stripped === "") {
76572
+ ctx.addIssue({
76573
+ code: "custom",
76574
+ message: "must be a valid Timeback grade"
76575
+ });
76576
+ return exports_external2.NEVER;
76577
+ }
76467
76578
  if (stripped === "pre-k" || stripped === "pk") {
76468
76579
  return -1;
76469
76580
  }
@@ -78317,8 +78428,13 @@ var CredentialsSchema2 = exports_external2.object({
78317
78428
  clientSecret: NonEmptyString4,
78318
78429
  email: exports_external2.email("Valid email is required").optional()
78319
78430
  });
78320
- var ClientIdSchema2 = exports_external2.string().min(1, "Client ID is required").regex(/^[a-z0-9]+$/, "Client ID must contain only lowercase letters and numbers");
78321
- var ClientSecretSchema2 = exports_external2.string().min(1, "Client secret is required").regex(/^[a-z0-9]+$/, "Client secret must contain only lowercase letters and numbers").max(53, "Client secret must be less than 53 characters");
78431
+ var StoredCredentialsSchema2 = exports_external2.object({
78432
+ clientId: NonEmptyString4.optional(),
78433
+ clientSecret: NonEmptyString4.optional(),
78434
+ email: exports_external2.email("Valid email is required").optional()
78435
+ }).refine((value) => value.email || value.clientId && value.clientSecret, "Saved credentials must include an email or a client ID/client secret pair").refine((value) => Boolean(value.clientId) === Boolean(value.clientSecret), "Saved credentials must include both client ID and client secret");
78436
+ var ClientIdSchema2 = exports_external2.string().min(1, "Client ID is required").max(128, "Client ID must be at most 128 characters").regex(/^[\w+]+$/, "Client ID must contain only letters, digits, underscores, or +");
78437
+ var ClientSecretSchema2 = exports_external2.string().min(24, "Client secret must be at least 24 characters").max(64, "Client secret must be at most 64 characters").regex(/^[\w+]+$/, "Client secret must contain only letters, digits, underscores, or +");
78322
78438
  var MasteryTrackCredentialsSchema2 = exports_external2.object({
78323
78439
  apiKey: NonEmptyString4,
78324
78440
  email: exports_external2.email("Valid email is required")
@@ -78639,24 +78755,41 @@ async function writeCredentialsStore2(store) {
78639
78755
  await writeFile3(CREDENTIALS_FILE2, JSON.stringify(cleaned, null, 2), { mode: 384 });
78640
78756
  return true;
78641
78757
  }
78642
- async function getSavedCredentials2(environment) {
78643
- const store = await readCredentialsStore2();
78758
+ function getStoredCredentialsFromStore2(store, environment) {
78644
78759
  const namespaced = store.timeback?.[environment];
78645
78760
  if (namespaced) {
78646
- const result = CredentialsSchema2.safeParse(namespaced);
78761
+ const result = StoredCredentialsSchema2.safeParse(namespaced);
78647
78762
  if (result.success) {
78648
78763
  return result.data;
78649
78764
  }
78650
78765
  }
78651
78766
  const legacy = store[environment];
78652
78767
  if (legacy) {
78653
- const result = CredentialsSchema2.safeParse(legacy);
78768
+ const result = StoredCredentialsSchema2.safeParse(legacy);
78654
78769
  if (result.success) {
78655
78770
  return result.data;
78656
78771
  }
78657
78772
  }
78658
78773
  return null;
78659
78774
  }
78775
+ async function getStoredCredentials2(environment) {
78776
+ const store = await readCredentialsStore2();
78777
+ return getStoredCredentialsFromStore2(store, environment);
78778
+ }
78779
+ function toCredentials2(stored) {
78780
+ if (stored?.clientId && stored.clientSecret) {
78781
+ return {
78782
+ clientId: stored.clientId,
78783
+ clientSecret: stored.clientSecret,
78784
+ email: stored.email
78785
+ };
78786
+ }
78787
+ return null;
78788
+ }
78789
+ async function getSavedCredentials2(environment) {
78790
+ const stored = await getStoredCredentials2(environment);
78791
+ return toCredentials2(stored);
78792
+ }
78660
78793
  async function saveCredentials2(environment, credentials) {
78661
78794
  const store = await readCredentialsStore2();
78662
78795
  store.timeback = store.timeback ?? {};
@@ -78664,6 +78797,44 @@ async function saveCredentials2(environment, credentials) {
78664
78797
  delete store[environment];
78665
78798
  return writeCredentialsStore2(store);
78666
78799
  }
78800
+ async function saveCredentialEmail2(environment, email32) {
78801
+ const store = await readCredentialsStore2();
78802
+ const existing = getStoredCredentialsFromStore2(store, environment);
78803
+ if (email32) {
78804
+ store.timeback = store.timeback ?? {};
78805
+ store.timeback[environment] = {
78806
+ ...existing ?? {},
78807
+ email: email32
78808
+ };
78809
+ } else if (existing?.clientId && existing.clientSecret) {
78810
+ store.timeback = store.timeback ?? {};
78811
+ store.timeback[environment] = {
78812
+ clientId: existing.clientId,
78813
+ clientSecret: existing.clientSecret
78814
+ };
78815
+ } else {
78816
+ delete store.timeback?.[environment];
78817
+ if (store.timeback && Object.keys(store.timeback).length === 0) {
78818
+ delete store.timeback;
78819
+ }
78820
+ }
78821
+ delete store[environment];
78822
+ return writeCredentialsStore2(store);
78823
+ }
78824
+ async function getStoredCredentialEmail2(environment) {
78825
+ const stored = await getStoredCredentials2(environment);
78826
+ return stored?.email;
78827
+ }
78828
+ async function getEnvironmentsWithStoredEmail2() {
78829
+ const store = await readCredentialsStore2();
78830
+ const environments = [];
78831
+ for (const env22 of ENVIRONMENTS2) {
78832
+ if (getStoredCredentialsFromStore2(store, env22)?.email) {
78833
+ environments.push(env22);
78834
+ }
78835
+ }
78836
+ return environments;
78837
+ }
78667
78838
  async function clearCredentials2(environment) {
78668
78839
  const store = await readCredentialsStore2();
78669
78840
  if (environment) {
@@ -78684,18 +78855,23 @@ async function getConfiguredEnvironments2() {
78684
78855
  const store = await readCredentialsStore2();
78685
78856
  const configured = [];
78686
78857
  for (const env22 of ENVIRONMENTS2) {
78687
- const namespaced = store.timeback?.[env22];
78688
- if (namespaced && CredentialsSchema2.safeParse(namespaced).success) {
78689
- configured.push(env22);
78690
- continue;
78691
- }
78692
- const legacy = store[env22];
78693
- if (legacy && CredentialsSchema2.safeParse(legacy).success) {
78858
+ if (toCredentials2(getStoredCredentialsFromStore2(store, env22))) {
78694
78859
  configured.push(env22);
78695
78860
  }
78696
78861
  }
78697
78862
  return configured;
78698
78863
  }
78864
+ async function getAvailableEnvironments2() {
78865
+ const available = await getConfiguredEnvironments2();
78866
+ if (getEnvCredentials2()) {
78867
+ for (const env22 of ENVIRONMENTS2) {
78868
+ if (!available.includes(env22)) {
78869
+ available.push(env22);
78870
+ }
78871
+ }
78872
+ }
78873
+ return available;
78874
+ }
78699
78875
  function getEnvCredentials2() {
78700
78876
  const clientId = process.env.TIMEBACK_API_CLIENT_ID ?? process.env.TIMEBACK_CLIENT_ID;
78701
78877
  const clientSecret = process.env.TIMEBACK_API_CLIENT_SECRET ?? process.env.TIMEBACK_CLIENT_SECRET;
@@ -78709,10 +78885,12 @@ function getEnvCredentials2() {
78709
78885
  }
78710
78886
  async function loadCredentials2(environment) {
78711
78887
  const envCreds = getEnvCredentials2();
78888
+ const storedCreds = await getStoredCredentials2(environment);
78712
78889
  if (envCreds) {
78713
- return { success: true, credentials: envCreds, source: "env" };
78890
+ const merged = storedCreds?.email ? { ...envCreds, email: storedCreds.email } : envCreds;
78891
+ return { success: true, credentials: merged, source: "env" };
78714
78892
  }
78715
- const savedCreds = await getSavedCredentials2(environment);
78893
+ const savedCreds = toCredentials2(storedCreds);
78716
78894
  if (savedCreds) {
78717
78895
  return { success: true, credentials: savedCreds, source: "saved" };
78718
78896
  }
@@ -78722,7 +78900,7 @@ async function loadCredentials2(environment) {
78722
78900
  };
78723
78901
  }
78724
78902
  async function loadAllCredentials2() {
78725
- const configuredEnvs = await getConfiguredEnvironments2();
78903
+ const configuredEnvs = await getAvailableEnvironments2();
78726
78904
  const credentials = {};
78727
78905
  for (const env22 of configuredEnvs) {
78728
78906
  const result = await loadCredentials2(env22);
@@ -80236,21 +80414,40 @@ async function addCredentials2(options = {}) {
80236
80414
  async function listCredentials2(options = {}) {
80237
80415
  const { exitOnComplete = true, inline = false } = options;
80238
80416
  const lines = [];
80417
+ const envCreds = getEnvCredentials2();
80239
80418
  let hasAny = false;
80419
+ let hasSaved = false;
80420
+ const hasEnv = Boolean(envCreds);
80240
80421
  for (const env22 of ENVIRONMENTS2) {
80241
80422
  const creds = await getSavedCredentials2(env22);
80242
- if (creds) {
80423
+ if (creds && hasEnv) {
80424
+ lines.push(`${dim2("●")} ${env22}: ${creds.clientId}`);
80425
+ hasSaved = true;
80426
+ } else if (creds) {
80243
80427
  lines.push(`${green2("●")} ${env22}: ${creds.clientId}`);
80244
- hasAny = true;
80428
+ hasSaved = true;
80245
80429
  } else {
80246
- lines.push(`${dim2("○")} ${env22}: ${"not configured"}`);
80430
+ lines.push(`${dim2("○")} ${env22}: not configured`);
80247
80431
  }
80248
80432
  }
80433
+ hasAny = hasSaved || hasEnv;
80434
+ if (envCreds) {
80435
+ lines.push(`${green2("●")} env: ${envCreds.clientId} ${yellow2("[override]")}`);
80436
+ }
80249
80437
  Me2(lines.join(`
80250
80438
  `), "Configured environments");
80251
80439
  if (hasAny) {
80252
- if (!inline)
80253
- outro2.info(`Credentials stored in ${blueBright2(underline2(getCredentialsPath2()))}`);
80440
+ const details = [];
80441
+ if (hasSaved) {
80442
+ details.push(`Credentials stored in ${blueBright2(underline2(getCredentialsPath2()))}`);
80443
+ }
80444
+ if (hasEnv) {
80445
+ details.push("Timeback env credentials are active");
80446
+ }
80447
+ if (details.length > 0 && !inline) {
80448
+ outro2.info(details.join(`
80449
+ `));
80450
+ }
80254
80451
  } else {
80255
80452
  const shouldAdd = await ye2({
80256
80453
  message: "No credentials configured. Add now?"
@@ -80267,12 +80464,12 @@ async function listCredentials2(options = {}) {
80267
80464
  }
80268
80465
  async function updateEmail2(options = {}) {
80269
80466
  const { exitOnComplete = true, inline = false } = options;
80270
- const configuredEnvs = [];
80271
- for (const env22 of ENVIRONMENTS2) {
80272
- const creds = await getSavedCredentials2(env22);
80273
- if (creds)
80274
- configuredEnvs.push(env22);
80275
- }
80467
+ const configuredEnvs = [
80468
+ ...new Set([
80469
+ ...await getAvailableEnvironments2(),
80470
+ ...await getEnvironmentsWithStoredEmail2()
80471
+ ])
80472
+ ];
80276
80473
  if (configuredEnvs.length === 0) {
80277
80474
  M22.warn("No credentials configured");
80278
80475
  if (!inline)
@@ -80301,16 +80498,8 @@ async function updateEmail2(options = {}) {
80301
80498
  }
80302
80499
  targetEnv = selected;
80303
80500
  }
80304
- const currentCreds = await getSavedCredentials2(targetEnv);
80305
- if (!currentCreds) {
80306
- M22.error("Credentials not found");
80307
- if (!inline)
80308
- outro2.error("Credentials not found");
80309
- if (exitOnComplete)
80310
- process.exit(0);
80311
- return;
80312
- }
80313
- const currentEmail = currentCreds.email;
80501
+ const currentCredsResult = await loadCredentials2(targetEnv);
80502
+ const currentEmail = currentCredsResult.success ? currentCredsResult.credentials.email : await getStoredCredentialEmail2(targetEnv);
80314
80503
  const message = currentEmail ? `Email ${dim2(`(current: ${currentEmail})`)}` : `Email ${dim2("(not set)")}`;
80315
80504
  const email32 = await he2({
80316
80505
  message,
@@ -80333,7 +80522,23 @@ async function updateEmail2(options = {}) {
80333
80522
  }
80334
80523
  const normalized = email32 ? normalizeEmail2(email32) : undefined;
80335
80524
  const emailUnchanged = (normalized ?? "") === (currentEmail ?? "");
80525
+ if (emailUnchanged) {
80526
+ if (!inline)
80527
+ outro2.info("Email unchanged");
80528
+ if (exitOnComplete)
80529
+ process.exit(0);
80530
+ return;
80531
+ }
80336
80532
  if (normalized) {
80533
+ if (!currentCredsResult.success) {
80534
+ M22.error("Credentials required to set email");
80535
+ if (!inline)
80536
+ outro2.error("Credentials required to set email");
80537
+ if (exitOnComplete)
80538
+ process.exit(0);
80539
+ return;
80540
+ }
80541
+ const currentCreds = currentCredsResult.credentials;
80337
80542
  const s = Y22();
80338
80543
  s.start("Checking account...");
80339
80544
  const result = await validateEmailWithTimeback2(targetEnv, currentCreds.clientId, currentCreds.clientSecret, normalized);
@@ -80354,11 +80559,8 @@ async function updateEmail2(options = {}) {
80354
80559
  clientSecret: currentCreds.clientSecret,
80355
80560
  email: normalized
80356
80561
  });
80357
- if (accountCreated && !emailUnchanged) {
80358
- await saveCredentials2(targetEnv, {
80359
- ...currentCreds,
80360
- email: normalized
80361
- });
80562
+ if (accountCreated) {
80563
+ await saveCredentialEmail2(targetEnv, normalized);
80362
80564
  M22.success(`Email saved for ${targetEnv}`);
80363
80565
  }
80364
80566
  if (!inline) {
@@ -80373,17 +80575,7 @@ async function updateEmail2(options = {}) {
80373
80575
  return;
80374
80576
  }
80375
80577
  s.stop(green2("Account verified"));
80376
- if (emailUnchanged) {
80377
- if (!inline)
80378
- outro2.info("Email unchanged");
80379
- if (exitOnComplete)
80380
- process.exit(0);
80381
- return;
80382
- }
80383
- await saveCredentials2(targetEnv, {
80384
- ...currentCreds,
80385
- email: normalized
80386
- });
80578
+ await saveCredentialEmail2(targetEnv, normalized);
80387
80579
  M22.success(`Email updated for ${targetEnv}`);
80388
80580
  if (!inline)
80389
80581
  outro2.success();
@@ -80391,10 +80583,7 @@ async function updateEmail2(options = {}) {
80391
80583
  process.exit(0);
80392
80584
  return;
80393
80585
  }
80394
- await saveCredentials2(targetEnv, {
80395
- ...currentCreds,
80396
- email: undefined
80397
- });
80586
+ await saveCredentialEmail2(targetEnv, undefined);
80398
80587
  if (!inline)
80399
80588
  outro2.success(`Email cleared for ${targetEnv}`);
80400
80589
  if (exitOnComplete)
@@ -80409,7 +80598,7 @@ async function removeCredentials2(options = {}) {
80409
80598
  configuredEnvs.push(env22);
80410
80599
  }
80411
80600
  if (configuredEnvs.length === 0) {
80412
- M22.warn("No credentials configured");
80601
+ M22.warn("No saved credentials configured");
80413
80602
  if (exitOnComplete)
80414
80603
  process.exit(0);
80415
80604
  return;
@@ -80615,7 +80804,7 @@ async function promptNoConfig(credentials, configuredEnvs, opts = {}) {
80615
80804
  }
80616
80805
  async function resolveConfigSource(courseIds, opts, defaultEnvironment) {
80617
80806
  const parserOpts = { playcademy: opts.playcademy };
80618
- let configuredEnvs = await getConfiguredEnvironments2();
80807
+ let configuredEnvs = await getAvailableEnvironments2();
80619
80808
  let credentials = {};
80620
80809
  if (courseIds.length > 0) {
80621
80810
  if (configuredEnvs.length > 0) {
@@ -82945,8 +83134,8 @@ var cors = (options) => {
82945
83134
  async function handleBootstrap(c, ctx) {
82946
83135
  const { bootstrap } = c.get("services");
82947
83136
  const env22 = c.get("env");
82948
- const freshCredentials = await getSavedCredentials2(env22);
82949
- const email32 = freshCredentials?.email;
83137
+ const freshCredentials = await loadCredentials2(env22);
83138
+ const email32 = freshCredentials.success ? freshCredentials.credentials.email : undefined;
82950
83139
  const courseIds = ctx.userConfig.courseIds[env22];
82951
83140
  const result = await bootstrap.getBootstrap({ email: email32, courseIds });
82952
83141
  return c.json(result);
@@ -83975,18 +84164,18 @@ class StatusService {
83975
84164
  this.ctx = ctx;
83976
84165
  }
83977
84166
  async getStatus() {
83978
- const configuredEnvironments = await getConfiguredEnvironments2();
84167
+ const configuredEnvironments = await getAvailableEnvironments2();
83979
84168
  const [stagingCreds, productionCreds] = await Promise.all([
83980
- getSavedCredentials2("staging"),
83981
- getSavedCredentials2("production")
84169
+ loadCredentials2("staging"),
84170
+ loadCredentials2("production")
83982
84171
  ]);
83983
- const defaultCreds = this.ctx.defaultEnvironment === "staging" ? stagingCreds : this.ctx.defaultEnvironment === "production" ? productionCreds : null;
83984
- const clientId = defaultCreds?.clientId ?? stagingCreds?.clientId ?? productionCreds?.clientId ?? this.ctx.credentials.staging?.clientId ?? this.ctx.credentials.production?.clientId;
84172
+ const defaultCreds = this.ctx.defaultEnvironment === "staging" ? stagingCreds.success ? stagingCreds.credentials : null : this.ctx.defaultEnvironment === "production" ? productionCreds.success ? productionCreds.credentials : null : null;
84173
+ const clientId = defaultCreds?.clientId ?? (stagingCreds.success ? stagingCreds.credentials.clientId : undefined) ?? (productionCreds.success ? productionCreds.credentials.clientId : undefined) ?? this.ctx.credentials.staging?.clientId ?? this.ctx.credentials.production?.clientId;
83985
84174
  return {
83986
84175
  config: this.ctx.userConfig,
83987
84176
  environment: this.ctx.defaultEnvironment,
83988
84177
  configuredEnvironments,
83989
- hasEmail: !!stagingCreds?.email || !!productionCreds?.email,
84178
+ hasEmail: stagingCreds.success && Boolean(stagingCreds.credentials.email) || productionCreds.success && Boolean(productionCreds.credentials.email),
83990
84179
  clientId,
83991
84180
  sensors: this.ctx.derivedSensors
83992
84181
  };
@@ -84636,7 +84825,7 @@ async function launchServer(serverConfig, userConfig, credentials, env22, opts,
84636
84825
  }
84637
84826
  async function handleCourseIdsCase(courseIds, opts, serverConfig) {
84638
84827
  const env22 = opts.env ?? await promptEnvironmentForCourseIds(courseIds.length);
84639
- let configuredEnvs = await getConfiguredEnvironments2();
84828
+ let configuredEnvs = await getAvailableEnvironments2();
84640
84829
  let credentials = configuredEnvs.length > 0 ? await loadAllCredentials2() : {};
84641
84830
  const resolvedResult = await resolveConfigSource(courseIds, opts, env22);
84642
84831
  if (!resolvedResult) {
@@ -84688,7 +84877,7 @@ async function handleConfigFileCase(userConfig, configPath, opts, serverConfig)
84688
84877
  await launchServer(serverConfig, userConfig, credentials, env22, opts, configPath);
84689
84878
  }
84690
84879
  async function handleImportFlowCase(opts, serverConfig) {
84691
- let configuredEnvs = await getConfiguredEnvironments2();
84880
+ let configuredEnvs = await getAvailableEnvironments2();
84692
84881
  let credentials = {};
84693
84882
  let selectedEnv;
84694
84883
  if (configuredEnvs.length === 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "timeback",
3
- "version": "0.2.2-beta.20260401002024",
3
+ "version": "0.2.2-beta.20260402000431",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "timeback": "./dist/cli.js"
package/schema.json CHANGED
@@ -15,13 +15,13 @@
15
15
  "$ref": "#/definitions/__schema22"
16
16
  },
17
17
  "sensor": {
18
- "$ref": "#/definitions/__schema52"
18
+ "$ref": "#/definitions/__schema38"
19
19
  },
20
20
  "launchUrl": {
21
- "$ref": "#/definitions/__schema53"
21
+ "$ref": "#/definitions/__schema39"
22
22
  },
23
23
  "studio": {
24
- "$ref": "#/definitions/__schema54"
24
+ "$ref": "#/definitions/__schema40"
25
25
  }
26
26
  },
27
27
  "required": ["name", "courses"],
@@ -282,16 +282,16 @@
282
282
  "$ref": "#/definitions/__schema24"
283
283
  },
284
284
  "ids": {
285
- "$ref": "#/definitions/__schema40"
285
+ "$ref": "#/definitions/__schema26"
286
286
  },
287
287
  "sensor": {
288
- "$ref": "#/definitions/__schema43"
288
+ "$ref": "#/definitions/__schema29"
289
289
  },
290
290
  "launchUrl": {
291
- "$ref": "#/definitions/__schema44"
291
+ "$ref": "#/definitions/__schema30"
292
292
  },
293
293
  "overrides": {
294
- "$ref": "#/definitions/__schema45"
294
+ "$ref": "#/definitions/__schema31"
295
295
  }
296
296
  },
297
297
  "required": ["subject"],
@@ -325,121 +325,88 @@
325
325
  "__schema24": {
326
326
  "allOf": [
327
327
  {
328
+ "allOf": [
329
+ {
330
+ "$ref": "#/definitions/__schema25"
331
+ }
332
+ ],
328
333
  "$ref": "#/definitions/TimebackGrade"
329
334
  }
330
335
  ]
331
336
  },
332
337
  "__schema25": {
333
- "type": "number",
334
- "const": -1
335
- },
336
- "__schema26": {
337
- "type": "number",
338
- "const": 0
339
- },
340
- "__schema27": {
341
- "type": "number",
342
- "const": 1
343
- },
344
- "__schema28": {
345
- "type": "number",
346
- "const": 2
347
- },
348
- "__schema29": {
349
- "type": "number",
350
- "const": 3
351
- },
352
- "__schema30": {
353
- "type": "number",
354
- "const": 4
355
- },
356
- "__schema31": {
357
- "type": "number",
358
- "const": 5
359
- },
360
- "__schema32": {
361
- "type": "number",
362
- "const": 6
363
- },
364
- "__schema33": {
365
- "type": "number",
366
- "const": 7
367
- },
368
- "__schema34": {
369
- "type": "number",
370
- "const": 8
371
- },
372
- "__schema35": {
373
- "type": "number",
374
- "const": 9
375
- },
376
- "__schema36": {
377
- "type": "number",
378
- "const": 10
379
- },
380
- "__schema37": {
381
- "type": "number",
382
- "const": 11
383
- },
384
- "__schema38": {
385
- "type": "number",
386
- "const": 12
387
- },
388
- "__schema39": {
389
- "type": "number",
390
- "const": 13
391
- },
392
- "TimebackGrade": {
393
338
  "anyOf": [
394
339
  {
395
- "$ref": "#/definitions/__schema25"
340
+ "type": "number",
341
+ "const": -1
396
342
  },
397
343
  {
398
- "$ref": "#/definitions/__schema26"
344
+ "type": "number",
345
+ "const": 0
399
346
  },
400
347
  {
401
- "$ref": "#/definitions/__schema27"
348
+ "type": "number",
349
+ "const": 1
402
350
  },
403
351
  {
404
- "$ref": "#/definitions/__schema28"
352
+ "type": "number",
353
+ "const": 2
405
354
  },
406
355
  {
407
- "$ref": "#/definitions/__schema29"
356
+ "type": "number",
357
+ "const": 3
408
358
  },
409
359
  {
410
- "$ref": "#/definitions/__schema30"
360
+ "type": "number",
361
+ "const": 4
411
362
  },
412
363
  {
413
- "$ref": "#/definitions/__schema31"
364
+ "type": "number",
365
+ "const": 5
414
366
  },
415
367
  {
416
- "$ref": "#/definitions/__schema32"
368
+ "type": "number",
369
+ "const": 6
417
370
  },
418
371
  {
419
- "$ref": "#/definitions/__schema33"
372
+ "type": "number",
373
+ "const": 7
420
374
  },
421
375
  {
422
- "$ref": "#/definitions/__schema34"
376
+ "type": "number",
377
+ "const": 8
423
378
  },
424
379
  {
425
- "$ref": "#/definitions/__schema35"
380
+ "type": "number",
381
+ "const": 9
426
382
  },
427
383
  {
428
- "$ref": "#/definitions/__schema36"
384
+ "type": "number",
385
+ "const": 10
429
386
  },
430
387
  {
431
- "$ref": "#/definitions/__schema37"
388
+ "type": "number",
389
+ "const": 11
432
390
  },
433
391
  {
434
- "$ref": "#/definitions/__schema38"
392
+ "type": "number",
393
+ "const": 12
435
394
  },
436
395
  {
437
- "$ref": "#/definitions/__schema39"
396
+ "type": "number",
397
+ "const": 13
438
398
  }
439
- ],
440
- "description": "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)"
399
+ ]
441
400
  },
442
- "__schema40": {
401
+ "TimebackGrade": {
402
+ "description": "Grade level (-1 = Pre-K, 0 = K, 1-12 = grades, 13 = AP)",
403
+ "allOf": [
404
+ {
405
+ "$ref": "#/definitions/__schema25"
406
+ }
407
+ ]
408
+ },
409
+ "__schema26": {
443
410
  "anyOf": [
444
411
  {
445
412
  "$ref": "#/definitions/CourseIds"
@@ -453,34 +420,34 @@
453
420
  "type": "object",
454
421
  "properties": {
455
422
  "staging": {
456
- "$ref": "#/definitions/__schema41"
423
+ "$ref": "#/definitions/__schema27"
457
424
  },
458
425
  "production": {
459
- "$ref": "#/definitions/__schema42"
426
+ "$ref": "#/definitions/__schema28"
460
427
  }
461
428
  },
462
429
  "additionalProperties": false,
463
430
  "description": "Environment-specific course IDs (populated by sync)"
464
431
  },
465
- "__schema41": {
432
+ "__schema27": {
466
433
  "type": "string",
467
434
  "description": "Course ID in staging environment"
468
435
  },
469
- "__schema42": {
436
+ "__schema28": {
470
437
  "type": "string",
471
438
  "description": "Course ID in production environment"
472
439
  },
473
- "__schema43": {
440
+ "__schema29": {
474
441
  "type": "string",
475
442
  "format": "uri",
476
443
  "description": "Caliper sensor endpoint URL for this course"
477
444
  },
478
- "__schema44": {
445
+ "__schema30": {
479
446
  "type": "string",
480
447
  "format": "uri",
481
448
  "description": "LTI launch URL for this course"
482
449
  },
483
- "__schema45": {
450
+ "__schema31": {
484
451
  "allOf": [
485
452
  {
486
453
  "$ref": "#/definitions/CourseOverrides"
@@ -491,16 +458,16 @@
491
458
  "type": "object",
492
459
  "properties": {
493
460
  "staging": {
494
- "$ref": "#/definitions/__schema46"
461
+ "$ref": "#/definitions/__schema32"
495
462
  },
496
463
  "production": {
497
- "$ref": "#/definitions/__schema51"
464
+ "$ref": "#/definitions/__schema37"
498
465
  }
499
466
  },
500
467
  "additionalProperties": false,
501
468
  "description": "Per-environment course overrides"
502
469
  },
503
- "__schema46": {
470
+ "__schema32": {
504
471
  "description": "Overrides for staging environment",
505
472
  "allOf": [
506
473
  {
@@ -508,21 +475,21 @@
508
475
  }
509
476
  ]
510
477
  },
511
- "__schema47": {
478
+ "__schema33": {
512
479
  "type": "string",
513
480
  "description": "Course level for this environment"
514
481
  },
515
- "__schema48": {
482
+ "__schema34": {
516
483
  "type": "string",
517
484
  "format": "uri",
518
485
  "description": "Caliper sensor endpoint URL for this environment"
519
486
  },
520
- "__schema49": {
487
+ "__schema35": {
521
488
  "type": "string",
522
489
  "format": "uri",
523
490
  "description": "LTI launch URL for this environment"
524
491
  },
525
- "__schema50": {
492
+ "__schema36": {
526
493
  "allOf": [
527
494
  {
528
495
  "$ref": "#/definitions/CourseMetadata"
@@ -533,22 +500,22 @@
533
500
  "type": "object",
534
501
  "properties": {
535
502
  "level": {
536
- "$ref": "#/definitions/__schema47"
503
+ "$ref": "#/definitions/__schema33"
537
504
  },
538
505
  "sensor": {
539
- "$ref": "#/definitions/__schema48"
506
+ "$ref": "#/definitions/__schema34"
540
507
  },
541
508
  "launchUrl": {
542
- "$ref": "#/definitions/__schema49"
509
+ "$ref": "#/definitions/__schema35"
543
510
  },
544
511
  "metadata": {
545
- "$ref": "#/definitions/__schema50"
512
+ "$ref": "#/definitions/__schema36"
546
513
  }
547
514
  },
548
515
  "additionalProperties": false,
549
516
  "description": "Environment-specific course overrides (non-identity fields)"
550
517
  },
551
- "__schema51": {
518
+ "__schema37": {
552
519
  "description": "Overrides for production environment",
553
520
  "allOf": [
554
521
  {
@@ -556,27 +523,27 @@
556
523
  }
557
524
  ]
558
525
  },
559
- "__schema52": {
526
+ "__schema38": {
560
527
  "type": "string",
561
528
  "format": "uri",
562
529
  "description": "Default Caliper sensor endpoint URL for all courses"
563
530
  },
564
- "__schema53": {
531
+ "__schema39": {
565
532
  "type": "string",
566
533
  "format": "uri",
567
534
  "description": "Default LTI launch URL for all courses"
568
535
  },
569
- "__schema54": {
536
+ "__schema40": {
570
537
  "type": "object",
571
538
  "properties": {
572
539
  "telemetry": {
573
- "$ref": "#/definitions/__schema55"
540
+ "$ref": "#/definitions/__schema41"
574
541
  }
575
542
  },
576
543
  "additionalProperties": false,
577
544
  "description": "Studio-specific configuration"
578
545
  },
579
- "__schema55": {
546
+ "__schema41": {
580
547
  "default": true,
581
548
  "type": "boolean",
582
549
  "description": "Enable anonymous usage telemetry for Studio (default: true)"
@@ -597,13 +564,13 @@
597
564
  "$ref": "#/definitions/__schema22"
598
565
  },
599
566
  "sensor": {
600
- "$ref": "#/definitions/__schema52"
567
+ "$ref": "#/definitions/__schema38"
601
568
  },
602
569
  "launchUrl": {
603
- "$ref": "#/definitions/__schema53"
570
+ "$ref": "#/definitions/__schema39"
604
571
  },
605
572
  "studio": {
606
- "$ref": "#/definitions/__schema54"
573
+ "$ref": "#/definitions/__schema40"
607
574
  }
608
575
  },
609
576
  "required": ["name", "courses"],