@playcademy/sandbox 0.12.2-beta.4 → 0.12.2-beta.6

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
@@ -2517,7 +2517,7 @@ var package_default;
2517
2517
  var init_package = __esm(() => {
2518
2518
  package_default = {
2519
2519
  name: "@playcademy/sandbox",
2520
- version: "0.12.2-beta.4",
2520
+ version: "0.12.2-beta.6",
2521
2521
  description: "Local development server for Playcademy game development",
2522
2522
  type: "module",
2523
2523
  exports: {
@@ -20895,12 +20895,13 @@ var init_table7 = __esm(() => {
20895
20895
  });
20896
20896
 
20897
20897
  // ../data/src/domains/embed/table.ts
20898
- var embedPartners, embedLaunches, embedLaunchesRelations;
20898
+ var embedContextRoleEnum, embedPartners, embedLaunches, embedLaunchesRelations;
20899
20899
  var init_table8 = __esm(() => {
20900
20900
  init_drizzle_orm();
20901
20901
  init_pg_core();
20902
20902
  init_table5();
20903
20903
  init_table3();
20904
+ embedContextRoleEnum = pgEnum("embed_context_role", ["host"]);
20904
20905
  embedPartners = pgTable("embed_partners", {
20905
20906
  id: uuid("id").primaryKey().defaultRandom(),
20906
20907
  name: text("name").notNull(),
@@ -20917,6 +20918,8 @@ var init_table8 = __esm(() => {
20917
20918
  gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
20918
20919
  parentOrigin: text("parent_origin").notNull(),
20919
20920
  timebackCourseId: text("timeback_course_id"),
20921
+ contextId: text("context_id"),
20922
+ contextRole: embedContextRoleEnum("context_role"),
20920
20923
  passHash: text("pass_hash").unique(),
20921
20924
  passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
20922
20925
  redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
@@ -20956,6 +20959,7 @@ __export(exports_tables_index, {
20956
20959
  deploymentProviderEnum: () => deploymentProviderEnum,
20957
20960
  deploymentTargetEnum: () => deploymentTargetEnum,
20958
20961
  developerStatusEnum: () => developerStatusEnum,
20962
+ embedContextRoleEnum: () => embedContextRoleEnum,
20959
20963
  embedLaunches: () => embedLaunches,
20960
20964
  embedLaunchesRelations: () => embedLaunchesRelations,
20961
20965
  embedPartners: () => embedPartners,
@@ -50823,13 +50827,21 @@ function isCanonicalOrigin(value) {
50823
50827
  }
50824
50828
  return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
50825
50829
  }
50826
- var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50830
+ var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50827
50831
  var init_schemas5 = __esm(() => {
50828
50832
  init_esm();
50833
+ init_table8();
50829
50834
  LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
50830
50835
  EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
50831
50836
  message: "Origin must be a bare https origin like https://example.com, with no path or slash"
50832
50837
  });
50838
+ EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
50839
+ message: "context.id must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
50840
+ });
50841
+ EmbedContextSchema = objectType({
50842
+ id: EmbedContextIdSchema,
50843
+ role: enumType(embedContextRoleEnum.enumValues).optional()
50844
+ }).strict();
50833
50845
  EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
50834
50846
  EnsureEmbedPartnerSchema = objectType({
50835
50847
  name: stringType().trim().min(1).max(100)
@@ -50841,7 +50853,8 @@ var init_schemas5 = __esm(() => {
50841
50853
  timebackId: stringType().trim().min(1).max(200),
50842
50854
  gameSlug: stringType().trim().min(1).max(100),
50843
50855
  parentOrigin: EmbedOriginSchema,
50844
- timebackCourseId: stringType().trim().min(1).max(200).optional()
50856
+ timebackCourseId: stringType().trim().min(1).max(200).optional(),
50857
+ context: EmbedContextSchema.optional()
50845
50858
  }).strict();
50846
50859
  RedeemEmbedPassSchema = objectType({
50847
50860
  pass: stringType().min(1).max(200),
@@ -95683,6 +95696,21 @@ var init_embed_util = __esm(() => {
95683
95696
  });
95684
95697
 
95685
95698
  // ../api-core/src/services/embed.service.ts
95699
+ async function scopedContextId(partnerId, contextId) {
95700
+ return sha256Hex(`${partnerId}:${contextId}`);
95701
+ }
95702
+ function launchTokenClaims(launch) {
95703
+ if (!launch.contextId) {
95704
+ return {};
95705
+ }
95706
+ return {
95707
+ context: {
95708
+ id: launch.contextId,
95709
+ ...launch.contextRole && { role: launch.contextRole }
95710
+ }
95711
+ };
95712
+ }
95713
+
95686
95714
  class EmbedService {
95687
95715
  deps;
95688
95716
  constructor(deps) {
@@ -95726,6 +95754,8 @@ class EmbedService {
95726
95754
  gameId: game.id,
95727
95755
  parentOrigin: input.parentOrigin,
95728
95756
  timebackCourseId: input.timebackCourseId ?? null,
95757
+ contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
95758
+ contextRole: input.context?.role ?? null,
95729
95759
  passHash: await sha256Hex(pass),
95730
95760
  passExpiresAt,
95731
95761
  expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
@@ -95733,7 +95763,12 @@ class EmbedService {
95733
95763
  if (!launch) {
95734
95764
  throw new InternalError("Failed to create embed launch");
95735
95765
  }
95736
- setAttributes({ "app.launch.id": launch.id, "app.user.id": user.id });
95766
+ setAttributes({
95767
+ "app.launch.id": launch.id,
95768
+ "app.user.id": user.id,
95769
+ "app.embed.has_context": input.context !== undefined,
95770
+ ...input.context?.role && { "app.embed.context_role": input.context.role }
95771
+ });
95737
95772
  return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
95738
95773
  }
95739
95774
  async redeemLaunch(input, timeZone) {
@@ -95748,7 +95783,7 @@ class EmbedService {
95748
95783
  const [game, profile, token] = await Promise.all([
95749
95784
  this.deps.getGameForStudent(launch.gameId, user),
95750
95785
  this.deps.getGameProfile(user, launch.gameId, timeZone),
95751
- this.deps.mintGameToken(launch.gameId, user.id)
95786
+ this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
95752
95787
  ]);
95753
95788
  await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
95754
95789
  return {
@@ -95780,7 +95815,7 @@ class EmbedService {
95780
95815
  if (outcome) {
95781
95816
  throw new AccessDeniedError("This launch has ended");
95782
95817
  }
95783
- return this.deps.mintGameToken(launch.gameId, launch.userId);
95818
+ return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
95784
95819
  }
95785
95820
  async endLaunch(launchId) {
95786
95821
  await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
@@ -96882,10 +96917,10 @@ function createSandboxAuthProvider() {
96882
96917
  }
96883
96918
  return deleted;
96884
96919
  },
96885
- async mintGameToken(gameId, userId) {
96920
+ async mintGameToken(gameId, userId, claims) {
96886
96921
  const header = btoa(JSON.stringify({ alg: "none", typ: "sandbox" }));
96887
96922
  const exp = Math.floor(Date.now() / 1000) + 15 * 60;
96888
- const payload = btoa(JSON.stringify({ sub: gameId, uid: userId, exp }));
96923
+ const payload = btoa(JSON.stringify({ ...claims, sub: gameId, uid: userId, exp }));
96889
96924
  const token = `${header}.${payload}.sandbox`;
96890
96925
  return { token, exp: exp * 1000 };
96891
96926
  },
package/dist/server.js CHANGED
@@ -697,7 +697,7 @@ var package_default;
697
697
  var init_package = __esm(() => {
698
698
  package_default = {
699
699
  name: "@playcademy/sandbox",
700
- version: "0.12.2-beta.4",
700
+ version: "0.12.2-beta.6",
701
701
  description: "Local development server for Playcademy game development",
702
702
  type: "module",
703
703
  exports: {
@@ -19075,12 +19075,13 @@ var init_table7 = __esm(() => {
19075
19075
  });
19076
19076
 
19077
19077
  // ../data/src/domains/embed/table.ts
19078
- var embedPartners, embedLaunches, embedLaunchesRelations;
19078
+ var embedContextRoleEnum, embedPartners, embedLaunches, embedLaunchesRelations;
19079
19079
  var init_table8 = __esm(() => {
19080
19080
  init_drizzle_orm();
19081
19081
  init_pg_core();
19082
19082
  init_table5();
19083
19083
  init_table3();
19084
+ embedContextRoleEnum = pgEnum("embed_context_role", ["host"]);
19084
19085
  embedPartners = pgTable("embed_partners", {
19085
19086
  id: uuid("id").primaryKey().defaultRandom(),
19086
19087
  name: text("name").notNull(),
@@ -19097,6 +19098,8 @@ var init_table8 = __esm(() => {
19097
19098
  gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
19098
19099
  parentOrigin: text("parent_origin").notNull(),
19099
19100
  timebackCourseId: text("timeback_course_id"),
19101
+ contextId: text("context_id"),
19102
+ contextRole: embedContextRoleEnum("context_role"),
19100
19103
  passHash: text("pass_hash").unique(),
19101
19104
  passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
19102
19105
  redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
@@ -19136,6 +19139,7 @@ __export(exports_tables_index, {
19136
19139
  deploymentProviderEnum: () => deploymentProviderEnum,
19137
19140
  deploymentTargetEnum: () => deploymentTargetEnum,
19138
19141
  developerStatusEnum: () => developerStatusEnum,
19142
+ embedContextRoleEnum: () => embedContextRoleEnum,
19139
19143
  embedLaunches: () => embedLaunches,
19140
19144
  embedLaunchesRelations: () => embedLaunchesRelations,
19141
19145
  embedPartners: () => embedPartners,
@@ -49003,13 +49007,21 @@ function isCanonicalOrigin(value) {
49003
49007
  }
49004
49008
  return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
49005
49009
  }
49006
- var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
49010
+ var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
49007
49011
  var init_schemas5 = __esm(() => {
49008
49012
  init_esm();
49013
+ init_table8();
49009
49014
  LOCAL_HOSTNAMES = new Set(["localhost", "127.0.0.1"]);
49010
49015
  EmbedOriginSchema = stringType().refine(isCanonicalOrigin, {
49011
49016
  message: "Origin must be a bare https origin like https://example.com, with no path or slash"
49012
49017
  });
49018
+ EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
49019
+ message: "context.id must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
49020
+ });
49021
+ EmbedContextSchema = objectType({
49022
+ id: EmbedContextIdSchema,
49023
+ role: enumType(embedContextRoleEnum.enumValues).optional()
49024
+ }).strict();
49013
49025
  EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
49014
49026
  EnsureEmbedPartnerSchema = objectType({
49015
49027
  name: stringType().trim().min(1).max(100)
@@ -49021,7 +49033,8 @@ var init_schemas5 = __esm(() => {
49021
49033
  timebackId: stringType().trim().min(1).max(200),
49022
49034
  gameSlug: stringType().trim().min(1).max(100),
49023
49035
  parentOrigin: EmbedOriginSchema,
49024
- timebackCourseId: stringType().trim().min(1).max(200).optional()
49036
+ timebackCourseId: stringType().trim().min(1).max(200).optional(),
49037
+ context: EmbedContextSchema.optional()
49025
49038
  }).strict();
49026
49039
  RedeemEmbedPassSchema = objectType({
49027
49040
  pass: stringType().min(1).max(200),
@@ -93863,6 +93876,21 @@ var init_embed_util = __esm(() => {
93863
93876
  });
93864
93877
 
93865
93878
  // ../api-core/src/services/embed.service.ts
93879
+ async function scopedContextId(partnerId, contextId) {
93880
+ return sha256Hex(`${partnerId}:${contextId}`);
93881
+ }
93882
+ function launchTokenClaims(launch) {
93883
+ if (!launch.contextId) {
93884
+ return {};
93885
+ }
93886
+ return {
93887
+ context: {
93888
+ id: launch.contextId,
93889
+ ...launch.contextRole && { role: launch.contextRole }
93890
+ }
93891
+ };
93892
+ }
93893
+
93866
93894
  class EmbedService {
93867
93895
  deps;
93868
93896
  constructor(deps) {
@@ -93906,6 +93934,8 @@ class EmbedService {
93906
93934
  gameId: game.id,
93907
93935
  parentOrigin: input.parentOrigin,
93908
93936
  timebackCourseId: input.timebackCourseId ?? null,
93937
+ contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
93938
+ contextRole: input.context?.role ?? null,
93909
93939
  passHash: await sha256Hex(pass),
93910
93940
  passExpiresAt,
93911
93941
  expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
@@ -93913,7 +93943,12 @@ class EmbedService {
93913
93943
  if (!launch) {
93914
93944
  throw new InternalError("Failed to create embed launch");
93915
93945
  }
93916
- setAttributes({ "app.launch.id": launch.id, "app.user.id": user.id });
93946
+ setAttributes({
93947
+ "app.launch.id": launch.id,
93948
+ "app.user.id": user.id,
93949
+ "app.embed.has_context": input.context !== undefined,
93950
+ ...input.context?.role && { "app.embed.context_role": input.context.role }
93951
+ });
93917
93952
  return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
93918
93953
  }
93919
93954
  async redeemLaunch(input, timeZone) {
@@ -93928,7 +93963,7 @@ class EmbedService {
93928
93963
  const [game, profile, token] = await Promise.all([
93929
93964
  this.deps.getGameForStudent(launch.gameId, user),
93930
93965
  this.deps.getGameProfile(user, launch.gameId, timeZone),
93931
- this.deps.mintGameToken(launch.gameId, user.id)
93966
+ this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
93932
93967
  ]);
93933
93968
  await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
93934
93969
  return {
@@ -93960,7 +93995,7 @@ class EmbedService {
93960
93995
  if (outcome) {
93961
93996
  throw new AccessDeniedError("This launch has ended");
93962
93997
  }
93963
- return this.deps.mintGameToken(launch.gameId, launch.userId);
93998
+ return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
93964
93999
  }
93965
94000
  async endLaunch(launchId) {
93966
94001
  await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
@@ -95062,10 +95097,10 @@ function createSandboxAuthProvider() {
95062
95097
  }
95063
95098
  return deleted;
95064
95099
  },
95065
- async mintGameToken(gameId, userId) {
95100
+ async mintGameToken(gameId, userId, claims) {
95066
95101
  const header = btoa(JSON.stringify({ alg: "none", typ: "sandbox" }));
95067
95102
  const exp = Math.floor(Date.now() / 1000) + 15 * 60;
95068
- const payload = btoa(JSON.stringify({ sub: gameId, uid: userId, exp }));
95103
+ const payload = btoa(JSON.stringify({ ...claims, sub: gameId, uid: userId, exp }));
95069
95104
  const token = `${header}.${payload}.sandbox`;
95070
95105
  return { token, exp: exp * 1000 };
95071
95106
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.12.2-beta.4",
3
+ "version": "0.12.2-beta.6",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {