@specific.dev/spectest 0.88.0 → 0.88.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.
@@ -26,11 +26,10 @@
26
26
  // `entry.mjs` routes by Host header, applies the per-provider path table,
27
27
  // and corrects the discovery document. It also re-issues every id_token
28
28
  // with one RSA key it generates at boot, and serves that key at the
29
- // provider's real JWKS URL. That single step covers three separate faults:
29
+ // provider's real JWKS URL. This normalizes signing and issuer handling:
30
30
  // the Google emulator signs HS256 and publishes an empty JWKS (so nothing
31
- // can verify its token), the Microsoft one cannot put the tenant in its
32
- // issuer, and any future provider whose issuer is not its base URL is
33
- // handled in advance.
31
+ // can verify its token), and providers whose issuer differs from their
32
+ // base URL can declare it explicitly.
34
33
  //
35
34
  // The Dockerfile is deliberately CONSTANT — every provider package is
36
35
  // installed whether or not this component is the one using it. Two reasons:
@@ -2,8 +2,8 @@ import type { ProviderOptions } from "./emulate/service.js";
2
2
  export interface MicrosoftOptions extends ProviderOptions {
3
3
  /**
4
4
  * Directory (tenant) the app signs in against — whatever its authority
5
- * URL uses. It appears in the issuer and in every endpoint path, so a
6
- * client that checks the issuer sees what it expects. Default `"common"`.
5
+ * URL uses. Sets the seeded users' and client's tenant as well as the
6
+ * issuer and endpoint paths. Default `"common"`.
7
7
  */
8
8
  tenantId?: string;
9
9
  }
@@ -8,16 +8,15 @@
8
8
  // widening what it serves is a table entry.
9
9
  import { emulatorService, oauthClients, resolveUsers } from "./emulate/service.js";
10
10
  function spec(users, opts) {
11
- const authority = `https://login.microsoftonline.com/${opts.tenantId || "common"}`;
11
+ const tenantId = opts.tenantId || "common";
12
+ const authority = `https://login.microsoftonline.com/${tenantId}`;
12
13
  return {
13
14
  name: "microsoft",
14
15
  module: "@emulators/microsoft",
15
16
  pluginExport: "microsoftPlugin",
16
17
  baseUrl: "https://login.microsoftonline.com",
17
18
  hosts: ["login.microsoftonline.com", "graph.microsoft.com"],
18
- // The emulator has no tenant, so its issuer is the bare host. A client
19
- // that checks the issuer against the authority it configured would
20
- // reject that; the re-issue step corrects it.
19
+ // Keep the re-issued token aligned with discovery and the seeded tenant.
21
20
  issuer: `${authority}/v2.0`,
22
21
  discovery: {
23
22
  issuer: `${authority}/v2.0`,
@@ -40,8 +39,8 @@ function spec(users, opts) {
40
39
  scopes: ["openid", "email", "profile", "User.Read"],
41
40
  },
42
41
  seed: {
43
- users: users.map((u) => ({ email: u.email, name: u.name })),
44
- ...oauthClients(opts.client),
42
+ users: users.map((u) => ({ email: u.email, name: u.name, tenant_id: tenantId })),
43
+ ...oauthClients(opts.client, { tenant_id: tenantId }),
45
44
  },
46
45
  };
47
46
  }
package/dist/index.d.ts CHANGED
@@ -823,7 +823,7 @@ export type ServiceImage = {
823
823
  * Runs in `defineEnvironment`, and in the daemon's runtime `startService`
824
824
  * twin, inside the VM, where the repo sits at the project root. Existence
825
825
  * checks go through the project-file resolver, so the same rules as
826
- * `ctx.readProjectFile` apply (a `spectest/.envignore`d path is refused
826
+ * `ctx.readProjectFile` apply (a `spectest/.specignore`d path is refused
827
827
  * instead of read stale).
828
828
  */
829
829
  export declare function validateServiceImage(serviceName: string, image: ServiceImage): ServiceImage;
package/dist/index.js CHANGED
@@ -253,7 +253,7 @@ const BUILD_TARGET_RE = /^[^\s]+$/;
253
253
  * Runs in `defineEnvironment`, and in the daemon's runtime `startService`
254
254
  * twin, inside the VM, where the repo sits at the project root. Existence
255
255
  * checks go through the project-file resolver, so the same rules as
256
- * `ctx.readProjectFile` apply (a `spectest/.envignore`d path is refused
256
+ * `ctx.readProjectFile` apply (a `spectest/.specignore`d path is refused
257
257
  * instead of read stale).
258
258
  */
259
259
  export function validateServiceImage(serviceName, image) {
@@ -12,14 +12,14 @@
12
12
  // is correct for every file that is IN that hash. It can be behind for the two
13
13
  // kinds of file the hash excludes: `spectest/tests/**` (excluded so a test-only
14
14
  // edit keeps the fast start) and anything a project lists in
15
- // `spectest/.envignore`.
15
+ // `spectest/.specignore`.
16
16
  //
17
17
  // Hence the rule below: a path under `spectest/` resolves against the app copy
18
18
  // first, because that copy is always current — this is what lets a fixture live
19
19
  // in `spectest/tests/fixtures/` and still be found after it was added. Any
20
20
  // other path resolves against /workspace, which the hash keeps current.
21
21
  //
22
- // The remaining hole is a file that `.envignore` excludes AND that sits outside
22
+ // The remaining hole is a file that `.specignore` excludes AND that sits outside
23
23
  // `spectest/`: /workspace holds whatever the cold start uploaded, so a later
24
24
  // edit is invisible. Nothing can repair those bytes at read time, so we refuse
25
25
  // to read them instead of returning stale content. The control plane writes the
@@ -31,8 +31,8 @@ export const WORKSPACE = process.env.SPECTEST_WORKSPACE ?? "/workspace";
31
31
  /** The app dir; the user's `spectest/` sits directly under it. */
32
32
  export const APP_DIR = process.env.SPECTEST_APP_DIR ?? "/opt/spectest/app";
33
33
  /** Paths (repo-relative) the warm-template hash skipped because of
34
- * `spectest/.envignore`. Written by the control plane at env start; absent
35
- * when the project ships no `.envignore`. */
34
+ * `spectest/.specignore`. Written by the control plane at env start; absent
35
+ * when the project ships no `.specignore`. */
36
36
  const ENV_IGNORED_FILE = "/run/spectest-env-ignored.json";
37
37
  let _envIgnored;
38
38
  function envIgnored() {
@@ -44,7 +44,7 @@ function envIgnored() {
44
44
  paths = raw.paths ?? [];
45
45
  }
46
46
  catch {
47
- /* No file (no .envignore, or an older env) — nothing to refuse. */
47
+ /* No file (no .specignore, or an older env) — nothing to refuse. */
48
48
  }
49
49
  _envIgnored = new Set(paths);
50
50
  return _envIgnored;
@@ -90,7 +90,7 @@ export function resolveProjectPath(p, opts = {}) {
90
90
  return app;
91
91
  }
92
92
  if (envIgnored().has(rel)) {
93
- throw new Error(`project file "${p}" is excluded by spectest/.envignore, so the copy in the VM ` +
93
+ throw new Error(`project file "${p}" is excluded by the environment cache ignore rules, so the copy in the VM ` +
94
94
  `is whatever a cold start uploaded and can be out of date. Move it under ` +
95
95
  `spectest/tests/ (still cache-free, and always re-uploaded), or drop the pattern.`);
96
96
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@specific.dev/spectest",
3
- "version": "0.88.0",
3
+ "version": "0.88.1",
4
4
  "description": "Spectest SDK for defining test environments in TypeScript.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,13 +17,11 @@
17
17
  // origin. Real providers spread them over several hosts. We merge the
18
18
  // real URLs in.
19
19
  // * JWKS — we serve one RSA public key, for every provider.
20
- // * id_token — we re-issue it: same claims, RS256, our key, and a
21
- // corrected `iss` where the emulator cannot know it. This is one step
22
- // that fixes three faults. The Google emulator signs HS256 and serves
23
- // an empty JWKS, so no client can verify its token. The Microsoft one
24
- // cannot put the tenant in the issuer, because it does not know it.
25
- // And any future provider whose issuer does not match its base URL is
26
- // covered in advance. No signature check is needed on the way in: the
20
+ // * id_token — we re-issue it: same claims, RS256, our key, and an
21
+ // explicit `iss` where the provider declares one. The Google emulator
22
+ // signs HS256 and serves an empty JWKS, so no client can verify its
23
+ // token. Providers whose issuer differs from their base URL can
24
+ // declare it explicitly. No signature check is needed on the way in: the
27
25
  // token comes from an in-process function call, not from the network.
28
26
  // * the account picker page — each button gets a `data-testid`, so a
29
27
  // browser test has a stable locator.
@@ -26,11 +26,10 @@
26
26
  // `entry.mjs` routes by Host header, applies the per-provider path table,
27
27
  // and corrects the discovery document. It also re-issues every id_token
28
28
  // with one RSA key it generates at boot, and serves that key at the
29
- // provider's real JWKS URL. That single step covers three separate faults:
29
+ // provider's real JWKS URL. This normalizes signing and issuer handling:
30
30
  // the Google emulator signs HS256 and publishes an empty JWKS (so nothing
31
- // can verify its token), the Microsoft one cannot put the tenant in its
32
- // issuer, and any future provider whose issuer is not its base URL is
33
- // handled in advance.
31
+ // can verify its token), and providers whose issuer differs from their
32
+ // base URL can declare it explicitly.
34
33
  //
35
34
  // The Dockerfile is deliberately CONSTANT — every provider package is
36
35
  // installed whether or not this component is the one using it. Two reasons:
@@ -13,23 +13,22 @@ import { emulatorService, oauthClients, resolveUsers } from "./emulate/service.j
13
13
  export interface MicrosoftOptions extends ProviderOptions {
14
14
  /**
15
15
  * Directory (tenant) the app signs in against — whatever its authority
16
- * URL uses. It appears in the issuer and in every endpoint path, so a
17
- * client that checks the issuer sees what it expects. Default `"common"`.
16
+ * URL uses. Sets the seeded users' and client's tenant as well as the
17
+ * issuer and endpoint paths. Default `"common"`.
18
18
  */
19
19
  tenantId?: string;
20
20
  }
21
21
 
22
22
  function spec(users: ProviderUser[], opts: MicrosoftOptions): ProviderSpec {
23
- const authority = `https://login.microsoftonline.com/${opts.tenantId || "common"}`;
23
+ const tenantId = opts.tenantId || "common";
24
+ const authority = `https://login.microsoftonline.com/${tenantId}`;
24
25
  return {
25
26
  name: "microsoft",
26
27
  module: "@emulators/microsoft",
27
28
  pluginExport: "microsoftPlugin",
28
29
  baseUrl: "https://login.microsoftonline.com",
29
30
  hosts: ["login.microsoftonline.com", "graph.microsoft.com"],
30
- // The emulator has no tenant, so its issuer is the bare host. A client
31
- // that checks the issuer against the authority it configured would
32
- // reject that; the re-issue step corrects it.
31
+ // Keep the re-issued token aligned with discovery and the seeded tenant.
33
32
  issuer: `${authority}/v2.0`,
34
33
  discovery: {
35
34
  issuer: `${authority}/v2.0`,
@@ -52,8 +51,8 @@ function spec(users: ProviderUser[], opts: MicrosoftOptions): ProviderSpec {
52
51
  scopes: ["openid", "email", "profile", "User.Read"],
53
52
  },
54
53
  seed: {
55
- users: users.map((u) => ({ email: u.email, name: u.name })),
56
- ...oauthClients(opts.client),
54
+ users: users.map((u) => ({ email: u.email, name: u.name, tenant_id: tenantId })),
55
+ ...oauthClients(opts.client, { tenant_id: tenantId }),
57
56
  },
58
57
  };
59
58
  }
package/src/index.ts CHANGED
@@ -1254,7 +1254,7 @@ const BUILD_TARGET_RE = /^[^\s]+$/;
1254
1254
  * Runs in `defineEnvironment`, and in the daemon's runtime `startService`
1255
1255
  * twin, inside the VM, where the repo sits at the project root. Existence
1256
1256
  * checks go through the project-file resolver, so the same rules as
1257
- * `ctx.readProjectFile` apply (a `spectest/.envignore`d path is refused
1257
+ * `ctx.readProjectFile` apply (a `spectest/.specignore`d path is refused
1258
1258
  * instead of read stale).
1259
1259
  */
1260
1260
  export function validateServiceImage(serviceName: string, image: ServiceImage): ServiceImage {
@@ -12,14 +12,14 @@
12
12
  // is correct for every file that is IN that hash. It can be behind for the two
13
13
  // kinds of file the hash excludes: `spectest/tests/**` (excluded so a test-only
14
14
  // edit keeps the fast start) and anything a project lists in
15
- // `spectest/.envignore`.
15
+ // `spectest/.specignore`.
16
16
  //
17
17
  // Hence the rule below: a path under `spectest/` resolves against the app copy
18
18
  // first, because that copy is always current — this is what lets a fixture live
19
19
  // in `spectest/tests/fixtures/` and still be found after it was added. Any
20
20
  // other path resolves against /workspace, which the hash keeps current.
21
21
  //
22
- // The remaining hole is a file that `.envignore` excludes AND that sits outside
22
+ // The remaining hole is a file that `.specignore` excludes AND that sits outside
23
23
  // `spectest/`: /workspace holds whatever the cold start uploaded, so a later
24
24
  // edit is invisible. Nothing can repair those bytes at read time, so we refuse
25
25
  // to read them instead of returning stale content. The control plane writes the
@@ -34,8 +34,8 @@ export const WORKSPACE = process.env.SPECTEST_WORKSPACE ?? "/workspace";
34
34
  export const APP_DIR = process.env.SPECTEST_APP_DIR ?? "/opt/spectest/app";
35
35
 
36
36
  /** Paths (repo-relative) the warm-template hash skipped because of
37
- * `spectest/.envignore`. Written by the control plane at env start; absent
38
- * when the project ships no `.envignore`. */
37
+ * `spectest/.specignore`. Written by the control plane at env start; absent
38
+ * when the project ships no `.specignore`. */
39
39
  const ENV_IGNORED_FILE = "/run/spectest-env-ignored.json";
40
40
 
41
41
  let _envIgnored: Set<string> | undefined;
@@ -46,7 +46,7 @@ function envIgnored(): Set<string> {
46
46
  const raw = JSON.parse(readFileSync(ENV_IGNORED_FILE, "utf8")) as { paths?: string[] };
47
47
  paths = raw.paths ?? [];
48
48
  } catch {
49
- /* No file (no .envignore, or an older env) — nothing to refuse. */
49
+ /* No file (no .specignore, or an older env) — nothing to refuse. */
50
50
  }
51
51
  _envIgnored = new Set(paths);
52
52
  return _envIgnored;
@@ -94,7 +94,7 @@ export function resolveProjectPath(p: string, opts: { record?: boolean } = {}):
94
94
  }
95
95
  if (envIgnored().has(rel)) {
96
96
  throw new Error(
97
- `project file "${p}" is excluded by spectest/.envignore, so the copy in the VM ` +
97
+ `project file "${p}" is excluded by the environment cache ignore rules, so the copy in the VM ` +
98
98
  `is whatever a cold start uploaded and can be out of date. Move it under ` +
99
99
  `spectest/tests/ (still cache-free, and always re-uploaded), or drop the pattern.`,
100
100
  );