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

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 +31 -14
  2. package/dist/server.js +31 -14
  3. package/package.json +1 -1
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.6",
2520
+ version: "0.12.2-beta.7",
2521
2521
  description: "Local development server for Playcademy game development",
2522
2522
  type: "module",
2523
2523
  exports: {
@@ -20906,11 +20906,14 @@ var init_table8 = __esm(() => {
20906
20906
  id: uuid("id").primaryKey().defaultRandom(),
20907
20907
  name: text("name").notNull(),
20908
20908
  clientId: text("client_id").notNull().unique(),
20909
+ handle: text("handle").unique(),
20909
20910
  allowedOrigins: text("allowed_origins").array().notNull().default([]),
20910
20911
  enabled: boolean("enabled").notNull().default(true),
20911
20912
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
20912
20913
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
20913
- });
20914
+ }, (table) => [
20915
+ check("embed_partners_handle_check", sql`${table.handle} ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?([.][a-z0-9]([a-z0-9-]*[a-z0-9])?)+$'`)
20916
+ ]);
20914
20917
  embedLaunches = pgTable("embed_launches", {
20915
20918
  id: uuid("id").primaryKey().defaultRandom(),
20916
20919
  partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
@@ -20920,6 +20923,7 @@ var init_table8 = __esm(() => {
20920
20923
  timebackCourseId: text("timeback_course_id"),
20921
20924
  contextId: text("context_id"),
20922
20925
  contextRole: embedContextRoleEnum("context_role"),
20926
+ contextAttributes: jsonb("context_attributes").$type(),
20923
20927
  passHash: text("pass_hash").unique(),
20924
20928
  passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
20925
20929
  redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
@@ -50827,7 +50831,7 @@ function isCanonicalOrigin(value) {
50827
50831
  }
50828
50832
  return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
50829
50833
  }
50830
- var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50834
+ var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, MAX_CONTEXT_ATTRIBUTES = 10, EmbedContextAttributesSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
50831
50835
  var init_schemas5 = __esm(() => {
50832
50836
  init_esm();
50833
50837
  init_table8();
@@ -50838,9 +50842,17 @@ var init_schemas5 = __esm(() => {
50838
50842
  EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
50839
50843
  message: "context.id must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
50840
50844
  });
50845
+ EmbedContextAttributesSchema = recordType(stringType().regex(/^[A-Za-z][A-Za-z0-9_]{0,39}$/, {
50846
+ message: "context.attributes keys must be 1-40 characters: a letter, then letters, digits or _"
50847
+ }), stringType().regex(/^[A-Za-z0-9._~-]{1,100}$/, {
50848
+ message: "context.attributes values must be 1-100 URL-safe characters (letters, digits, . _ ~ -)"
50849
+ })).refine((attributes) => Object.keys(attributes).length <= MAX_CONTEXT_ATTRIBUTES, {
50850
+ message: `context.attributes takes at most ${MAX_CONTEXT_ATTRIBUTES} keys`
50851
+ });
50841
50852
  EmbedContextSchema = objectType({
50842
50853
  id: EmbedContextIdSchema,
50843
- role: enumType(embedContextRoleEnum.enumValues).optional()
50854
+ role: enumType(embedContextRoleEnum.enumValues).optional(),
50855
+ attributes: EmbedContextAttributesSchema.optional()
50844
50856
  }).strict();
50845
50857
  EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
50846
50858
  EnsureEmbedPartnerSchema = objectType({
@@ -95699,14 +95711,16 @@ var init_embed_util = __esm(() => {
95699
95711
  async function scopedContextId(partnerId, contextId) {
95700
95712
  return sha256Hex(`${partnerId}:${contextId}`);
95701
95713
  }
95702
- function launchTokenClaims(launch) {
95714
+ function launchTokenClaims(launch, partner) {
95703
95715
  if (!launch.contextId) {
95704
95716
  return {};
95705
95717
  }
95706
95718
  return {
95707
95719
  context: {
95708
95720
  id: launch.contextId,
95709
- ...launch.contextRole && { role: launch.contextRole }
95721
+ ...launch.contextRole && { role: launch.contextRole },
95722
+ ...launch.contextAttributes && { attributes: launch.contextAttributes },
95723
+ ...partner.handle && { partner: partner.handle }
95710
95724
  }
95711
95725
  };
95712
95726
  }
@@ -95756,6 +95770,7 @@ class EmbedService {
95756
95770
  timebackCourseId: input.timebackCourseId ?? null,
95757
95771
  contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
95758
95772
  contextRole: input.context?.role ?? null,
95773
+ contextAttributes: input.context?.attributes ?? null,
95759
95774
  passHash: await sha256Hex(pass),
95760
95775
  passExpiresAt,
95761
95776
  expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
@@ -95767,12 +95782,15 @@ class EmbedService {
95767
95782
  "app.launch.id": launch.id,
95768
95783
  "app.user.id": user.id,
95769
95784
  "app.embed.has_context": input.context !== undefined,
95770
- ...input.context?.role && { "app.embed.context_role": input.context.role }
95785
+ ...input.context?.role && { "app.embed.context_role": input.context.role },
95786
+ ...input.context?.attributes && {
95787
+ "app.embed.context_attribute_keys": Object.keys(input.context.attributes).toSorted()
95788
+ }
95771
95789
  });
95772
95790
  return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
95773
95791
  }
95774
95792
  async redeemLaunch(input, timeZone) {
95775
- const launch = await this.consumePass(input);
95793
+ const { launch, partner } = await this.consumePass(input);
95776
95794
  try {
95777
95795
  const user = await this.deps.db.query.users.findFirst({
95778
95796
  where: eq(users.id, launch.userId)
@@ -95783,7 +95801,7 @@ class EmbedService {
95783
95801
  const [game, profile, token] = await Promise.all([
95784
95802
  this.deps.getGameForStudent(launch.gameId, user),
95785
95803
  this.deps.getGameProfile(user, launch.gameId, timeZone),
95786
- this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
95804
+ this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch, partner))
95787
95805
  ]);
95788
95806
  await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
95789
95807
  return {
@@ -95815,7 +95833,7 @@ class EmbedService {
95815
95833
  if (outcome) {
95816
95834
  throw new AccessDeniedError("This launch has ended");
95817
95835
  }
95818
- return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
95836
+ return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch, partner));
95819
95837
  }
95820
95838
  async endLaunch(launchId) {
95821
95839
  await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
@@ -95835,18 +95853,17 @@ class EmbedService {
95835
95853
  });
95836
95854
  const partner = await this.deps.db.query.embedPartners.findFirst({
95837
95855
  where: eq(embedPartners.id, launch.partnerId),
95838
- columns: { enabled: true, allowedOrigins: true }
95856
+ columns: { enabled: true, allowedOrigins: true, handle: true }
95839
95857
  });
95840
95858
  const originMatches = launch.parentOrigin === input.parentOrigin;
95841
- const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
95842
- if (!originMatches || !stillAllowed) {
95859
+ if (!originMatches || !partner || !canPartnerLaunch(partner, launch.parentOrigin)) {
95843
95860
  await this.endLaunch(launch.id);
95844
95861
  setAttributes({
95845
95862
  "app.embed.outcome": originMatches ? "partner_disallowed" : "origin_mismatch"
95846
95863
  });
95847
95864
  throw new UnauthorizedError("This launch pass is not valid");
95848
95865
  }
95849
- return launch;
95866
+ return { launch, partner };
95850
95867
  }
95851
95868
  }
95852
95869
  function renewalRefusal(launch, isAllowed) {
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.6",
700
+ version: "0.12.2-beta.7",
701
701
  description: "Local development server for Playcademy game development",
702
702
  type: "module",
703
703
  exports: {
@@ -19086,11 +19086,14 @@ var init_table8 = __esm(() => {
19086
19086
  id: uuid("id").primaryKey().defaultRandom(),
19087
19087
  name: text("name").notNull(),
19088
19088
  clientId: text("client_id").notNull().unique(),
19089
+ handle: text("handle").unique(),
19089
19090
  allowedOrigins: text("allowed_origins").array().notNull().default([]),
19090
19091
  enabled: boolean("enabled").notNull().default(true),
19091
19092
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
19092
19093
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
19093
- });
19094
+ }, (table) => [
19095
+ check("embed_partners_handle_check", sql`${table.handle} ~ '^[a-z0-9]([a-z0-9-]*[a-z0-9])?([.][a-z0-9]([a-z0-9-]*[a-z0-9])?)+$'`)
19096
+ ]);
19094
19097
  embedLaunches = pgTable("embed_launches", {
19095
19098
  id: uuid("id").primaryKey().defaultRandom(),
19096
19099
  partnerId: uuid("partner_id").notNull().references(() => embedPartners.id, { onDelete: "restrict" }),
@@ -19100,6 +19103,7 @@ var init_table8 = __esm(() => {
19100
19103
  timebackCourseId: text("timeback_course_id"),
19101
19104
  contextId: text("context_id"),
19102
19105
  contextRole: embedContextRoleEnum("context_role"),
19106
+ contextAttributes: jsonb("context_attributes").$type(),
19103
19107
  passHash: text("pass_hash").unique(),
19104
19108
  passExpiresAt: timestamp("pass_expires_at", { withTimezone: true }).notNull(),
19105
19109
  redeemedAt: timestamp("redeemed_at", { withTimezone: true }),
@@ -49007,7 +49011,7 @@ function isCanonicalOrigin(value) {
49007
49011
  }
49008
49012
  return url.protocol === "https:" || url.protocol === "http:" && isLocalHostname(url.hostname);
49009
49013
  }
49010
- var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
49014
+ var LOCAL_HOSTNAMES, EmbedOriginSchema, EmbedContextIdSchema, MAX_CONTEXT_ATTRIBUTES = 10, EmbedContextAttributesSchema, EmbedContextSchema, EmbedPartnerClientIdSchema, EnsureEmbedPartnerSchema, AddEmbedPartnerOriginSchema, CreateEmbedLaunchSchema, RedeemEmbedPassSchema;
49011
49015
  var init_schemas5 = __esm(() => {
49012
49016
  init_esm();
49013
49017
  init_table8();
@@ -49018,9 +49022,17 @@ var init_schemas5 = __esm(() => {
49018
49022
  EmbedContextIdSchema = stringType().regex(/^[A-Za-z0-9._~-]{1,128}$/, {
49019
49023
  message: "context.id must be 1-128 URL-safe characters (letters, digits, . _ ~ -)"
49020
49024
  });
49025
+ EmbedContextAttributesSchema = recordType(stringType().regex(/^[A-Za-z][A-Za-z0-9_]{0,39}$/, {
49026
+ message: "context.attributes keys must be 1-40 characters: a letter, then letters, digits or _"
49027
+ }), stringType().regex(/^[A-Za-z0-9._~-]{1,100}$/, {
49028
+ message: "context.attributes values must be 1-100 URL-safe characters (letters, digits, . _ ~ -)"
49029
+ })).refine((attributes) => Object.keys(attributes).length <= MAX_CONTEXT_ATTRIBUTES, {
49030
+ message: `context.attributes takes at most ${MAX_CONTEXT_ATTRIBUTES} keys`
49031
+ });
49021
49032
  EmbedContextSchema = objectType({
49022
49033
  id: EmbedContextIdSchema,
49023
- role: enumType(embedContextRoleEnum.enumValues).optional()
49034
+ role: enumType(embedContextRoleEnum.enumValues).optional(),
49035
+ attributes: EmbedContextAttributesSchema.optional()
49024
49036
  }).strict();
49025
49037
  EmbedPartnerClientIdSchema = stringType().trim().min(1).max(200);
49026
49038
  EnsureEmbedPartnerSchema = objectType({
@@ -93879,14 +93891,16 @@ var init_embed_util = __esm(() => {
93879
93891
  async function scopedContextId(partnerId, contextId) {
93880
93892
  return sha256Hex(`${partnerId}:${contextId}`);
93881
93893
  }
93882
- function launchTokenClaims(launch) {
93894
+ function launchTokenClaims(launch, partner) {
93883
93895
  if (!launch.contextId) {
93884
93896
  return {};
93885
93897
  }
93886
93898
  return {
93887
93899
  context: {
93888
93900
  id: launch.contextId,
93889
- ...launch.contextRole && { role: launch.contextRole }
93901
+ ...launch.contextRole && { role: launch.contextRole },
93902
+ ...launch.contextAttributes && { attributes: launch.contextAttributes },
93903
+ ...partner.handle && { partner: partner.handle }
93890
93904
  }
93891
93905
  };
93892
93906
  }
@@ -93936,6 +93950,7 @@ class EmbedService {
93936
93950
  timebackCourseId: input.timebackCourseId ?? null,
93937
93951
  contextId: input.context ? await scopedContextId(partner.id, input.context.id) : null,
93938
93952
  contextRole: input.context?.role ?? null,
93953
+ contextAttributes: input.context?.attributes ?? null,
93939
93954
  passHash: await sha256Hex(pass),
93940
93955
  passExpiresAt,
93941
93956
  expiresAt: new Date(now + EMBED_LAUNCH_TTL_MS)
@@ -93947,12 +93962,15 @@ class EmbedService {
93947
93962
  "app.launch.id": launch.id,
93948
93963
  "app.user.id": user.id,
93949
93964
  "app.embed.has_context": input.context !== undefined,
93950
- ...input.context?.role && { "app.embed.context_role": input.context.role }
93965
+ ...input.context?.role && { "app.embed.context_role": input.context.role },
93966
+ ...input.context?.attributes && {
93967
+ "app.embed.context_attribute_keys": Object.keys(input.context.attributes).toSorted()
93968
+ }
93951
93969
  });
93952
93970
  return { launchId: launch.id, pass, passExpiresAt, gameSlug: game.slug };
93953
93971
  }
93954
93972
  async redeemLaunch(input, timeZone) {
93955
- const launch = await this.consumePass(input);
93973
+ const { launch, partner } = await this.consumePass(input);
93956
93974
  try {
93957
93975
  const user = await this.deps.db.query.users.findFirst({
93958
93976
  where: eq(users.id, launch.userId)
@@ -93963,7 +93981,7 @@ class EmbedService {
93963
93981
  const [game, profile, token] = await Promise.all([
93964
93982
  this.deps.getGameForStudent(launch.gameId, user),
93965
93983
  this.deps.getGameProfile(user, launch.gameId, timeZone),
93966
- this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch))
93984
+ this.deps.mintGameToken(launch.gameId, user.id, launchTokenClaims(launch, partner))
93967
93985
  ]);
93968
93986
  await this.deps.db.update(embedLaunches).set({ redeemedAt: new Date }).where(eq(embedLaunches.id, launch.id));
93969
93987
  return {
@@ -93995,7 +94013,7 @@ class EmbedService {
93995
94013
  if (outcome) {
93996
94014
  throw new AccessDeniedError("This launch has ended");
93997
94015
  }
93998
- return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch));
94016
+ return this.deps.mintGameToken(launch.gameId, launch.userId, launchTokenClaims(launch, partner));
93999
94017
  }
94000
94018
  async endLaunch(launchId) {
94001
94019
  await this.deps.db.update(embedLaunches).set({ endedAt: sql`coalesce(${embedLaunches.endedAt}, now())`, passHash: null }).where(eq(embedLaunches.id, launchId));
@@ -94015,18 +94033,17 @@ class EmbedService {
94015
94033
  });
94016
94034
  const partner = await this.deps.db.query.embedPartners.findFirst({
94017
94035
  where: eq(embedPartners.id, launch.partnerId),
94018
- columns: { enabled: true, allowedOrigins: true }
94036
+ columns: { enabled: true, allowedOrigins: true, handle: true }
94019
94037
  });
94020
94038
  const originMatches = launch.parentOrigin === input.parentOrigin;
94021
- const stillAllowed = partner !== undefined && canPartnerLaunch(partner, launch.parentOrigin);
94022
- if (!originMatches || !stillAllowed) {
94039
+ if (!originMatches || !partner || !canPartnerLaunch(partner, launch.parentOrigin)) {
94023
94040
  await this.endLaunch(launch.id);
94024
94041
  setAttributes({
94025
94042
  "app.embed.outcome": originMatches ? "partner_disallowed" : "origin_mismatch"
94026
94043
  });
94027
94044
  throw new UnauthorizedError("This launch pass is not valid");
94028
94045
  }
94029
- return launch;
94046
+ return { launch, partner };
94030
94047
  }
94031
94048
  }
94032
94049
  function renewalRefusal(launch, isAllowed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.12.2-beta.6",
3
+ "version": "0.12.2-beta.7",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {