@things-factory/auth-ui 10.1.40 → 10.1.44

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.
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Taking the invitation token out of the link, and out of the address bar. (Pure rule.)
2
+ * Taking the token out of a mailed link, and out of the address bar. (Pure rule.)
3
3
  *
4
4
  * ## Why the token is in the fragment
5
5
  *
6
6
  * A browser does not send what follows `#`, so a link shaped `…/accept-invitation#<token>`
7
- * reaches the server as `/accept-invitation` and nothing else. The token never appears in an
7
+ * reaches the server as `/accept-invitation` and nothing else — and the same for
8
+ * `/reset-password`, `/unlock-user` and `/verify`. The token never appears in an
8
9
  * access log, a proxy log, or a `Referer` sent onward to some other site.
9
10
  *
10
11
  * That leaves the places the browser itself keeps it: the address bar and the history entry.
@@ -19,10 +20,10 @@
19
20
  * anchor — to the server as a credential attempt. It answers empty instead, and the page shows
20
21
  * the box where the code from the mail can be pasted.
21
22
  */
22
- export type InvitationLink = {
23
+ export type LinkToken = {
23
24
  /** The token, or an empty string when the link carries none this rule will read. */
24
25
  token: string;
25
26
  /** The same URL without its fragment, for the caller to replace the address with. */
26
27
  clean: string;
27
28
  };
28
- export declare function readInvitationToken(href: string): InvitationLink;
29
+ export declare function readLinkToken(href: string): LinkToken;
@@ -1,10 +1,11 @@
1
1
  /**
2
- * Taking the invitation token out of the link, and out of the address bar. (Pure rule.)
2
+ * Taking the token out of a mailed link, and out of the address bar. (Pure rule.)
3
3
  *
4
4
  * ## Why the token is in the fragment
5
5
  *
6
6
  * A browser does not send what follows `#`, so a link shaped `…/accept-invitation#<token>`
7
- * reaches the server as `/accept-invitation` and nothing else. The token never appears in an
7
+ * reaches the server as `/accept-invitation` and nothing else — and the same for
8
+ * `/reset-password`, `/unlock-user` and `/verify`. The token never appears in an
8
9
  * access log, a proxy log, or a `Referer` sent onward to some other site.
9
10
  *
10
11
  * That leaves the places the browser itself keeps it: the address bar and the history entry.
@@ -19,9 +20,9 @@
19
20
  * anchor — to the server as a credential attempt. It answers empty instead, and the page shows
20
21
  * the box where the code from the mail can be pasted.
21
22
  */
22
- /** 16 bytes as hex, which is what `makeInvitationToken` produces. */
23
+ /** 16 bytes as hex, which is what `makeInvitationToken` and `makeVerificationToken` produce. */
23
24
  const TOKEN = /^[0-9a-f]{32}$/;
24
- export function readInvitationToken(href) {
25
+ export function readLinkToken(href) {
25
26
  if (!href) {
26
27
  return { token: '', clean: '' };
27
28
  }
@@ -37,4 +38,4 @@ export function readInvitationToken(href) {
37
38
  const raw = decodeURIComponent(href.slice(hash + 1)).replace(/^invite=/, '');
38
39
  return { token: TOKEN.test(raw) ? raw : '', clean };
39
40
  }
40
- //# sourceMappingURL=invitation-token.js.map
41
+ //# sourceMappingURL=link-token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-token.js","sourceRoot":"","sources":["../../client/utils/link-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,gGAAgG;AAChG,MAAM,KAAK,GAAG,gBAAgB,CAAA;AAS9B,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;IACjC,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAE9B,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACb,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IACnC,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAEjC;;;OAGG;IACH,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;IAE5E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAA;AACrD,CAAC","sourcesContent":["/**\n * Taking the token out of a mailed link, and out of the address bar. (Pure rule.)\n *\n * ## Why the token is in the fragment\n *\n * A browser does not send what follows `#`, so a link shaped `…/accept-invitation#<token>`\n * reaches the server as `/accept-invitation` and nothing else — and the same for\n * `/reset-password`, `/unlock-user` and `/verify`. The token never appears in an\n * access log, a proxy log, or a `Referer` sent onward to some other site.\n *\n * That leaves the places the browser itself keeps it: the address bar and the history entry.\n * Both are cleared by replacing the URL with the one this returns as `clean`, which is why the\n * two answers come back together — reading the token and removing it are one act, and a caller\n * that did the first without the second would leave it on screen.\n *\n * ## What it refuses to read\n *\n * A token is hex from `crypto.randomBytes(16)`. Anything else in the fragment is not one, and\n * taking it anyway would send whatever a link happened to carry — a tracking fragment, a page\n * anchor — to the server as a credential attempt. It answers empty instead, and the page shows\n * the box where the code from the mail can be pasted.\n */\n\n/** 16 bytes as hex, which is what `makeInvitationToken` and `makeVerificationToken` produce. */\nconst TOKEN = /^[0-9a-f]{32}$/\n\nexport type LinkToken = {\n /** The token, or an empty string when the link carries none this rule will read. */\n token: string\n /** The same URL without its fragment, for the caller to replace the address with. */\n clean: string\n}\n\nexport function readLinkToken(href: string): LinkToken {\n if (!href) {\n return { token: '', clean: '' }\n }\n\n const hash = href.indexOf('#')\n\n if (hash < 0) {\n return { token: '', clean: href }\n }\n\n const clean = href.slice(0, hash)\n\n /*\n * `#invite=<token>` as well as a bare `#<token>`. Both shapes turn up in mail clients that\n * rewrite links, and neither costs anything to accept.\n */\n const raw = decodeURIComponent(href.slice(hash + 1)).replace(/^invite=/, '')\n\n return { token: TOKEN.test(raw) ? raw : '', clean }\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { readInvitationToken } from './invitation-token';
1
+ import { readLinkToken } from './link-token';
2
2
  /**
3
3
  * Reading the invitation token out of the link, and handing back the link without it.
4
4
  *
@@ -10,51 +10,51 @@ const TOKEN = '0123456789abcdef0123456789abcdef';
10
10
  const PAGE = 'https://acme.example.com/accept-invitation';
11
11
  describe('a link with a token in it', () => {
12
12
  it('reads a bare fragment', () => {
13
- expect(readInvitationToken(`${PAGE}#${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE });
13
+ expect(readLinkToken(`${PAGE}#${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE });
14
14
  });
15
15
  it('reads the named form some mail clients produce', () => {
16
- expect(readInvitationToken(`${PAGE}#invite=${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE });
16
+ expect(readLinkToken(`${PAGE}#invite=${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE });
17
17
  });
18
18
  it('reads one that arrived percent-encoded', () => {
19
- expect(readInvitationToken(`${PAGE}#invite%3D${TOKEN}`).token).toEqual(TOKEN);
19
+ expect(readLinkToken(`${PAGE}#invite%3D${TOKEN}`).token).toEqual(TOKEN);
20
20
  });
21
21
  it('hands back the address without the fragment', () => {
22
22
  /* ⚠ What the caller replaces the address with. Without it the token stays on screen and in
23
23
  this history entry, which is the half of the hiding the fragment does not do by itself. */
24
- expect(readInvitationToken(`${PAGE}?x=1#${TOKEN}`).clean).toEqual(`${PAGE}?x=1`);
24
+ expect(readLinkToken(`${PAGE}?x=1#${TOKEN}`).clean).toEqual(`${PAGE}?x=1`);
25
25
  });
26
26
  });
27
27
  describe('a link with nothing usable in it', () => {
28
28
  it('has no fragment at all', () => {
29
- expect(readInvitationToken(PAGE)).toEqual({ token: '', clean: PAGE });
29
+ expect(readLinkToken(PAGE)).toEqual({ token: '', clean: PAGE });
30
30
  });
31
31
  it('has an empty fragment', () => {
32
- expect(readInvitationToken(`${PAGE}#`)).toEqual({ token: '', clean: PAGE });
32
+ expect(readLinkToken(`${PAGE}#`)).toEqual({ token: '', clean: PAGE });
33
33
  });
34
34
  it('⚠ carries something that is not a token', () => {
35
35
  /*
36
36
  * ⚠ A page anchor, a tracking fragment, anything a link happened to pick up. Sending it as a
37
37
  * credential attempt would be this page volunteering whatever it was given.
38
38
  */
39
- expect(readInvitationToken(`${PAGE}#section-2`).token).toEqual('');
40
- expect(readInvitationToken(`${PAGE}#utm_source=mail`).token).toEqual('');
41
- expect(readInvitationToken(`${PAGE}#<script>`).token).toEqual('');
39
+ expect(readLinkToken(`${PAGE}#section-2`).token).toEqual('');
40
+ expect(readLinkToken(`${PAGE}#utm_source=mail`).token).toEqual('');
41
+ expect(readLinkToken(`${PAGE}#<script>`).token).toEqual('');
42
42
  });
43
43
  it('carries something token-shaped but the wrong length', () => {
44
- expect(readInvitationToken(`${PAGE}#0123456789abcdef`).token).toEqual('');
45
- expect(readInvitationToken(`${PAGE}#${TOKEN}ff`).token).toEqual('');
44
+ expect(readLinkToken(`${PAGE}#0123456789abcdef`).token).toEqual('');
45
+ expect(readLinkToken(`${PAGE}#${TOKEN}ff`).token).toEqual('');
46
46
  });
47
47
  it('carries hex in the wrong case', () => {
48
48
  /* `makeInvitationToken` produces lower-case hex. Anything else did not come from us. */
49
- expect(readInvitationToken(`${PAGE}#${TOKEN.toUpperCase()}`).token).toEqual('');
49
+ expect(readLinkToken(`${PAGE}#${TOKEN.toUpperCase()}`).token).toEqual('');
50
50
  });
51
51
  it('still hands back a clean address to replace with', () => {
52
52
  /* Even a fragment that is not ours should not be left sitting in the bar for this page. */
53
- expect(readInvitationToken(`${PAGE}#section-2`).clean).toEqual(PAGE);
53
+ expect(readLinkToken(`${PAGE}#section-2`).clean).toEqual(PAGE);
54
54
  });
55
55
  it('is given nothing at all', () => {
56
- expect(readInvitationToken('')).toEqual({ token: '', clean: '' });
57
- expect(readInvitationToken(undefined)).toEqual({ token: '', clean: '' });
56
+ expect(readLinkToken('')).toEqual({ token: '', clean: '' });
57
+ expect(readLinkToken(undefined)).toEqual({ token: '', clean: '' });
58
58
  });
59
59
  });
60
- //# sourceMappingURL=invitation-token.test.js.map
60
+ //# sourceMappingURL=link-token.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"link-token.test.js","sourceRoot":"","sources":["../../client/utils/link-token.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { readLinkToken } from './link-token'\n\n/**\n * Reading the invitation token out of the link, and handing back the link without it.\n *\n * The two answers travel together because removing the token is not optional: the fragment keeps\n * it out of server and proxy logs, and replacing the address is what keeps it out of the browser\n * history and off the screen. A caller that read it and forgot to replace would undo the point.\n */\n\nconst TOKEN = '0123456789abcdef0123456789abcdef'\nconst PAGE = 'https://acme.example.com/accept-invitation'\n\ndescribe('a link with a token in it', () => {\n it('reads a bare fragment', () => {\n expect(readLinkToken(`${PAGE}#${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE })\n })\n\n it('reads the named form some mail clients produce', () => {\n expect(readLinkToken(`${PAGE}#invite=${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE })\n })\n\n it('reads one that arrived percent-encoded', () => {\n expect(readLinkToken(`${PAGE}#invite%3D${TOKEN}`).token).toEqual(TOKEN)\n })\n\n it('hands back the address without the fragment', () => {\n /* ⚠ What the caller replaces the address with. Without it the token stays on screen and in\n this history entry, which is the half of the hiding the fragment does not do by itself. */\n expect(readLinkToken(`${PAGE}?x=1#${TOKEN}`).clean).toEqual(`${PAGE}?x=1`)\n })\n})\n\ndescribe('a link with nothing usable in it', () => {\n it('has no fragment at all', () => {\n expect(readLinkToken(PAGE)).toEqual({ token: '', clean: PAGE })\n })\n\n it('has an empty fragment', () => {\n expect(readLinkToken(`${PAGE}#`)).toEqual({ token: '', clean: PAGE })\n })\n\n it('⚠ carries something that is not a token', () => {\n /*\n * ⚠ A page anchor, a tracking fragment, anything a link happened to pick up. Sending it as a\n * credential attempt would be this page volunteering whatever it was given.\n */\n expect(readLinkToken(`${PAGE}#section-2`).token).toEqual('')\n expect(readLinkToken(`${PAGE}#utm_source=mail`).token).toEqual('')\n expect(readLinkToken(`${PAGE}#<script>`).token).toEqual('')\n })\n\n it('carries something token-shaped but the wrong length', () => {\n expect(readLinkToken(`${PAGE}#0123456789abcdef`).token).toEqual('')\n expect(readLinkToken(`${PAGE}#${TOKEN}ff`).token).toEqual('')\n })\n\n it('carries hex in the wrong case', () => {\n /* `makeInvitationToken` produces lower-case hex. Anything else did not come from us. */\n expect(readLinkToken(`${PAGE}#${TOKEN.toUpperCase()}`).token).toEqual('')\n })\n\n it('still hands back a clean address to replace with', () => {\n /* Even a fragment that is not ours should not be left sitting in the bar for this page. */\n expect(readLinkToken(`${PAGE}#section-2`).clean).toEqual(PAGE)\n })\n\n it('is given nothing at all', () => {\n expect(readLinkToken('')).toEqual({ token: '', clean: '' })\n expect(readLinkToken(undefined as any)).toEqual({ token: '', clean: '' })\n })\n})\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-ui",
3
- "version": "10.1.40",
3
+ "version": "10.1.44",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -38,11 +38,11 @@
38
38
  "@operato/p13n": "^10.0.0",
39
39
  "@operato/popup": "^10.15.0",
40
40
  "@operato/styles": "^10.0.0",
41
- "@things-factory/auth-base": "^10.1.40",
42
- "@things-factory/component-ui": "^10.1.40",
43
- "@things-factory/more-base": "^10.1.40",
41
+ "@things-factory/auth-base": "^10.1.44",
42
+ "@things-factory/component-ui": "^10.1.44",
43
+ "@things-factory/more-base": "^10.1.44",
44
44
  "base64url": "^3.0.1",
45
45
  "clipboard": "^2.0.6"
46
46
  },
47
- "gitHead": "51c5d804c14ad8fcc6d4707e6ef80c9deede2766"
47
+ "gitHead": "7c95c4e75e155e51c714409e865d96f317232c23"
48
48
  }
@@ -336,6 +336,7 @@
336
336
  "text.your account is not activated": "your account is not activated.",
337
337
  "text.your email address": "your email address",
338
338
  "title.accept invitation": "accept invitation",
339
+ "title.activate account": "activate account",
339
340
  "title.add domain owner": "add domain owner",
340
341
  "title.attribute model": "attribute model",
341
342
  "title.attribute-item list": "attribute-item list",
@@ -267,6 +267,7 @@
267
267
  "text.your account is not activated": "アカウントがアクティブ化されていません.",
268
268
  "text.your email address": "ユーザー メール",
269
269
  "title.accept invitation": "招待を受ける",
270
+ "title.activate account": "アカウント有効化",
270
271
  "title.attribute model": "属性モデル",
271
272
  "title.attribute-item list": "属性アイテム リスト",
272
273
  "title.attributes": "属性",
@@ -335,6 +335,7 @@
335
335
  "text.your account is not activated": "계정이 활성화되지 않았습니다.",
336
336
  "text.your email address": "사용자 이메일",
337
337
  "title.accept invitation": "초대 수락",
338
+ "title.activate account": "계정 활성화",
338
339
  "title.add domain owner": "$t(term.domain) 소유자 추가",
339
340
  "title.attribute model": "속성 모델",
340
341
  "title.attribute-item list": "속성 아이템 리스트",
@@ -268,6 +268,7 @@
268
268
  "text.your account is not activated": "akaun anda belum diaktifkan",
269
269
  "text.your email address": "alamat emel anda",
270
270
  "title.accept invitation": "terima jemputan",
271
+ "title.activate account": "aktifkan akaun",
271
272
  "title.attribute model": "model atribut",
272
273
  "title.attribute-item list": "senarai ciri-ciri",
273
274
  "title.attributes": "ciri-ciri",
@@ -268,6 +268,7 @@
268
268
  "text.your account is not activated": "您的账号还没验证",
269
269
  "text.your email address": "您的邮件地址",
270
270
  "title.accept invitation": "接受邀请",
271
+ "title.activate account": "激活帐户",
271
272
  "title.attribute model": "属性模型",
272
273
  "title.attribute-item list": "属性列表",
273
274
  "title.attributes": "属性",
@@ -1 +0,0 @@
1
- {"version":3,"file":"invitation-token.js","sourceRoot":"","sources":["../../client/utils/invitation-token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,qEAAqE;AACrE,MAAM,KAAK,GAAG,gBAAgB,CAAA;AAS9B,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IAAI,CAAC,IAAI,EAAE,CAAC;QACV,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;IACjC,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAE9B,IAAI,IAAI,GAAG,CAAC,EAAE,CAAC;QACb,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IACnC,CAAC;IAED,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IAEjC;;;OAGG;IACH,MAAM,GAAG,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAA;IAE5E,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAA;AACrD,CAAC","sourcesContent":["/**\n * Taking the invitation token out of the link, and out of the address bar. (Pure rule.)\n *\n * ## Why the token is in the fragment\n *\n * A browser does not send what follows `#`, so a link shaped `…/accept-invitation#<token>`\n * reaches the server as `/accept-invitation` and nothing else. The token never appears in an\n * access log, a proxy log, or a `Referer` sent onward to some other site.\n *\n * That leaves the places the browser itself keeps it: the address bar and the history entry.\n * Both are cleared by replacing the URL with the one this returns as `clean`, which is why the\n * two answers come back together — reading the token and removing it are one act, and a caller\n * that did the first without the second would leave it on screen.\n *\n * ## What it refuses to read\n *\n * A token is hex from `crypto.randomBytes(16)`. Anything else in the fragment is not one, and\n * taking it anyway would send whatever a link happened to carry — a tracking fragment, a page\n * anchor — to the server as a credential attempt. It answers empty instead, and the page shows\n * the box where the code from the mail can be pasted.\n */\n\n/** 16 bytes as hex, which is what `makeInvitationToken` produces. */\nconst TOKEN = /^[0-9a-f]{32}$/\n\nexport type InvitationLink = {\n /** The token, or an empty string when the link carries none this rule will read. */\n token: string\n /** The same URL without its fragment, for the caller to replace the address with. */\n clean: string\n}\n\nexport function readInvitationToken(href: string): InvitationLink {\n if (!href) {\n return { token: '', clean: '' }\n }\n\n const hash = href.indexOf('#')\n\n if (hash < 0) {\n return { token: '', clean: href }\n }\n\n const clean = href.slice(0, hash)\n\n /*\n * `#invite=<token>` as well as a bare `#<token>`. Both shapes turn up in mail clients that\n * rewrite links, and neither costs anything to accept.\n */\n const raw = decodeURIComponent(href.slice(hash + 1)).replace(/^invite=/, '')\n\n return { token: TOKEN.test(raw) ? raw : '', clean }\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"invitation-token.test.js","sourceRoot":"","sources":["../../client/utils/invitation-token.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { readInvitationToken } from './invitation-token'\n\n/**\n * Reading the invitation token out of the link, and handing back the link without it.\n *\n * The two answers travel together because removing the token is not optional: the fragment keeps\n * it out of server and proxy logs, and replacing the address is what keeps it out of the browser\n * history and off the screen. A caller that read it and forgot to replace would undo the point.\n */\n\nconst TOKEN = '0123456789abcdef0123456789abcdef'\nconst PAGE = 'https://acme.example.com/accept-invitation'\n\ndescribe('a link with a token in it', () => {\n it('reads a bare fragment', () => {\n expect(readInvitationToken(`${PAGE}#${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE })\n })\n\n it('reads the named form some mail clients produce', () => {\n expect(readInvitationToken(`${PAGE}#invite=${TOKEN}`)).toEqual({ token: TOKEN, clean: PAGE })\n })\n\n it('reads one that arrived percent-encoded', () => {\n expect(readInvitationToken(`${PAGE}#invite%3D${TOKEN}`).token).toEqual(TOKEN)\n })\n\n it('hands back the address without the fragment', () => {\n /* ⚠ What the caller replaces the address with. Without it the token stays on screen and in\n this history entry, which is the half of the hiding the fragment does not do by itself. */\n expect(readInvitationToken(`${PAGE}?x=1#${TOKEN}`).clean).toEqual(`${PAGE}?x=1`)\n })\n})\n\ndescribe('a link with nothing usable in it', () => {\n it('has no fragment at all', () => {\n expect(readInvitationToken(PAGE)).toEqual({ token: '', clean: PAGE })\n })\n\n it('has an empty fragment', () => {\n expect(readInvitationToken(`${PAGE}#`)).toEqual({ token: '', clean: PAGE })\n })\n\n it('⚠ carries something that is not a token', () => {\n /*\n * ⚠ A page anchor, a tracking fragment, anything a link happened to pick up. Sending it as a\n * credential attempt would be this page volunteering whatever it was given.\n */\n expect(readInvitationToken(`${PAGE}#section-2`).token).toEqual('')\n expect(readInvitationToken(`${PAGE}#utm_source=mail`).token).toEqual('')\n expect(readInvitationToken(`${PAGE}#<script>`).token).toEqual('')\n })\n\n it('carries something token-shaped but the wrong length', () => {\n expect(readInvitationToken(`${PAGE}#0123456789abcdef`).token).toEqual('')\n expect(readInvitationToken(`${PAGE}#${TOKEN}ff`).token).toEqual('')\n })\n\n it('carries hex in the wrong case', () => {\n /* `makeInvitationToken` produces lower-case hex. Anything else did not come from us. */\n expect(readInvitationToken(`${PAGE}#${TOKEN.toUpperCase()}`).token).toEqual('')\n })\n\n it('still hands back a clean address to replace with', () => {\n /* Even a fragment that is not ours should not be left sitting in the bar for this page. */\n expect(readInvitationToken(`${PAGE}#section-2`).clean).toEqual(PAGE)\n })\n\n it('is given nothing at all', () => {\n expect(readInvitationToken('')).toEqual({ token: '', clean: '' })\n expect(readInvitationToken(undefined as any)).toEqual({ token: '', clean: '' })\n })\n})\n"]}