@things-factory/auth-base 10.1.34 → 10.1.38

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 (37) hide show
  1. package/dist-client/auth.d.ts +8 -0
  2. package/dist-client/auth.js +26 -0
  3. package/dist-client/auth.js.map +1 -1
  4. package/dist-client/domain-required.d.ts +54 -0
  5. package/dist-client/domain-required.js +91 -0
  6. package/dist-client/domain-required.js.map +1 -0
  7. package/dist-client/tsconfig.tsbuildinfo +1 -1
  8. package/dist-server/controllers/invitation.d.ts +26 -0
  9. package/dist-server/controllers/invitation.js +12 -1
  10. package/dist-server/controllers/invitation.js.map +1 -1
  11. package/dist-server/service/app-binding/app-binding-query.d.ts +17 -0
  12. package/dist-server/service/app-binding/app-binding-query.js +32 -2
  13. package/dist-server/service/app-binding/app-binding-query.js.map +1 -1
  14. package/dist-server/service/index.d.ts +7 -2
  15. package/dist-server/service/index.js +3 -0
  16. package/dist-server/service/index.js.map +1 -1
  17. package/dist-server/service/open-fields.d.ts +9 -0
  18. package/dist-server/service/open-fields.js +24 -0
  19. package/dist-server/service/open-fields.js.map +1 -0
  20. package/dist-server/service/privilege/open-field-rule.d.ts +93 -0
  21. package/dist-server/service/privilege/open-field-rule.js +81 -0
  22. package/dist-server/service/privilege/open-field-rule.js.map +1 -0
  23. package/dist-server/service/privilege/privilege-directive.d.ts +9 -1
  24. package/dist-server/service/privilege/privilege-directive.js +177 -95
  25. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  26. package/dist-server/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +4 -4
  28. package/tests/app-binding-db.test.ts +48 -0
  29. package/tests/domain-required.test.ts +127 -0
  30. package/tests/invitation-route.test.ts +97 -0
  31. package/tests/open-doors.test.ts +13 -21
  32. package/tests/open-fields-enforcement.test.ts +215 -0
  33. package/translations/en.json +0 -1
  34. package/translations/ja.json +5 -6
  35. package/translations/ko.json +6 -7
  36. package/translations/ms.json +0 -1
  37. package/translations/zh.json +5 -6
@@ -14,6 +14,9 @@ declare class ClientAuth {
14
14
  private listeners;
15
15
  private authRequiredEventListener;
16
16
  private activateRequiredEventListener;
17
+ private domainRequiredEventListener;
18
+ /** 한 화면이 질의를 여럿 내므로 403 도 여럿 온다. 보내는 것은 한 번이다. */
19
+ private domainRedirecting;
17
20
  private _credential;
18
21
  private accessToken?;
19
22
  private domains;
@@ -28,6 +31,11 @@ declare class ClientAuth {
28
31
  private onAuthError;
29
32
  private onPasswordChanged;
30
33
  private onAuthRequired;
34
+ /**
35
+ * GraphQL 이 403 을 받았다 — 이 화면은 사업장이 안 정해진 채로 떴다. 체크인으로 보낸다.
36
+ * 어디로 보내고 언제 안 보내는지는 `domain-required.ts` 가 정한다.
37
+ */
38
+ private onDomainRequired;
31
39
  private onActivateRequired;
32
40
  get credential(): any;
33
41
  route(path: any, redirected: any): void;
@@ -6,6 +6,7 @@
6
6
  changepassPath = '/auth/change-pass'
7
7
  deleteUserPath = '/auth/delete-user'
8
8
  */
9
+ import { decideDomainRedirect, DOMAIN_REDIRECT_KEY } from './domain-required.js';
9
10
  const HEADER_JSON = {
10
11
  'Content-Type': 'application/json',
11
12
  Accept: 'application/json'
@@ -22,10 +23,14 @@ class ClientAuth {
22
23
  };
23
24
  this.authRequiredEventListener = this.onAuthRequired.bind(this);
24
25
  this.activateRequiredEventListener = this.onActivateRequired.bind(this);
26
+ this.domainRequiredEventListener = this.onDomainRequired.bind(this);
27
+ /** 한 화면이 질의를 여럿 내므로 403 도 여럿 온다. 보내는 것은 한 번이다. */
28
+ this.domainRedirecting = false;
25
29
  this.domains = [];
26
30
  this.languages = [];
27
31
  document.addEventListener('auth-required', this.authRequiredEventListener);
28
32
  document.addEventListener('activate-required', this.activateRequiredEventListener);
33
+ document.addEventListener('domain-required', this.domainRequiredEventListener);
29
34
  }
30
35
  on(event, handler) {
31
36
  var listeners = this.listeners[event];
@@ -56,6 +61,7 @@ class ClientAuth {
56
61
  dispose() {
57
62
  document.removeEventListener('auth-required', this.authRequiredEventListener);
58
63
  document.removeEventListener('activate-required', this.activateRequiredEventListener);
64
+ document.removeEventListener('domain-required', this.domainRequiredEventListener);
59
65
  this.listeners = {
60
66
  profile: [],
61
67
  signin: [],
@@ -101,6 +107,26 @@ class ClientAuth {
101
107
  url.searchParams.append('redirect_to', window.location.href);
102
108
  window.location.href = url.href;
103
109
  }
110
+ /**
111
+ * GraphQL 이 403 을 받았다 — 이 화면은 사업장이 안 정해진 채로 떴다. 체크인으로 보낸다.
112
+ * 어디로 보내고 언제 안 보내는지는 `domain-required.ts` 가 정한다.
113
+ */
114
+ onDomainRequired() {
115
+ if (this.domainRedirecting)
116
+ return;
117
+ /* 보낼지와 도장은 domain-required.ts 가 정한다 — 도장을 못 적는 브라우저에서는 보내지 않는다. */
118
+ const target = decideDomainRedirect(window.location.href, {
119
+ read: () => sessionStorage.getItem(DOMAIN_REDIRECT_KEY),
120
+ write: value => sessionStorage.setItem(DOMAIN_REDIRECT_KEY, value)
121
+ });
122
+ if (!target) {
123
+ console.warn('domain required - not sending to check-in (just came back, or cannot remember that we did)');
124
+ return;
125
+ }
126
+ this.domainRedirecting = true;
127
+ console.warn('domain required');
128
+ window.location.href = target;
129
+ }
104
130
  onActivateRequired(e) {
105
131
  console.warn('activate required');
106
132
  var params = new URLSearchParams();
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../client/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;EAOE;AAEF,MAAM,WAAW,GAAG;IAClB,cAAc,EAAE,kBAAkB;IAClC,MAAM,EAAE,kBAAkB;CAC3B,CAAA;AAWD,MAAM,UAAU;IA0Bd;QAzBQ,cAAS,GAOb;YACF,OAAO,EAAE,EAAE;YACX,OAAO,EAAE,EAAE;YACX,MAAM,EAAE,EAAE;YACV,UAAU,EAAE,EAAE;YACd,cAAc,EAAE,EAAE;YAClB,KAAK,EAAE,EAAE;SACV,CAAA;QAEO,8BAAyB,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC1D,kCAA6B,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAIlE,YAAO,GAAU,EAAE,CAAA;QAEnB,cAAS,GAAwC,EAAE,CAAA;QAGzD,QAAQ,CAAC,gBAAgB,CAAC,eAAe,EAAE,IAAI,CAAC,yBAAyB,CAAC,CAAA;QAC1E,QAAQ,CAAC,gBAAgB,CAAC,mBAAmB,EAAE,IAAI,CAAC,6BAA6B,CAAC,CAAA;IACpF,CAAC;IAED,EAAE,CAAC,KAAoB,EAAE,OAA4C;QACnE,IAAI,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QACrC,IAAI,SAAS,EAAE,CAAC;YACd,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACzB,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,KAAK,CAAC,CAAA;QACrC,CAAC;QAED,IAAI,KAAK,IAAI,SAAS,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAC3C;;;cAGE;YACF,OAAO,CAAC,EAAE,UAAU,EAAE,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAA;QAClH,CAAC;IACH,CAAC;IAED,GAAG,CAAC,KAAoB,EAAE,OAA4C;QACpE,IAAI,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QACrC,IAAI,SAAS,EAAE,CAAC;YACd,IAAI,GAAG,GAAG,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;YACpC,GAAG,IAAI,CAAC,IAAI,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;QACtC,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,KAAK,CAAC,CAAA;QACrC,CAAC;IACH,CAAC;IAED,OAAO;QACL,QAAQ,CAAC,mBAAmB,CAAC,eAAe,EAAE,IAAI,CAAC,yBAAyB,CAAC,CAAA;QAC7E,QAAQ,CAAC,mBAAmB,CAAC,mBAAmB,EAAE,IAAI,CAAC,6BAA6B,CAAC,CAAA;QAErF,IAAI,CAAC,SAAS,GAAG;YACf,OAAO,EAAE,EAAE;YACX,MAAM,EAAE,EAAE;YACV,OAAO,EAAE,EAAE;YACX,UAAU,EAAE,EAAE;YACd,cAAc,EAAE,EAAE;YAClB,KAAK,EAAE,EAAE;SACV,CAAA;IACH,CAAC;IAEO,gBAAgB,CAAC,EAAE,UAAU,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE;QAC9E,IAAI,CAAC,WAAW,GAAG,UAAU,CAAA;QAC7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAE1B,IAAI,WAAW,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACrC;;;cAGE;YACF,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;YAC9B,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;QAChG,CAAC;QACD,WAAW,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC,CAAA;QAC/C,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAA;IAChG,CAAC;IAEO,KAAK,CAAC,YAAY;QACxB,KAAK,IAAI,YAAY,IAAI,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC;YACnD,MAAM,YAAY,EAAE,CAAA;QACtB,CAAC;IACH,CAAC;IAEO,WAAW,CAAC,KAAK;QACvB,oCAAoC;QACpC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAA;IAC1D,CAAC;IAEO,iBAAiB,CAAC,MAAM;QAC9B,4CAA4C;QAC5C,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAA;IACpE,CAAC;IAEO,cAAc,CAAC,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAA;QACvC,IAAI,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QACvC,GAAG,CAAC,QAAQ,GAAG,cAAc,CAAA;QAC7B,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;QAE5D,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAA;IACjC,CAAC;IAEO,kBAAkB,CAAC,CAAC;QAC1B,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAA;QACjC,IAAI,MAAM,GAAG,IAAI,eAAe,EAAE,CAAA;QAClC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,CAAA;QAE/B,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,kBAAkB,MAAM,EAAE,CAAC,CAAA;IACrD,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,WAAW,CAAA;IACzB,CAAC;IAED,KAAK,CAAC,IAAI,EAAE,UAAU;QACpB,yCAAyC;QACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;QAChC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,QAAQ,GAAG,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAA;QAC1E,MAAM,IAAI,GAAG,GAAG,MAAM,GAAG,IAAI,EAAE,CAAA;QAE/B,IAAI,QAAQ,CAAC,QAAQ,KAAK,IAAI;YAAE,OAAM;QAEtC,yCAAyC;QACzC,qCAAqC;QACrC,iEAAiE;QACjE,qDAAqD;QACrD,kDAAkD;QAClD,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAA;QAClD,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,CAAA;QAEtC,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAA;IACvB,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,SAAS;QAC3B,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,sBAAsB,EAAE;YACnD,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,SAAS;YACtB,OAAO,EAAE,WAAW;YACpB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;SAChC,CAAC,CAAA;QAEF,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QACrC,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;YAChB,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;YACtB,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,kCAAkC;YACpC,CAAC;YAED,OAAO,OAAO,CAAA;QAChB,CAAC;QAED,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAA;IAC1B,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,cAAc,CAAC,SAAS;QAC5B,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,EAAE;gBAChD,MAAM,EAAE,MAAM;gBACd,WAAW,EAAE,SAAS;gBACtB,OAAO,EAAE,WAAW;gBACpB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;aAChC,CAAC,CAAA;YAEF,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;YACrC,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,IAAI,CAAC,iBAAiB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;gBACnC,OAAO,IAAI,CAAA;YACb,CAAC;YAED,IAAI,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;YAC7B,OAAO,KAAK,CAAA;QACd,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAA;YACnB,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,MAAM;QACrB,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,mBAAmB,EAAE;YAChD,MAAM,EAAE,MAAM;YACd,WAAW,EAAE,SAAS;YACtB,OAAO,EAAE,WAAW;YACpB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;SAC7B,CAAC,CAAA;QAEF,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;QACrC,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;YAChB,OAAO,OAAO,CAAA;QAChB,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAA;QAC1B,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,IAAI,CAAC;YACH,IAAI,YAAY,GAAG,IAAI,eAAe,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;YACvD,IAAI,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACrC,IAAI,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAA;YAErD,IAAI,KAAK,EAAE,CAAC;gBACV,OAAO,CAAC,aAAa,GAAG,UAAU,KAAK,EAAE,CAAA;YAC3C,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,eAAe,EAAE;gBAC5C,MAAM,EAAE,KAAK;gBACb,WAAW,EAAE,SAAS;gBACtB,OAAO;aACR,CAAC,CAAA;YAEF,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;gBAChB,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC;oBACxB,QAAQ,CAAC,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAA;oBAC5B,OAAM;gBACR,CAAC;gBAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;gBAElC,IAAI,CAAC,gBAAgB,CAAC;oBACpB,UAAU,EAAE,IAAI,CAAC,IAAI;oBACrB,WAAW,EAAE,IAAI,CAAC,KAAK;oBACvB,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,SAAS,EAAE,IAAI,CAAC,SAAS;iBAC1B,CAAC,CAAA;gBAEF,OAAM;YACR,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,YAAY,EAAE,CAAA;QAEzB,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,eAAe,CAAA;IACxC,CAAC;CACF;AAED,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,UAAU,EAAE,CAAA","sourcesContent":["/* [ AUTH PATH ]\n signinPath = '/auth/signin'\n signoutPath = '/auth/signout'\n profilePath = '/auth/profile'\n updateProfilePath = '/auth/update-profile'\n changepassPath = '/auth/change-pass'\n deleteUserPath = '/auth/delete-user'\n*/\n\nconst HEADER_JSON = {\n 'Content-Type': 'application/json',\n Accept: 'application/json'\n}\ntype AuthEventName = 'profile' | 'signin' | 'signout' | 'presignout' | 'passwordchange' | 'error'\ntype AuthEventHandler = (e?: {\n accessToken?: string\n credential?: string\n domains: any[]\n domain: any\n languages?: { code: string; display: string }[]\n}) => void\ntype AuthErrorHandler = (err: any) => void\n\nclass ClientAuth {\n private listeners: {\n profile: AuthEventHandler[]\n signout: AuthEventHandler[]\n signin: AuthEventHandler[]\n presignout: AuthEventHandler[]\n passwordchange: AuthEventHandler[]\n error: AuthErrorHandler[]\n } = {\n profile: [],\n signout: [],\n signin: [],\n presignout: [],\n passwordchange: [],\n error: []\n }\n\n private authRequiredEventListener = this.onAuthRequired.bind(this)\n private activateRequiredEventListener = this.onActivateRequired.bind(this)\n\n private _credential: any\n private accessToken?: string\n private domains: any[] = []\n private domain: any\n private languages: { code: string; display: string }[] = []\n\n constructor() {\n document.addEventListener('auth-required', this.authRequiredEventListener)\n document.addEventListener('activate-required', this.activateRequiredEventListener)\n }\n\n on(event: AuthEventName, handler: AuthEventHandler | AuthErrorHandler) {\n var listeners = this.listeners[event]\n if (listeners) {\n listeners.push(handler)\n } else {\n console.log('unknown event', event)\n }\n\n if (event == 'profile' && this._credential) {\n /* \n 특별히 event 가 profile 인 경우에는 이미 fetch된 credential이 있다면, 콜백을 해준다. \n 시스템 bootstrap에서 profile 이벤트가 사용되는 경우가 많은데, profile도 매우 초기에 fetch 되므로 레이스컨디션이 발생할 수 있기 때문에, 확실하게 event 콜백을 보장하기 위해서이다.\n */\n handler({ credential: this._credential, domains: this.domains, domain: this.domain, languages: this.languages })\n }\n }\n\n off(event: AuthEventName, handler: AuthEventHandler | AuthErrorHandler) {\n var listeners = this.listeners[event]\n if (listeners) {\n let idx = listeners.indexOf(handler)\n idx >= 0 && listeners.splice(idx, 1)\n } else {\n console.log('unknown event', event)\n }\n }\n\n dispose() {\n document.removeEventListener('auth-required', this.authRequiredEventListener)\n document.removeEventListener('activate-required', this.activateRequiredEventListener)\n\n this.listeners = {\n profile: [],\n signin: [],\n signout: [],\n presignout: [],\n passwordchange: [],\n error: []\n }\n }\n\n private onProfileFetched({ credential, accessToken, domains, domain, languages }) {\n this._credential = credential\n this.domains = domains\n this.domain = domain\n this.languages = languages\n\n if (accessToken && !this.accessToken) {\n /*\n 기존에 세션을 가지거나, 액세스토큰으로 인증된 경우,\n 이 경우는 signin 이벤트리스너들을 호출해서 authenticated 상태로 되도록 유도한다.\n */\n this.accessToken = accessToken\n this.listeners.signin.forEach(handler => handler({ accessToken, domains, domain, languages }))\n }\n accessToken && (this.accessToken = accessToken)\n this.listeners.profile.forEach(handler => handler({ credential, domains, domain, languages }))\n }\n\n private async onPreSignout() {\n for (let onpresignout of this.listeners.presignout) {\n await onpresignout()\n }\n }\n\n private onAuthError(error) {\n /* signin, signup 과정에서 에러가 발생한 경우 */\n this.listeners?.error.forEach(handler => handler(error))\n }\n\n private onPasswordChanged(result) {\n //event is passwordchange, handler is result\n this.listeners?.passwordchange.forEach(handler => handler(result))\n }\n\n private onAuthRequired(e) {\n console.warn('authentication required')\n let url = new URL(window.location.href)\n url.pathname = '/auth/signin'\n url.searchParams.append('redirect_to', window.location.href)\n\n window.location.href = url.href\n }\n\n private onActivateRequired(e) {\n console.warn('activate required')\n var params = new URLSearchParams()\n params.append('email', e.email)\n\n window.location.replace(`/auth/activate?${params}`)\n }\n\n get credential() {\n return this._credential\n }\n\n route(path, redirected) {\n /* history에 남긴다. redirected된 상태임을 남긴다. */\n const location = window.location\n const origin = location.origin || location.protocol + '//' + location.host\n const href = `${origin}${path}`\n\n if (location.pathname === path) return\n\n // popstate 이벤트가 history.back() 에서만 발생하므로\n // 히스토리에 두번을 넣고 back()을 호출하는 편법을 사용함.\n // forward history가 한번 남는 문제가 있으나 signin 프로세스 중에만 발생하므로 큰 문제는 아님.\n // 이 로직은 login process가 어플리케이션 구조에 종속되는 것을 최소화하기 위함임.\n // 예를 들면, redux 구조에 들어가지 않아도 로그인 프로세스가 동작하도록 한 것임.\n window.history.pushState({ redirected }, '', href)\n window.history.pushState({}, '', href)\n\n window.history.back()\n }\n\n async updateProfile(formProps) {\n const response = await fetch('/auth/update-profile', {\n method: 'POST',\n credentials: 'include',\n headers: HEADER_JSON,\n body: JSON.stringify(formProps)\n })\n\n const message = await response.text()\n if (response.ok) {\n try {\n await this.profile()\n } catch (e) {\n /* ignore profile refresh error */\n }\n\n return message\n }\n\n throw new Error(message)\n }\n\n /**\n * Change the password, and say whether it took.\n *\n * The listeners still get the message, as before. The boolean is for the caller that has the\n * form in front of it: the change-password screen used to clear all three fields the moment\n * it called this, so a wrong current password wiped what the person had typed and they had to\n * enter everything again. Existing callers that ignore the return value behave as they did.\n */\n async changePassword(formProps): Promise<boolean> {\n try {\n const response = await fetch('/auth/change-pass', {\n method: 'POST',\n credentials: 'include',\n headers: HEADER_JSON,\n body: JSON.stringify(formProps)\n })\n\n const message = await response.text()\n if (response.ok) {\n this.onPasswordChanged({ message })\n return true\n }\n\n this.onAuthError({ message })\n return false\n } catch (e) {\n this.onAuthError(e)\n return false\n }\n }\n\n async deleteUser(params) {\n const response = await fetch('/auth/delete-user', {\n method: 'POST',\n credentials: 'include',\n headers: HEADER_JSON,\n body: JSON.stringify(params)\n })\n\n const message = await response.text()\n if (response.ok) {\n return message\n } else {\n throw new Error(message)\n }\n }\n\n async profile() {\n try {\n var searchParams = new URLSearchParams(location.search)\n var token = searchParams.get('token')\n var headers = JSON.parse(JSON.stringify(HEADER_JSON))\n\n if (token) {\n headers.authorization = `Bearer ${token}`\n }\n\n const response = await fetch('/auth/profile', {\n method: 'GET',\n credentials: 'include',\n headers\n })\n\n if (response.ok) {\n if (response.redirected) {\n location.href = response.url\n return\n }\n\n const data = await response.json()\n\n this.onProfileFetched({\n credential: data.user,\n accessToken: data.token,\n domains: data.domains,\n domain: data.domain,\n languages: data.languages\n })\n\n return\n }\n } catch (e) {\n this.onAuthError(e)\n }\n }\n\n async signout() {\n await this.onPreSignout()\n\n window.location.href = '/auth/signout'\n }\n}\n\nexport const auth = new ClientAuth()\n"]}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../client/auth.ts"],"names":[],"mappings":"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","sourcesContent":["/* [ AUTH PATH ]\n signinPath = '/auth/signin'\n signoutPath = '/auth/signout'\n profilePath = '/auth/profile'\n updateProfilePath = '/auth/update-profile'\n changepassPath = '/auth/change-pass'\n deleteUserPath = '/auth/delete-user'\n*/\n\nimport { decideDomainRedirect, DOMAIN_REDIRECT_KEY } from './domain-required.js'\n\nconst HEADER_JSON = {\n 'Content-Type': 'application/json',\n Accept: 'application/json'\n}\ntype AuthEventName = 'profile' | 'signin' | 'signout' | 'presignout' | 'passwordchange' | 'error'\ntype AuthEventHandler = (e?: {\n accessToken?: string\n credential?: string\n domains: any[]\n domain: any\n languages?: { code: string; display: string }[]\n}) => void\ntype AuthErrorHandler = (err: any) => void\n\nclass ClientAuth {\n private listeners: {\n profile: AuthEventHandler[]\n signout: AuthEventHandler[]\n signin: AuthEventHandler[]\n presignout: AuthEventHandler[]\n passwordchange: AuthEventHandler[]\n error: AuthErrorHandler[]\n } = {\n profile: [],\n signout: [],\n signin: [],\n presignout: [],\n passwordchange: [],\n error: []\n }\n\n private authRequiredEventListener = this.onAuthRequired.bind(this)\n private activateRequiredEventListener = this.onActivateRequired.bind(this)\n private domainRequiredEventListener = this.onDomainRequired.bind(this)\n\n /** 한 화면이 질의를 여럿 내므로 403 도 여럿 온다. 보내는 것은 한 번이다. */\n private domainRedirecting = false\n\n private _credential: any\n private accessToken?: string\n private domains: any[] = []\n private domain: any\n private languages: { code: string; display: string }[] = []\n\n constructor() {\n document.addEventListener('auth-required', this.authRequiredEventListener)\n document.addEventListener('activate-required', this.activateRequiredEventListener)\n document.addEventListener('domain-required', this.domainRequiredEventListener)\n }\n\n on(event: AuthEventName, handler: AuthEventHandler | AuthErrorHandler) {\n var listeners = this.listeners[event]\n if (listeners) {\n listeners.push(handler)\n } else {\n console.log('unknown event', event)\n }\n\n if (event == 'profile' && this._credential) {\n /* \n 특별히 event 가 profile 인 경우에는 이미 fetch된 credential이 있다면, 콜백을 해준다. \n 시스템 bootstrap에서 profile 이벤트가 사용되는 경우가 많은데, profile도 매우 초기에 fetch 되므로 레이스컨디션이 발생할 수 있기 때문에, 확실하게 event 콜백을 보장하기 위해서이다.\n */\n handler({ credential: this._credential, domains: this.domains, domain: this.domain, languages: this.languages })\n }\n }\n\n off(event: AuthEventName, handler: AuthEventHandler | AuthErrorHandler) {\n var listeners = this.listeners[event]\n if (listeners) {\n let idx = listeners.indexOf(handler)\n idx >= 0 && listeners.splice(idx, 1)\n } else {\n console.log('unknown event', event)\n }\n }\n\n dispose() {\n document.removeEventListener('auth-required', this.authRequiredEventListener)\n document.removeEventListener('activate-required', this.activateRequiredEventListener)\n document.removeEventListener('domain-required', this.domainRequiredEventListener)\n\n this.listeners = {\n profile: [],\n signin: [],\n signout: [],\n presignout: [],\n passwordchange: [],\n error: []\n }\n }\n\n private onProfileFetched({ credential, accessToken, domains, domain, languages }) {\n this._credential = credential\n this.domains = domains\n this.domain = domain\n this.languages = languages\n\n if (accessToken && !this.accessToken) {\n /*\n 기존에 세션을 가지거나, 액세스토큰으로 인증된 경우,\n 이 경우는 signin 이벤트리스너들을 호출해서 authenticated 상태로 되도록 유도한다.\n */\n this.accessToken = accessToken\n this.listeners.signin.forEach(handler => handler({ accessToken, domains, domain, languages }))\n }\n accessToken && (this.accessToken = accessToken)\n this.listeners.profile.forEach(handler => handler({ credential, domains, domain, languages }))\n }\n\n private async onPreSignout() {\n for (let onpresignout of this.listeners.presignout) {\n await onpresignout()\n }\n }\n\n private onAuthError(error) {\n /* signin, signup 과정에서 에러가 발생한 경우 */\n this.listeners?.error.forEach(handler => handler(error))\n }\n\n private onPasswordChanged(result) {\n //event is passwordchange, handler is result\n this.listeners?.passwordchange.forEach(handler => handler(result))\n }\n\n private onAuthRequired(e) {\n console.warn('authentication required')\n let url = new URL(window.location.href)\n url.pathname = '/auth/signin'\n url.searchParams.append('redirect_to', window.location.href)\n\n window.location.href = url.href\n }\n\n /**\n * GraphQL 이 403 을 받았다 — 이 화면은 사업장이 안 정해진 채로 떴다. 체크인으로 보낸다.\n * 어디로 보내고 언제 안 보내는지는 `domain-required.ts` 가 정한다.\n */\n private onDomainRequired() {\n if (this.domainRedirecting) return\n\n /* 보낼지와 도장은 domain-required.ts 가 정한다 — 도장을 못 적는 브라우저에서는 보내지 않는다. */\n const target = decideDomainRedirect(window.location.href, {\n read: () => sessionStorage.getItem(DOMAIN_REDIRECT_KEY),\n write: value => sessionStorage.setItem(DOMAIN_REDIRECT_KEY, value)\n })\n\n if (!target) {\n console.warn('domain required - not sending to check-in (just came back, or cannot remember that we did)')\n return\n }\n\n this.domainRedirecting = true\n console.warn('domain required')\n window.location.href = target\n }\n\n private onActivateRequired(e) {\n console.warn('activate required')\n var params = new URLSearchParams()\n params.append('email', e.email)\n\n window.location.replace(`/auth/activate?${params}`)\n }\n\n get credential() {\n return this._credential\n }\n\n route(path, redirected) {\n /* history에 남긴다. redirected된 상태임을 남긴다. */\n const location = window.location\n const origin = location.origin || location.protocol + '//' + location.host\n const href = `${origin}${path}`\n\n if (location.pathname === path) return\n\n // popstate 이벤트가 history.back() 에서만 발생하므로\n // 히스토리에 두번을 넣고 back()을 호출하는 편법을 사용함.\n // forward history가 한번 남는 문제가 있으나 signin 프로세스 중에만 발생하므로 큰 문제는 아님.\n // 이 로직은 login process가 어플리케이션 구조에 종속되는 것을 최소화하기 위함임.\n // 예를 들면, redux 구조에 들어가지 않아도 로그인 프로세스가 동작하도록 한 것임.\n window.history.pushState({ redirected }, '', href)\n window.history.pushState({}, '', href)\n\n window.history.back()\n }\n\n async updateProfile(formProps) {\n const response = await fetch('/auth/update-profile', {\n method: 'POST',\n credentials: 'include',\n headers: HEADER_JSON,\n body: JSON.stringify(formProps)\n })\n\n const message = await response.text()\n if (response.ok) {\n try {\n await this.profile()\n } catch (e) {\n /* ignore profile refresh error */\n }\n\n return message\n }\n\n throw new Error(message)\n }\n\n /**\n * Change the password, and say whether it took.\n *\n * The listeners still get the message, as before. The boolean is for the caller that has the\n * form in front of it: the change-password screen used to clear all three fields the moment\n * it called this, so a wrong current password wiped what the person had typed and they had to\n * enter everything again. Existing callers that ignore the return value behave as they did.\n */\n async changePassword(formProps): Promise<boolean> {\n try {\n const response = await fetch('/auth/change-pass', {\n method: 'POST',\n credentials: 'include',\n headers: HEADER_JSON,\n body: JSON.stringify(formProps)\n })\n\n const message = await response.text()\n if (response.ok) {\n this.onPasswordChanged({ message })\n return true\n }\n\n this.onAuthError({ message })\n return false\n } catch (e) {\n this.onAuthError(e)\n return false\n }\n }\n\n async deleteUser(params) {\n const response = await fetch('/auth/delete-user', {\n method: 'POST',\n credentials: 'include',\n headers: HEADER_JSON,\n body: JSON.stringify(params)\n })\n\n const message = await response.text()\n if (response.ok) {\n return message\n } else {\n throw new Error(message)\n }\n }\n\n async profile() {\n try {\n var searchParams = new URLSearchParams(location.search)\n var token = searchParams.get('token')\n var headers = JSON.parse(JSON.stringify(HEADER_JSON))\n\n if (token) {\n headers.authorization = `Bearer ${token}`\n }\n\n const response = await fetch('/auth/profile', {\n method: 'GET',\n credentials: 'include',\n headers\n })\n\n if (response.ok) {\n if (response.redirected) {\n location.href = response.url\n return\n }\n\n const data = await response.json()\n\n this.onProfileFetched({\n credential: data.user,\n accessToken: data.token,\n domains: data.domains,\n domain: data.domain,\n languages: data.languages\n })\n\n return\n }\n } catch (e) {\n this.onAuthError(e)\n }\n }\n\n async signout() {\n await this.onPreSignout()\n\n window.location.href = '/auth/signout'\n }\n}\n\nexport const auth = new ClientAuth()\n"]}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 도메인을 못 정한 채 떠 버린 화면을 **어디로 보내나.** 보내지 않을 때는 `null`.
3
+ *
4
+ * ── 무엇이 비어 있었나 (2026-09-24) ────────────────────────────────────────
5
+ * GraphQL 이 403 을 받으면 `@operato/graphql` 이 `domain-required` 를 내보낸다. 그 자리의 주석이
6
+ * 「auth 모듈 등에서 이 메시지를 받아서 domain-register 프로세스를 진행할 수 있다」고 적고 있는데,
7
+ * **받는 곳이 한 군데도 없었다** [확인함: 세 저장소 client 전체에서 addEventListener 0건]. 짝인
8
+ * `auth-required`(401)는 이 파일 옆 `auth.ts` 가 받아 로그인으로 보낸다.
9
+ *
10
+ * 그래서 도메인이 안 정해진 채로 화면이 뜨면 모든 질의가 403 이고, 화면은 「다시 시도」를 내놓는데
11
+ * 다시 해도 또 403 이다 — **영영 안 된다.** 사람에게는 고장 난 화면으로만 보인다.
12
+ *
13
+ * ── 어디로 보내나 ──────────────────────────────────────────────────────────
14
+ * 서버가 HTML 요청에 **이미 하는 일**을 그대로 한다 — `/auth/checkin?redirect_to=<지금 주소>`
15
+ * (`auth-base/server/middlewares/authenticate-401-middleware.ts`). 거기서 서버가
16
+ *
17
+ * 사업장이 하나면 바로 체크인하고 되돌려 보낸다
18
+ * 여럿이면 「사업장 선택」을 보인다
19
+ * 자격이 없으면 「허용되지 않은 사업장」과 함께 고를 수 있는 것을 보인다
20
+ *
21
+ * 돌아갈 주소에 도메인 조각이 없어도 서버가 `/domain/<사업장>` 을 앞에 붙인다
22
+ * (`shell/server/utils/get-domain.ts` 의 `getRedirectSubdomainPath`). 그래서 **같은 자리를 돌지 않는다.**
23
+ *
24
+ * 화면마다 403 을 따로 그리지 않는다 — 이것은 화면의 결함이 아니라 셸이 아직 사업장을 못 정한 것이다.
25
+ */
26
+ /**
27
+ * 한 번 보낸 뒤 이만큼 안에 또 오면 **다시 보내지 않는다.**
28
+ *
29
+ * 403 은 「도메인 없음」만 뜻하지 않는다. 사업장 IP 차단 목록에 걸린 요청에도 셸이 403 을 준다
30
+ * (`shell/server/middlewares/domain-middleware.ts`). 그 사람은 자격도 있고 사업장도 정해져 있어서, 체크인은
31
+ * 바로 되돌려 보내고 되돌아간 화면은 또 403 을 받는다 — 보내기만 하면 **체크인과 화면 사이를 끝없이 돈다.**
32
+ * 도는 화면은 멈춘 화면보다 나쁘다(아키텍트 지적 2026-09-24).
33
+ *
34
+ * 그래서 까닭을 가리지 않고 **한 번 다녀온 뒤에도 403 이면 멈춘다.** 도메인이 없던 것이라면 한 번 다녀오면
35
+ * 풀린다. 한 번으로 안 풀리면 체크인이 풀 수 있는 일이 아니다.
36
+ *
37
+ * 주소로 알아보지 않고 시각으로 안다 — 돌아올 때 서버가 `/domain/<사업장>` 을 앞에 붙여 주소가 바뀐다.
38
+ */
39
+ export declare const DOMAIN_REDIRECT_WINDOW_MS = 30000;
40
+ export declare function domainRequiredTarget(href: string, lastRedirectAt?: number | null, now?: number): string | null;
41
+ /** 도장을 적고 읽는 자리. 브라우저가 막으면 던진다. */
42
+ export type RedirectStamp = {
43
+ read(): string | null;
44
+ write(value: string): void;
45
+ };
46
+ export declare const DOMAIN_REDIRECT_KEY = "auth:domain-required-redirected-at";
47
+ /**
48
+ * 보낼지 정하고, 보낸다면 **먼저 도장을 적는다.** 보낼 곳을 돌려준다 — 보내지 않으면 `null`.
49
+ *
50
+ * ⚠ **도장을 못 적거나 못 읽으면 보내지 않는다.** 도장이 없으면 「방금 다녀왔다」를 알 수 없고, 그러면
51
+ * IP 차단된 사람이 체크인과 화면 사이를 다시 돈다. 멈춘 화면이 도는 화면보다 낫다(아키텍트 지적 09-24 —
52
+ * 처음 판은 「못 읽으면 처음으로 본다」로 반대쪽에 기울어 있었다).
53
+ */
54
+ export declare function decideDomainRedirect(href: string, stamp: RedirectStamp, now?: number): string | null;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * 도메인을 못 정한 채 떠 버린 화면을 **어디로 보내나.** 보내지 않을 때는 `null`.
3
+ *
4
+ * ── 무엇이 비어 있었나 (2026-09-24) ────────────────────────────────────────
5
+ * GraphQL 이 403 을 받으면 `@operato/graphql` 이 `domain-required` 를 내보낸다. 그 자리의 주석이
6
+ * 「auth 모듈 등에서 이 메시지를 받아서 domain-register 프로세스를 진행할 수 있다」고 적고 있는데,
7
+ * **받는 곳이 한 군데도 없었다** [확인함: 세 저장소 client 전체에서 addEventListener 0건]. 짝인
8
+ * `auth-required`(401)는 이 파일 옆 `auth.ts` 가 받아 로그인으로 보낸다.
9
+ *
10
+ * 그래서 도메인이 안 정해진 채로 화면이 뜨면 모든 질의가 403 이고, 화면은 「다시 시도」를 내놓는데
11
+ * 다시 해도 또 403 이다 — **영영 안 된다.** 사람에게는 고장 난 화면으로만 보인다.
12
+ *
13
+ * ── 어디로 보내나 ──────────────────────────────────────────────────────────
14
+ * 서버가 HTML 요청에 **이미 하는 일**을 그대로 한다 — `/auth/checkin?redirect_to=<지금 주소>`
15
+ * (`auth-base/server/middlewares/authenticate-401-middleware.ts`). 거기서 서버가
16
+ *
17
+ * 사업장이 하나면 바로 체크인하고 되돌려 보낸다
18
+ * 여럿이면 「사업장 선택」을 보인다
19
+ * 자격이 없으면 「허용되지 않은 사업장」과 함께 고를 수 있는 것을 보인다
20
+ *
21
+ * 돌아갈 주소에 도메인 조각이 없어도 서버가 `/domain/<사업장>` 을 앞에 붙인다
22
+ * (`shell/server/utils/get-domain.ts` 의 `getRedirectSubdomainPath`). 그래서 **같은 자리를 돌지 않는다.**
23
+ *
24
+ * 화면마다 403 을 따로 그리지 않는다 — 이것은 화면의 결함이 아니라 셸이 아직 사업장을 못 정한 것이다.
25
+ */
26
+ /**
27
+ * 한 번 보낸 뒤 이만큼 안에 또 오면 **다시 보내지 않는다.**
28
+ *
29
+ * 403 은 「도메인 없음」만 뜻하지 않는다. 사업장 IP 차단 목록에 걸린 요청에도 셸이 403 을 준다
30
+ * (`shell/server/middlewares/domain-middleware.ts`). 그 사람은 자격도 있고 사업장도 정해져 있어서, 체크인은
31
+ * 바로 되돌려 보내고 되돌아간 화면은 또 403 을 받는다 — 보내기만 하면 **체크인과 화면 사이를 끝없이 돈다.**
32
+ * 도는 화면은 멈춘 화면보다 나쁘다(아키텍트 지적 2026-09-24).
33
+ *
34
+ * 그래서 까닭을 가리지 않고 **한 번 다녀온 뒤에도 403 이면 멈춘다.** 도메인이 없던 것이라면 한 번 다녀오면
35
+ * 풀린다. 한 번으로 안 풀리면 체크인이 풀 수 있는 일이 아니다.
36
+ *
37
+ * 주소로 알아보지 않고 시각으로 안다 — 돌아올 때 서버가 `/domain/<사업장>` 을 앞에 붙여 주소가 바뀐다.
38
+ */
39
+ export const DOMAIN_REDIRECT_WINDOW_MS = 30_000;
40
+ export function domainRequiredTarget(href, lastRedirectAt, now = Date.now()) {
41
+ if (typeof lastRedirectAt === 'number' && now - lastRedirectAt >= 0 && now - lastRedirectAt < DOMAIN_REDIRECT_WINDOW_MS) {
42
+ return null;
43
+ }
44
+ let current;
45
+ try {
46
+ current = new URL(href);
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ /*
52
+ * 체크인 화면 · 로그인 화면 자체에서 403 이 나면 보내지 않는다. 거기서 다시 체크인으로 보내면
53
+ * 제자리를 돈다 — 그 화면들은 자기가 사람에게 말한다.
54
+ */
55
+ if (current.pathname === '/auth' || current.pathname.startsWith('/auth/')) {
56
+ return null;
57
+ }
58
+ const target = new URL('/auth/checkin', current.origin);
59
+ target.searchParams.set('redirect_to', href);
60
+ return target.toString();
61
+ }
62
+ export const DOMAIN_REDIRECT_KEY = 'auth:domain-required-redirected-at';
63
+ /**
64
+ * 보낼지 정하고, 보낸다면 **먼저 도장을 적는다.** 보낼 곳을 돌려준다 — 보내지 않으면 `null`.
65
+ *
66
+ * ⚠ **도장을 못 적거나 못 읽으면 보내지 않는다.** 도장이 없으면 「방금 다녀왔다」를 알 수 없고, 그러면
67
+ * IP 차단된 사람이 체크인과 화면 사이를 다시 돈다. 멈춘 화면이 도는 화면보다 낫다(아키텍트 지적 09-24 —
68
+ * 처음 판은 「못 읽으면 처음으로 본다」로 반대쪽에 기울어 있었다).
69
+ */
70
+ export function decideDomainRedirect(href, stamp, now = Date.now()) {
71
+ let lastRedirectAt;
72
+ try {
73
+ const saved = stamp.read();
74
+ const parsed = saved == null ? NaN : Number(saved);
75
+ lastRedirectAt = Number.isFinite(parsed) && parsed > 0 ? parsed : null;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ const target = domainRequiredTarget(href, lastRedirectAt, now);
81
+ if (!target)
82
+ return null;
83
+ try {
84
+ stamp.write(String(now));
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ return target;
90
+ }
91
+ //# sourceMappingURL=domain-required.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"domain-required.js","sourceRoot":"","sources":["../client/domain-required.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAA;AAE/C,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,cAA8B,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACzG,IAAI,OAAO,cAAc,KAAK,QAAQ,IAAI,GAAG,GAAG,cAAc,IAAI,CAAC,IAAI,GAAG,GAAG,cAAc,GAAG,yBAAyB,EAAE,CAAC;QACxH,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,OAAY,CAAA;IAEhB,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAA;IACzB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;OAGG;IACH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1E,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACvD,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,aAAa,EAAE,IAAI,CAAC,CAAA;IAE5C,OAAO,MAAM,CAAC,QAAQ,EAAE,CAAA;AAC1B,CAAC;AAQD,MAAM,CAAC,MAAM,mBAAmB,GAAG,oCAAoC,CAAA;AAEvE;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAE,KAAoB,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IAC/F,IAAI,cAA6B,CAAA;IAEjC,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAA;QAC1B,MAAM,MAAM,GAAG,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAClD,cAAc,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAA;IACxE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,MAAM,GAAG,oBAAoB,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,CAAC,CAAA;IAC9D,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,IAAI,CAAC;QACH,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAA;IACb,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC","sourcesContent":["/**\n * 도메인을 못 정한 채 떠 버린 화면을 **어디로 보내나.** 보내지 않을 때는 `null`.\n *\n * ── 무엇이 비어 있었나 (2026-09-24) ────────────────────────────────────────\n * GraphQL 이 403 을 받으면 `@operato/graphql` 이 `domain-required` 를 내보낸다. 그 자리의 주석이\n * 「auth 모듈 등에서 이 메시지를 받아서 domain-register 프로세스를 진행할 수 있다」고 적고 있는데,\n * **받는 곳이 한 군데도 없었다** [확인함: 세 저장소 client 전체에서 addEventListener 0건]. 짝인\n * `auth-required`(401)는 이 파일 옆 `auth.ts` 가 받아 로그인으로 보낸다.\n *\n * 그래서 도메인이 안 정해진 채로 화면이 뜨면 모든 질의가 403 이고, 화면은 「다시 시도」를 내놓는데\n * 다시 해도 또 403 이다 — **영영 안 된다.** 사람에게는 고장 난 화면으로만 보인다.\n *\n * ── 어디로 보내나 ──────────────────────────────────────────────────────────\n * 서버가 HTML 요청에 **이미 하는 일**을 그대로 한다 — `/auth/checkin?redirect_to=<지금 주소>`\n * (`auth-base/server/middlewares/authenticate-401-middleware.ts`). 거기서 서버가\n *\n * 사업장이 하나면 바로 체크인하고 되돌려 보낸다\n * 여럿이면 「사업장 선택」을 보인다\n * 자격이 없으면 「허용되지 않은 사업장」과 함께 고를 수 있는 것을 보인다\n *\n * 돌아갈 주소에 도메인 조각이 없어도 서버가 `/domain/<사업장>` 을 앞에 붙인다\n * (`shell/server/utils/get-domain.ts` 의 `getRedirectSubdomainPath`). 그래서 **같은 자리를 돌지 않는다.**\n *\n * 화면마다 403 을 따로 그리지 않는다 — 이것은 화면의 결함이 아니라 셸이 아직 사업장을 못 정한 것이다.\n */\n/**\n * 한 번 보낸 뒤 이만큼 안에 또 오면 **다시 보내지 않는다.**\n *\n * 403 은 「도메인 없음」만 뜻하지 않는다. 사업장 IP 차단 목록에 걸린 요청에도 셸이 403 을 준다\n * (`shell/server/middlewares/domain-middleware.ts`). 그 사람은 자격도 있고 사업장도 정해져 있어서, 체크인은\n * 바로 되돌려 보내고 되돌아간 화면은 또 403 을 받는다 — 보내기만 하면 **체크인과 화면 사이를 끝없이 돈다.**\n * 도는 화면은 멈춘 화면보다 나쁘다(아키텍트 지적 2026-09-24).\n *\n * 그래서 까닭을 가리지 않고 **한 번 다녀온 뒤에도 403 이면 멈춘다.** 도메인이 없던 것이라면 한 번 다녀오면\n * 풀린다. 한 번으로 안 풀리면 체크인이 풀 수 있는 일이 아니다.\n *\n * 주소로 알아보지 않고 시각으로 안다 — 돌아올 때 서버가 `/domain/<사업장>` 을 앞에 붙여 주소가 바뀐다.\n */\nexport const DOMAIN_REDIRECT_WINDOW_MS = 30_000\n\nexport function domainRequiredTarget(href: string, lastRedirectAt?: number | null, now: number = Date.now()): string | null {\n if (typeof lastRedirectAt === 'number' && now - lastRedirectAt >= 0 && now - lastRedirectAt < DOMAIN_REDIRECT_WINDOW_MS) {\n return null\n }\n\n let current: URL\n\n try {\n current = new URL(href)\n } catch {\n return null\n }\n\n /*\n * 체크인 화면 · 로그인 화면 자체에서 403 이 나면 보내지 않는다. 거기서 다시 체크인으로 보내면\n * 제자리를 돈다 — 그 화면들은 자기가 사람에게 말한다.\n */\n if (current.pathname === '/auth' || current.pathname.startsWith('/auth/')) {\n return null\n }\n\n const target = new URL('/auth/checkin', current.origin)\n target.searchParams.set('redirect_to', href)\n\n return target.toString()\n}\n\n/** 도장을 적고 읽는 자리. 브라우저가 막으면 던진다. */\nexport type RedirectStamp = {\n read(): string | null\n write(value: string): void\n}\n\nexport const DOMAIN_REDIRECT_KEY = 'auth:domain-required-redirected-at'\n\n/**\n * 보낼지 정하고, 보낸다면 **먼저 도장을 적는다.** 보낼 곳을 돌려준다 — 보내지 않으면 `null`.\n *\n * ⚠ **도장을 못 적거나 못 읽으면 보내지 않는다.** 도장이 없으면 「방금 다녀왔다」를 알 수 없고, 그러면\n * IP 차단된 사람이 체크인과 화면 사이를 다시 돈다. 멈춘 화면이 도는 화면보다 낫다(아키텍트 지적 09-24 —\n * 처음 판은 「못 읽으면 처음으로 본다」로 반대쪽에 기울어 있었다).\n */\nexport function decideDomainRedirect(href: string, stamp: RedirectStamp, now: number = Date.now()): string | null {\n let lastRedirectAt: number | null\n\n try {\n const saved = stamp.read()\n const parsed = saved == null ? NaN : Number(saved)\n lastRedirectAt = Number.isFinite(parsed) && parsed > 0 ? parsed : null\n } catch {\n return null\n }\n\n const target = domainRequiredTarget(href, lastRedirectAt, now)\n if (!target) return null\n\n try {\n stamp.write(String(now))\n } catch {\n return null\n }\n\n return target\n}\n\n"]}