@superbuilders/primer-tives 2.2.1 → 3.5.1

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 (48) hide show
  1. package/README.md +813 -446
  2. package/dist/client/auth/access-token.d.ts +10 -0
  3. package/dist/client/auth/access-token.d.ts.map +1 -0
  4. package/dist/client/auth/browser.d.ts +20 -0
  5. package/dist/client/auth/browser.d.ts.map +1 -0
  6. package/dist/client/auth/callback.d.ts +10 -0
  7. package/dist/client/auth/callback.d.ts.map +1 -0
  8. package/dist/client/auth/hosted-popup.d.ts +14 -0
  9. package/dist/client/auth/hosted-popup.d.ts.map +1 -0
  10. package/dist/client/auth/provider.d.ts +14 -0
  11. package/dist/client/auth/provider.d.ts.map +1 -0
  12. package/dist/client/auth/storage.d.ts +9 -0
  13. package/dist/client/auth/storage.d.ts.map +1 -0
  14. package/dist/client/create.d.ts +22 -25
  15. package/dist/client/create.d.ts.map +1 -1
  16. package/dist/client/create.type-test.d.ts +2 -0
  17. package/dist/client/create.type-test.d.ts.map +1 -0
  18. package/dist/client/index.d.ts +1 -1
  19. package/dist/client/index.d.ts.map +1 -1
  20. package/dist/client/index.js +336 -74
  21. package/dist/client/index.js.map +14 -10
  22. package/dist/client/runtime-subject.d.ts +4 -0
  23. package/dist/client/runtime-subject.d.ts.map +1 -0
  24. package/dist/client/session.d.ts +2 -2
  25. package/dist/client/session.d.ts.map +1 -1
  26. package/dist/client/transport.d.ts +6 -4
  27. package/dist/client/transport.d.ts.map +1 -1
  28. package/dist/errors.d.ts +7 -3
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/errors.js +14 -6
  31. package/dist/errors.js.map +3 -3
  32. package/dist/subject-pcis.d.ts +11 -5
  33. package/dist/subject-pcis.d.ts.map +1 -1
  34. package/dist/subject-pcis.js +39 -0
  35. package/dist/subject-pcis.js.map +11 -0
  36. package/dist/subject.d.ts +1 -2
  37. package/dist/subject.d.ts.map +1 -1
  38. package/dist/subject.js.map +1 -1
  39. package/dist/version.d.ts +1 -1
  40. package/package.json +2 -6
  41. package/dist/server/create-server.d.ts +0 -17
  42. package/dist/server/create-server.d.ts.map +0 -1
  43. package/dist/server/exchange.d.ts +0 -16
  44. package/dist/server/exchange.d.ts.map +0 -1
  45. package/dist/server/index.d.ts +0 -3
  46. package/dist/server/index.d.ts.map +0 -1
  47. package/dist/server/index.js +0 -173
  48. package/dist/server/index.js.map +0 -12
@@ -3,7 +3,6 @@ import * as errors from "@superbuilders/errors";
3
3
  var ErrNetwork = errors.new("network");
4
4
  var ErrJsonParse = errors.new("json parse");
5
5
  var ErrUnsupportedPci = errors.new("unsupported pci");
6
- var ErrMissingRequiredPci = errors.new("missing required pci");
7
6
  var ErrInvalidAccessToken = errors.new("invalid access token");
8
7
  var ErrMalformedAccessToken = errors.new("malformed access token");
9
8
  var ErrTokenExpired = errors.new("access token expired");
@@ -17,7 +16,12 @@ var ErrRateLimited = errors.new("rate limited");
17
16
  var ErrServiceUnavailable = errors.new("service unavailable");
18
17
  var ErrNotSerializable = errors.new("PrimerState is live in-memory state and must not be serialized or stored");
19
18
  var ErrInvalidSubmission = errors.new("invalid submission");
20
- var ErrInvalidSecretKey = errors.new("invalid secret key");
19
+ var ErrAuthUnavailable = errors.new("auth unavailable");
20
+ var ErrAuthConfigInvalid = errors.new("auth config invalid");
21
+ var ErrAuthCallbackInvalid = errors.new("auth callback invalid");
22
+ var ErrAuthStateMismatch = errors.new("auth state mismatch");
23
+ var ErrAuthPopupBlocked = errors.new("auth popup blocked");
24
+ var ErrAuthCancelled = errors.new("auth cancelled");
21
25
  var ErrSdkUpgradeRequired = errors.new("sdk upgrade required");
22
26
  // src/contracts/content.ts
23
27
  function inlinesToPlainText(nodes) {
@@ -333,16 +337,11 @@ function validateSubmissionForInteraction(interaction, submission) {
333
337
  function submissionValidationMessage(result) {
334
338
  return result.issues.join("; ");
335
339
  }
336
- // src/subject.ts
337
- var SUBJECTS = ["math", "vocabulary", "science"];
338
- // src/client/create.ts
339
- import * as errors10 from "@superbuilders/errors";
340
-
341
340
  // src/client/transport.ts
342
341
  import * as errors2 from "@superbuilders/errors";
343
342
 
344
343
  // src/version.ts
345
- var SDK_VERSION = "2.2.1";
344
+ var SDK_VERSION = "3.5.1";
346
345
  var NPM_PACKAGE_URL = "https://www.npmjs.com/package/@superbuilders/primer-tives";
347
346
 
348
347
  // src/client/transport.ts
@@ -477,7 +476,8 @@ function createTransport(tc) {
477
476
  method: "POST",
478
477
  headers: {
479
478
  "Content-Type": "application/json",
480
- Authorization: `Bearer ${tc.accessToken}`,
479
+ Authorization: `Bearer ${tc.accessToken.value}`,
480
+ "X-Primer-Publishable-Key": tc.publishableKey,
481
481
  "X-Primer-SDK-Version": SDK_VERSION
482
482
  },
483
483
  body: JSON.stringify(body),
@@ -1065,7 +1065,12 @@ function makeSession(sc) {
1065
1065
  phase,
1066
1066
  intentKind: intent.kind
1067
1067
  });
1068
- return { phase: "fatal", error: result.error, retriable: false, toJSON: poisonToJSON };
1068
+ return {
1069
+ phase: "fatal",
1070
+ error: result.error,
1071
+ retriable: false,
1072
+ toJSON: poisonToJSON
1073
+ };
1069
1074
  }
1070
1075
  logger.warn("retriable transport error", {
1071
1076
  error: result.error,
@@ -1128,39 +1133,222 @@ function makeSession(sc) {
1128
1133
  return { execute };
1129
1134
  }
1130
1135
 
1131
- // src/subject-pcis.ts
1132
- var REQUIRED_PCIS_BY_SUBJECT = {
1133
- math: ["urn:primer:pci:fraction-input"],
1134
- vocabulary: [],
1135
- science: []
1136
- };
1137
- function requiredPcisForSubject(subject) {
1138
- if (subject === "all") {
1139
- const all = [];
1140
- for (const s of SUBJECTS) {
1141
- for (const pci of REQUIRED_PCIS_BY_SUBJECT[s]) {
1142
- if (!all.includes(pci)) {
1143
- all.push(pci);
1144
- }
1145
- }
1136
+ // src/client/auth/browser.ts
1137
+ import * as errors10 from "@superbuilders/errors";
1138
+ function browserStorage(options, logger) {
1139
+ if (options !== undefined && options.storage !== undefined) {
1140
+ return options.storage;
1141
+ }
1142
+ if (typeof globalThis.sessionStorage === "undefined") {
1143
+ logger.error("auth storage unavailable");
1144
+ throw ErrAuthUnavailable;
1145
+ }
1146
+ return globalThis.sessionStorage;
1147
+ }
1148
+ function currentUrl(options, logger) {
1149
+ if (options !== undefined && options.currentUrl !== undefined) {
1150
+ if (!URL.canParse(options.currentUrl)) {
1151
+ logger.error("auth current url invalid", { currentUrl: options.currentUrl });
1152
+ throw ErrAuthConfigInvalid;
1146
1153
  }
1147
- return all;
1154
+ return new URL(options.currentUrl);
1148
1155
  }
1149
- return REQUIRED_PCIS_BY_SUBJECT[subject];
1156
+ if (typeof globalThis.location === "undefined") {
1157
+ logger.error("auth location unavailable");
1158
+ throw ErrAuthUnavailable;
1159
+ }
1160
+ return new URL(globalThis.location.href);
1150
1161
  }
1151
- function missingPcisForSubject(subject, provided) {
1152
- const required = requiredPcisForSubject(subject);
1153
- const missing = [];
1154
- for (const pci of required) {
1155
- if (provided === undefined || !provided.includes(pci)) {
1156
- missing.push(pci);
1162
+ function redirectUri(options, url, logger) {
1163
+ if (options !== undefined && options.redirectUri !== undefined) {
1164
+ if (!URL.canParse(options.redirectUri)) {
1165
+ logger.error("auth redirect uri invalid", { redirectUri: options.redirectUri });
1166
+ throw ErrAuthConfigInvalid;
1157
1167
  }
1168
+ return options.redirectUri;
1169
+ }
1170
+ return `${url.origin}${url.pathname}${url.search}`;
1171
+ }
1172
+ function randomClientState(logger) {
1173
+ if (typeof globalThis.crypto === "undefined") {
1174
+ logger.error("auth crypto unavailable");
1175
+ throw ErrAuthUnavailable;
1176
+ }
1177
+ const bytes = new Uint8Array(24);
1178
+ globalThis.crypto.getRandomValues(bytes);
1179
+ let result = "";
1180
+ for (const byte of bytes) {
1181
+ result += byte.toString(16).padStart(2, "0");
1182
+ }
1183
+ return result;
1184
+ }
1185
+ function clearCallbackHash(options, url) {
1186
+ if (options !== undefined && options.currentUrl !== undefined) {
1187
+ return;
1188
+ }
1189
+ if (typeof globalThis.history === "undefined") {
1190
+ return;
1191
+ }
1192
+ const cleanUrl = `${url.pathname}${url.search}`;
1193
+ globalThis.history.replaceState(globalThis.history.state, "", cleanUrl);
1194
+ }
1195
+ function openAuthPopup(url, options, logger) {
1196
+ if (typeof globalThis.open === "undefined") {
1197
+ logger.error("auth popup api unavailable");
1198
+ throw ErrAuthUnavailable;
1199
+ }
1200
+ let target = "primer-auth";
1201
+ if (options !== undefined && options.popupTarget !== undefined) {
1202
+ target = options.popupTarget;
1203
+ }
1204
+ let features = "popup,width=480,height=720";
1205
+ if (options !== undefined && options.popupFeatures !== undefined) {
1206
+ features = options.popupFeatures;
1207
+ }
1208
+ const popup = globalThis.open(url, target, features);
1209
+ if (popup === null) {
1210
+ logger.error("auth popup blocked");
1211
+ throw ErrAuthPopupBlocked;
1212
+ }
1213
+ return popup;
1214
+ }
1215
+ function readablePopupUrl(popup) {
1216
+ const result = errors10.trySync(function readLocation() {
1217
+ return popup.location.href;
1218
+ });
1219
+ if (result.error) {
1220
+ return null;
1158
1221
  }
1159
- return missing;
1222
+ return result.data;
1223
+ }
1224
+ function sleep(ms) {
1225
+ return new Promise(function resolveLater(resolve) {
1226
+ setTimeout(resolve, ms);
1227
+ });
1160
1228
  }
1161
1229
 
1162
- // src/client/create.ts
1230
+ // src/client/auth/callback.ts
1231
+ var ACCESS_TOKEN_HASH_PARAM = "primer_access_token";
1232
+ var AUTH_STATUS_HASH_PARAM = "primer_auth";
1233
+ var AUTH_STATE_HASH_PARAM = "primer_state";
1234
+ var AUTH_SUCCESS = "success";
1235
+ var AUTH_ERROR = "error";
1236
+ function readAuthCallback(url, logger) {
1237
+ if (url.hash.length === 0) {
1238
+ return null;
1239
+ }
1240
+ const hash = new URLSearchParams(url.hash.slice(1));
1241
+ const authStatus = hash.get(AUTH_STATUS_HASH_PARAM);
1242
+ if (authStatus === null) {
1243
+ return null;
1244
+ }
1245
+ if (authStatus === AUTH_ERROR) {
1246
+ logger.error("auth callback returned error");
1247
+ throw ErrAuthCallbackInvalid;
1248
+ }
1249
+ if (authStatus !== AUTH_SUCCESS) {
1250
+ logger.error("auth callback status invalid", { authStatus });
1251
+ throw ErrAuthCallbackInvalid;
1252
+ }
1253
+ const accessToken = hash.get(ACCESS_TOKEN_HASH_PARAM);
1254
+ const state = hash.get(AUTH_STATE_HASH_PARAM);
1255
+ if (accessToken === null || accessToken.length === 0 || state === null || state.length === 0) {
1256
+ logger.error("auth callback missing token or state");
1257
+ throw ErrAuthCallbackInvalid;
1258
+ }
1259
+ return { accessToken, state };
1260
+ }
1261
+ function requireMatchingCallbackState(callback, expectedState, logger) {
1262
+ if (expectedState === null) {
1263
+ logger.error("auth callback expected state missing");
1264
+ throw ErrAuthCallbackInvalid;
1265
+ }
1266
+ if (callback.state !== expectedState) {
1267
+ logger.error("auth callback state mismatch");
1268
+ throw ErrAuthStateMismatch;
1269
+ }
1270
+ }
1271
+
1272
+ // src/client/auth/hosted-popup.ts
1273
+ import * as errors11 from "@superbuilders/errors";
1274
+ var DEFAULT_POPUP_TIMEOUT_MS = 10 * 60 * 1000;
1275
+ var POPUP_POLL_MS = 250;
1276
+ function hostedAuthUrl(config) {
1277
+ const logger = config.logger;
1278
+ if (!URL.canParse(config.origin)) {
1279
+ logger.error("hosted auth origin invalid", { origin: config.origin });
1280
+ throw ErrAuthCallbackInvalid;
1281
+ }
1282
+ const authUrl = new URL("/auth/timeback", config.origin);
1283
+ authUrl.searchParams.set("publishableKey", config.publishableKey);
1284
+ authUrl.searchParams.set("redirectUri", redirectUri(config.options, config.currentUrl, logger));
1285
+ authUrl.searchParams.set("state", config.clientState);
1286
+ return authUrl.toString();
1287
+ }
1288
+ function popupTimeoutMs(options) {
1289
+ if (options !== undefined && options.popupTimeoutMs !== undefined) {
1290
+ return options.popupTimeoutMs;
1291
+ }
1292
+ return DEFAULT_POPUP_TIMEOUT_MS;
1293
+ }
1294
+ function readPopupCallback(href, config) {
1295
+ const logger = config.logger;
1296
+ if (!URL.canParse(href)) {
1297
+ return null;
1298
+ }
1299
+ const callbackResult = errors11.trySync(function readCallback() {
1300
+ return readAuthCallback(new URL(href), logger);
1301
+ });
1302
+ if (callbackResult.error) {
1303
+ logger.error("hosted auth popup callback invalid", { error: callbackResult.error });
1304
+ throw callbackResult.error;
1305
+ }
1306
+ const callback = callbackResult.data;
1307
+ if (callback === null) {
1308
+ return null;
1309
+ }
1310
+ if (callback.state !== config.clientState) {
1311
+ logger.error("hosted auth popup state mismatch");
1312
+ throw ErrAuthStateMismatch;
1313
+ }
1314
+ return callback.accessToken;
1315
+ }
1316
+ async function beginHostedPopup(config) {
1317
+ const logger = config.logger;
1318
+ const popup = openAuthPopup(hostedAuthUrl(config), config.options, logger);
1319
+ const expiresAt = Date.now() + popupTimeoutMs(config.options);
1320
+ while (Date.now() < expiresAt) {
1321
+ if (popup.closed) {
1322
+ logger.error("hosted auth popup closed");
1323
+ throw ErrAuthCancelled;
1324
+ }
1325
+ const href = readablePopupUrl(popup);
1326
+ if (href !== null) {
1327
+ const accessTokenResult = errors11.trySync(function readAccessToken() {
1328
+ return readPopupCallback(href, config);
1329
+ });
1330
+ if (accessTokenResult.error) {
1331
+ popup.close();
1332
+ logger.error("hosted auth popup failed", { error: accessTokenResult.error });
1333
+ throw accessTokenResult.error;
1334
+ }
1335
+ if (accessTokenResult.data !== null) {
1336
+ popup.close();
1337
+ logger.debug("hosted auth popup completed");
1338
+ return accessTokenResult.data;
1339
+ }
1340
+ }
1341
+ await sleep(POPUP_POLL_MS);
1342
+ }
1343
+ popup.close();
1344
+ logger.error("hosted auth popup timed out");
1345
+ throw ErrAuthCancelled;
1346
+ }
1347
+
1348
+ // src/client/auth/access-token.ts
1349
+ import * as errors12 from "@superbuilders/errors";
1163
1350
  var ACCESS_TOKEN_PREFIX = "eyJ";
1351
+ var resolvedAccessTokenBrand = Symbol("primer resolved access token");
1164
1352
  function isMalformedJws(token) {
1165
1353
  if (!token.startsWith(ACCESS_TOKEN_PREFIX)) {
1166
1354
  return true;
@@ -1168,53 +1356,127 @@ function isMalformedJws(token) {
1168
1356
  const dotCount = token.split(".").length - 1;
1169
1357
  return dotCount !== 2;
1170
1358
  }
1171
- function create(config) {
1172
- const logger = config.logger;
1173
- let supportedPcis = [];
1174
- if (config.supportedPcis !== undefined) {
1175
- supportedPcis = config.supportedPcis;
1359
+ function resolveAccessToken(token, logger) {
1360
+ if (isMalformedJws(token)) {
1361
+ logger.error("malformed access token", { prefix: ACCESS_TOKEN_PREFIX });
1362
+ throw errors12.wrap(ErrMalformedAccessToken, `token must start with '${ACCESS_TOKEN_PREFIX}' and contain two dots`);
1176
1363
  }
1177
- const missingPcis = missingPcisForSubject(config.subject, supportedPcis);
1178
- if (missingPcis.length > 0) {
1179
- logger.error("renderer missing required pcis", { subject: config.subject, missingPcis });
1180
- throw errors10.wrap(ErrMissingRequiredPci, `subject '${config.subject}'`);
1364
+ return { value: token, [resolvedAccessTokenBrand]: true };
1365
+ }
1366
+
1367
+ // src/client/auth/storage.ts
1368
+ var ACCESS_TOKEN_KEY_PREFIX = "primer:access-token";
1369
+ var AUTH_STATE_KEY_PREFIX = "primer:auth-state";
1370
+ function accessTokenStorageKey(publishableKey) {
1371
+ return `${ACCESS_TOKEN_KEY_PREFIX}:${publishableKey}`;
1372
+ }
1373
+ function authStateStorageKey(publishableKey) {
1374
+ return `${AUTH_STATE_KEY_PREFIX}:${publishableKey}`;
1375
+ }
1376
+ function loadStoredAccessToken(storage, publishableKey) {
1377
+ const token = storage.getItem(accessTokenStorageKey(publishableKey));
1378
+ if (token === null || token.length === 0) {
1379
+ return null;
1181
1380
  }
1182
- if (isMalformedJws(config.accessToken)) {
1183
- logger.error("malformed access token", { prefix: ACCESS_TOKEN_PREFIX });
1184
- throw errors10.wrap(ErrMalformedAccessToken, `token must start with '${ACCESS_TOKEN_PREFIX}' and contain two dots`);
1381
+ return token;
1382
+ }
1383
+ function storeAccessToken(storage, publishableKey, accessToken) {
1384
+ storage.setItem(accessTokenStorageKey(publishableKey), accessToken);
1385
+ }
1386
+ function loadAuthState(storage, publishableKey) {
1387
+ const state = storage.getItem(authStateStorageKey(publishableKey));
1388
+ if (state === null || state.length === 0) {
1389
+ return null;
1185
1390
  }
1186
- const transport = createTransport({
1187
- accessToken: config.accessToken,
1188
- subject: config.subject,
1189
- origin: config.origin,
1190
- fetch: config.fetch,
1191
- abort: config.abort,
1391
+ return state;
1392
+ }
1393
+ function storeAuthState(storage, publishableKey, state) {
1394
+ storage.setItem(authStateStorageKey(publishableKey), state);
1395
+ }
1396
+ function clearAuthState(storage, publishableKey) {
1397
+ storage.removeItem(authStateStorageKey(publishableKey));
1398
+ }
1399
+
1400
+ // src/client/auth/provider.ts
1401
+ async function resolveHostedAccessToken(options) {
1402
+ const logger = options.logger;
1403
+ const storage = browserStorage(options.hostedAuth, logger);
1404
+ const url = currentUrl(options.hostedAuth, logger);
1405
+ const callback = readAuthCallback(url, logger);
1406
+ if (callback !== null) {
1407
+ requireMatchingCallbackState(callback, loadAuthState(storage, options.publishableKey), logger);
1408
+ storeAccessToken(storage, options.publishableKey, callback.accessToken);
1409
+ clearAuthState(storage, options.publishableKey);
1410
+ clearCallbackHash(options.hostedAuth, url);
1411
+ return resolveAccessToken(callback.accessToken, logger);
1412
+ }
1413
+ const stored = loadStoredAccessToken(storage, options.publishableKey);
1414
+ if (stored !== null) {
1415
+ return resolveAccessToken(stored, logger);
1416
+ }
1417
+ const clientState = randomClientState(logger);
1418
+ storeAuthState(storage, options.publishableKey, clientState);
1419
+ const accessToken = await beginHostedPopup({
1420
+ origin: options.origin,
1421
+ publishableKey: options.publishableKey,
1422
+ currentUrl: url,
1423
+ clientState,
1424
+ options: options.hostedAuth,
1192
1425
  logger
1193
1426
  });
1194
- let startPromise;
1195
- async function doStart() {
1196
- logger.debug("start");
1197
- const s = makeSession({
1198
- subject: config.subject,
1199
- supportedPcis,
1200
- logger,
1201
- transport
1202
- });
1203
- return s.execute({ kind: "observation" }, "observation");
1427
+ storeAccessToken(storage, options.publishableKey, accessToken);
1428
+ clearAuthState(storage, options.publishableKey);
1429
+ return resolveAccessToken(accessToken, logger);
1430
+ }
1431
+ async function resolveRuntimeAccessToken(options) {
1432
+ if (options.accessToken !== undefined) {
1433
+ return resolveAccessToken(options.accessToken, options.logger);
1204
1434
  }
1205
- return {
1206
- start() {
1207
- if (startPromise) {
1208
- logger.debug("start already called");
1209
- return startPromise;
1210
- }
1211
- startPromise = doStart();
1212
- return startPromise;
1213
- }
1214
- };
1435
+ return resolveHostedAccessToken(options);
1436
+ }
1437
+
1438
+ // src/client/create.ts
1439
+ function supportedPcisOrEmpty(supportedPcis) {
1440
+ if (supportedPcis !== undefined) {
1441
+ return supportedPcis;
1442
+ }
1443
+ return [];
1444
+ }
1445
+ function runtimeSubject(subject) {
1446
+ if (subject === undefined) {
1447
+ return "all";
1448
+ }
1449
+ return subject;
1450
+ }
1451
+ async function create(options) {
1452
+ const logger = options.logger;
1453
+ logger.debug("create");
1454
+ const subject = runtimeSubject(options.subject);
1455
+ const accessToken = await resolveRuntimeAccessToken({
1456
+ accessToken: options.accessToken,
1457
+ publishableKey: options.publishableKey,
1458
+ origin: options.origin,
1459
+ logger
1460
+ });
1461
+ const transport = createTransport({
1462
+ accessToken,
1463
+ publishableKey: options.publishableKey,
1464
+ subject,
1465
+ origin: options.origin,
1466
+ fetch: options.fetch,
1467
+ abort: options.abort,
1468
+ logger
1469
+ });
1470
+ const session = makeSession({
1471
+ subject,
1472
+ supportedPcis: supportedPcisOrEmpty(options.supportedPcis),
1473
+ logger,
1474
+ transport
1475
+ });
1476
+ return session.execute({ kind: "observation" }, "observation");
1215
1477
  }
1216
1478
  export {
1217
1479
  create
1218
1480
  };
1219
1481
 
1220
- //# debugId=859C0D980BD7BD1D64756E2164756E21
1482
+ //# debugId=DE8A27A355B8A82664756E2164756E21