@aiquants/auth-google-directory 0.3.0 → 0.4.0

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.
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import { AuthDirectoryCatalogGroup } from '@aiquants/auth-core';
1
2
  import { DirectoryProvider } from '@aiquants/auth-directory-core';
2
3
 
3
4
  /**
@@ -59,6 +60,16 @@ declare class GoogleDirectoryShapeError extends Error {
59
60
  readonly url: string;
60
61
  constructor(message: string, url: string);
61
62
  }
63
+ /**
64
+ * Renders an untrusted value for a diagnostic message. 信用できない値を診断文へ載せる形にする処理。
65
+ *
66
+ * ⚠️ 上流 (あるいは割り込んだ中間装置) が決めた値をそのまま文へ入れない。長さは相手が決められる
67
+ * ため、20 KB の文言がログにも `sync_error` 列にも流れる。資格情報が混じっていれば伏せる。
68
+ *
69
+ * @param value - Whatever the body carried. 本文が運んできた値。
70
+ * @returns A short, redacted rendering. 短く、伏せ字を通した表現。
71
+ */
72
+ declare function describeUntrusted(value: unknown): string;
62
73
  /** Configuration shared by the Google API clients. Google API クライアント共通の構成。 */
63
74
  type GoogleHttpConfig = {
64
75
  /** Supplies a bearer token for each call. 呼び出しごとに Bearer トークンを供給する処理。 */
@@ -128,17 +139,15 @@ declare function fetchAllPages<TItem>(reader: PagedReader<TItem>, config: Google
128
139
  * グループが 1 件も無いことと、上流へ届かないことが区別できなくなる。
129
140
  */
130
141
 
131
- /** One group as the catalog reports it. 候補一覧が報告するグループ 1 件。 */
132
- type DirectoryCatalogGroup = {
133
- /** Stable upstream identifier. 上流の安定識別子。 */
134
- externalId: string;
135
- /** Operator-facing key such as the group address. 運用者向けキー (グループアドレス等)。 */
136
- externalKey: string;
137
- name: string;
138
- /** Members the directory reports for the group. 上流が報告するメンバー数。 */
139
- memberCount?: number;
140
- description?: string;
141
- };
142
+ /**
143
+ * One group as the catalog reports it. 候補一覧が報告するグループ 1 件。
144
+ *
145
+ * ⚠️ **別名ではなく `@aiquants/auth-core` の型そのものである。** この形は上流アダプタ (ここ)・
146
+ * 永続化層・管理 UI 3 層を通る。層ごとに「同じ意味の別の型」を宣言すると、構造的部分型の
147
+ * せいでどの境界も通ってしまい、**上流へ足した欄が途中の層で黙って捨てられる** — 読み手側の
148
+ * 境界は型エラーを出さないので、足した本人も気付けない。定義は 1 つに置き、各層が import する。
149
+ */
150
+ type DirectoryCatalogGroup = AuthDirectoryCatalogGroup;
142
151
  /** Configuration of the catalog reader. 候補一覧リーダーの構成。 */
143
152
  type GoogleDirectoryCatalogConfig = GoogleHttpConfig & {
144
153
  /**
@@ -270,4 +279,4 @@ type DomainWideDelegationConfig = {
270
279
  */
271
280
  declare function createDomainWideDelegationTokenProvider(config: DomainWideDelegationConfig): GoogleHttpConfig["getAccessToken"];
272
281
 
273
- export { type DirectoryCatalogGroup, type DomainWideDelegationConfig, type FetchLike, GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE, GOOGLE_DIRECTORY_READONLY_SCOPES, GoogleDirectoryAuthError, type GoogleDirectoryCatalogConfig, type GoogleDirectoryConfig, GoogleDirectoryHttpError, GoogleDirectoryShapeError, type GoogleHttpConfig, type JwtClientLike, type PagedReader, type RetryPolicy, createDomainWideDelegationTokenProvider, createGoogleDirectoryCatalog, createGoogleDirectoryProvider, fetchAllPages, fetchJson };
282
+ export { type DirectoryCatalogGroup, type DomainWideDelegationConfig, type FetchLike, GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE, GOOGLE_DIRECTORY_READONLY_SCOPES, GoogleDirectoryAuthError, type GoogleDirectoryCatalogConfig, type GoogleDirectoryConfig, GoogleDirectoryHttpError, GoogleDirectoryShapeError, type GoogleHttpConfig, type JwtClientLike, type PagedReader, type RetryPolicy, createDomainWideDelegationTokenProvider, createGoogleDirectoryCatalog, createGoogleDirectoryProvider, describeUntrusted, fetchAllPages, fetchJson };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { AuthDirectoryCatalogGroup } from '@aiquants/auth-core';
1
2
  import { DirectoryProvider } from '@aiquants/auth-directory-core';
2
3
 
3
4
  /**
@@ -59,6 +60,16 @@ declare class GoogleDirectoryShapeError extends Error {
59
60
  readonly url: string;
60
61
  constructor(message: string, url: string);
61
62
  }
63
+ /**
64
+ * Renders an untrusted value for a diagnostic message. 信用できない値を診断文へ載せる形にする処理。
65
+ *
66
+ * ⚠️ 上流 (あるいは割り込んだ中間装置) が決めた値をそのまま文へ入れない。長さは相手が決められる
67
+ * ため、20 KB の文言がログにも `sync_error` 列にも流れる。資格情報が混じっていれば伏せる。
68
+ *
69
+ * @param value - Whatever the body carried. 本文が運んできた値。
70
+ * @returns A short, redacted rendering. 短く、伏せ字を通した表現。
71
+ */
72
+ declare function describeUntrusted(value: unknown): string;
62
73
  /** Configuration shared by the Google API clients. Google API クライアント共通の構成。 */
63
74
  type GoogleHttpConfig = {
64
75
  /** Supplies a bearer token for each call. 呼び出しごとに Bearer トークンを供給する処理。 */
@@ -128,17 +139,15 @@ declare function fetchAllPages<TItem>(reader: PagedReader<TItem>, config: Google
128
139
  * グループが 1 件も無いことと、上流へ届かないことが区別できなくなる。
129
140
  */
130
141
 
131
- /** One group as the catalog reports it. 候補一覧が報告するグループ 1 件。 */
132
- type DirectoryCatalogGroup = {
133
- /** Stable upstream identifier. 上流の安定識別子。 */
134
- externalId: string;
135
- /** Operator-facing key such as the group address. 運用者向けキー (グループアドレス等)。 */
136
- externalKey: string;
137
- name: string;
138
- /** Members the directory reports for the group. 上流が報告するメンバー数。 */
139
- memberCount?: number;
140
- description?: string;
141
- };
142
+ /**
143
+ * One group as the catalog reports it. 候補一覧が報告するグループ 1 件。
144
+ *
145
+ * ⚠️ **別名ではなく `@aiquants/auth-core` の型そのものである。** この形は上流アダプタ (ここ)・
146
+ * 永続化層・管理 UI 3 層を通る。層ごとに「同じ意味の別の型」を宣言すると、構造的部分型の
147
+ * せいでどの境界も通ってしまい、**上流へ足した欄が途中の層で黙って捨てられる** — 読み手側の
148
+ * 境界は型エラーを出さないので、足した本人も気付けない。定義は 1 つに置き、各層が import する。
149
+ */
150
+ type DirectoryCatalogGroup = AuthDirectoryCatalogGroup;
142
151
  /** Configuration of the catalog reader. 候補一覧リーダーの構成。 */
143
152
  type GoogleDirectoryCatalogConfig = GoogleHttpConfig & {
144
153
  /**
@@ -270,4 +279,4 @@ type DomainWideDelegationConfig = {
270
279
  */
271
280
  declare function createDomainWideDelegationTokenProvider(config: DomainWideDelegationConfig): GoogleHttpConfig["getAccessToken"];
272
281
 
273
- export { type DirectoryCatalogGroup, type DomainWideDelegationConfig, type FetchLike, GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE, GOOGLE_DIRECTORY_READONLY_SCOPES, GoogleDirectoryAuthError, type GoogleDirectoryCatalogConfig, type GoogleDirectoryConfig, GoogleDirectoryHttpError, GoogleDirectoryShapeError, type GoogleHttpConfig, type JwtClientLike, type PagedReader, type RetryPolicy, createDomainWideDelegationTokenProvider, createGoogleDirectoryCatalog, createGoogleDirectoryProvider, fetchAllPages, fetchJson };
282
+ export { type DirectoryCatalogGroup, type DomainWideDelegationConfig, type FetchLike, GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE, GOOGLE_DIRECTORY_READONLY_SCOPES, GoogleDirectoryAuthError, type GoogleDirectoryCatalogConfig, type GoogleDirectoryConfig, GoogleDirectoryHttpError, GoogleDirectoryShapeError, type GoogleHttpConfig, type JwtClientLike, type PagedReader, type RetryPolicy, createDomainWideDelegationTokenProvider, createGoogleDirectoryCatalog, createGoogleDirectoryProvider, describeUntrusted, fetchAllPages, fetchJson };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";var P=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var v=Object.getOwnPropertyNames;var U=Object.prototype.hasOwnProperty;var _=(e,t)=>{for(var n in t)P(e,n,{get:t[n],enumerable:!0})},L=(e,t,n,l)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of v(t))!U.call(e,i)&&i!==n&&P(e,i,{get:()=>t[i],enumerable:!(l=O(t,i))||l.enumerable});return e};var z=e=>L(P({},"__esModule",{value:!0}),e);var X={};_(X,{GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE:()=>N,GOOGLE_DIRECTORY_READONLY_SCOPES:()=>M,GoogleDirectoryAuthError:()=>E,GoogleDirectoryHttpError:()=>k,GoogleDirectoryShapeError:()=>d,createDomainWideDelegationTokenProvider:()=>V,createGoogleDirectoryCatalog:()=>A,createGoogleDirectoryProvider:()=>W,fetchAllPages:()=>D,fetchJson:()=>C});module.exports=z(X);var k=class extends Error{constructor(t,n,l,i){super(t),this.name="GoogleDirectoryHttpError",this.status=n,this.url=l,i&&(this.reason=i)}},E=class extends Error{constructor(t){super(t),this.name="GoogleDirectoryAuthError"}},d=class extends Error{constructor(t,n){super(t),this.name="GoogleDirectoryShapeError",this.url=n}},q=new Set(["userRateLimitExceeded","quotaExceeded","rateLimitExceeded","backendError"]);function J(e){try{let t=JSON.parse(e);return t.error?.errors?.[0]?.reason??t.error?.status}catch{return}}function G(e,t){return e===429||e===408||e>=500?!0:e===403&&t!==void 0&&q.has(t)}var K=e=>e.replace(/Bearer\s+[\w.\-~+/]+=*/gi,"Bearer [redacted]"),j={attempts:4,baseDelayMs:500,timeoutMs:3e4};function H(e){if(!Number.isInteger(e.attempts)||e.attempts<1)throw new TypeError(`[@aiquants/auth-google-directory] retry.attempts must be a positive integer, received ${String(e.attempts)}`);if(!Number.isFinite(e.baseDelayMs)||e.baseDelayMs<0)throw new TypeError(`[@aiquants/auth-google-directory] retry.baseDelayMs must be a non-negative number, received ${String(e.baseDelayMs)}`);if(!Number.isFinite(e.timeoutMs)||e.timeoutMs<=0)throw new TypeError(`[@aiquants/auth-google-directory] retry.timeoutMs must be a positive number, received ${String(e.timeoutMs)}`)}async function C(e,t,n){let l=t.fetch??globalThis.fetch,i=t.retry??j;H(i);let s=t.sleep??(w=>new Promise(r=>setTimeout(r,w))),h=t.random??Math.random,p=new k(`Google Directory request was never attempted: ${e}`,0,e);for(let w=1;w<=i.attempts;w++){try{let r;try{r=await t.getAccessToken()}catch(o){throw new E(`could not obtain an access token: ${o instanceof Error?o.message:String(o)}`)}let a=new AbortController,m=setTimeout(()=>a.abort(),i.timeoutMs),u;try{u=await l(e,{headers:{Authorization:`Bearer ${r}`,Accept:"application/json"},signal:a.signal})}finally{clearTimeout(m)}if(u.ok){let o=await u.text(),g;try{g=JSON.parse(o)}catch{throw new d(`Google Directory returned a non-JSON body (${o.slice(0,120)})`,e)}if(g===null||typeof g!="object"||Array.isArray(g))throw new d(`Google Directory returned ${Array.isArray(g)?"an array":String(g)} where a JSON object was expected`,e);return n(g,e)}else{let o=K((await u.text()).slice(0,500)),g=J(o),c=new k(`Google Directory request failed: ${u.status} ${o}`,u.status,e,g);if(!G(u.status,g))throw c;p=c}}catch(r){if(r instanceof E||r instanceof d||r instanceof k&&!G(r.status,r.reason))throw r;p=r instanceof Error?r:new Error(String(r))}if(w<i.attempts){let r=i.baseDelayMs*2**(w-1);await s(r+Math.floor(h()*i.baseDelayMs))}}throw p}async function D(e,t){let n=[],l=new Set,i;for(let s=0;s<e.maxPages;s++){let{items:h,nextPageToken:p}=await C(e.buildUrl(i),t,e.extract);if(n.push(...h),!p)return n;if(l.has(p))throw new Error(`Google Directory paging revisited a page token; refusing to loop (${e.buildUrl(i)})`);l.add(p),i=p}throw new Error(`Google Directory paging exceeded ${e.maxPages} pages; refusing to report a partial member list`)}var B="https://admin.googleapis.com/admin/directory/v1";function x(e){return typeof e=="string"?e.trim():""}function A(e){let t=e.pageSize??200,n=e.maxPages??200,l=(e.customerId??"my_customer").trim();if(!Number.isInteger(t)||t<1||t>200)throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(e.pageSize)}`);if(!Number.isInteger(n)||n<1)throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(e.maxPages)}`);if(l==="")throw new TypeError("[@aiquants/auth-google-directory] customerId must not be blank");return{async listGroups(){return(await D({buildUrl:s=>`${B}/groups?customer=${encodeURIComponent(l)}&maxResults=${t}${s?`&pageToken=${encodeURIComponent(s)}`:""}`,extract:(s,h)=>{if(s.kind!=="admin#directory#groups")throw new Error(`Google Directory returned an unexpected document for ${h}: kind=${String(s.kind)}`);let p=s.groups;if(p===void 0)return{items:[],nextPageToken:x(s.nextPageToken)||void 0};if(!Array.isArray(p))throw new Error(`Google Directory returned a non-array group list for ${h}`);return{items:p,nextPageToken:x(s.nextPageToken)||void 0}},maxPages:n},e)).map(s=>{let h=x(s.id),p=x(s.email);if(h===""||p==="")return null;let w=Number(x(s.directMembersCount)||s.directMembersCount),r=Number.isInteger(w)&&w>=0?w:void 0;return{externalId:h,externalKey:p,name:x(s.name)||p,...r===void 0?{}:{memberCount:r},...x(s.description)===""?{}:{description:x(s.description)}}}).filter(s=>s!==null)}}}var T="https://admin.googleapis.com/admin/directory/v1",S="https://cloudidentity.googleapis.com/v1",I={person:new Set(["USER","EXTERNAL"]),group:new Set(["GROUP"])},Y=new Set(["ACTIVE",""]),F=new Set(["SUSPENDED","ARCHIVED","INVITED","PENDING"]);function R(e){let t=e.trim();if(!/^[A-Za-z0-9][A-Za-z0-9._%+-]*(@[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,})?$/.test(t))throw new TypeError(`[@aiquants/auth-google-directory] externalId must be a plain identifier or address, received ${JSON.stringify(e)}`);return t}var y=e=>typeof e=="string"?e.trim():"";function W(e){let t=e.pageSize??200,n=e.maxPages??200;if(!Number.isInteger(t)||t<1||t>200)throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(e.pageSize)}`);if(!Number.isInteger(n)||n<1)throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(e.maxPages)}`);function l(r,a,m){if(y(a.kind)!==r)throw new d(`expected a ${r} document but the body carried kind=${JSON.stringify(a.kind??null)}; refusing to treat it as an empty result`,m);return a}function i(r,a,m){let u=Object.keys(a).filter(o=>!r.includes(o));if(u.length>0)throw new d(`body carried unexpected keys (${u.join(", ")}); refusing to treat it as an empty result`,m);return a}function s(r,a){if(r==="")throw new d("Google Directory returned a member row without an address; refusing to report an incomplete member list",T);return a?{email:r,externalUserId:a}:{email:r}}async function h(r){let a=R(r);try{let m=await C(`${T}/groups/${encodeURIComponent(a)}`,e,(g,c)=>l("admin#directory#group",g,c)),u=y(m.id),o=y(m.email);if(u===""||o==="")throw new d(`Google Directory returned an incomplete group resource for ${a}`,`${T}/groups/${a}`);return{externalId:u,externalKey:o,name:y(m.name)||o,...y(m.description)?{description:y(m.description)}:{}}}catch(m){if(m instanceof k&&m.status===404)return null;throw m}}async function p(r){let a=R(r.externalId),m=await D({buildUrl:o=>`${T}/groups/${encodeURIComponent(a)}/members?maxResults=${t}${o?`&pageToken=${encodeURIComponent(o)}`:""}`,extract:(o,g)=>{let c=l("admin#directory#members",o,g);return{items:c.members??[],nextPageToken:y(c.nextPageToken)||void 0}},maxPages:n},e),u=[];for(let o of m){let g=y(o.type)||"USER";if(I.group.has(g))continue;if(!I.person.has(g))throw new d(`Google Directory reported member type ${JSON.stringify(g)}, which this adapter cannot classify; refusing to guess whether it is a person`,T);let c=y(o.status);if(!F.has(c)){if(!Y.has(c))throw new d(`Google Directory reported member status ${JSON.stringify(c)}, which this adapter cannot classify`,T);u.push(s(y(o.email),y(o.id)||void 0))}}return u}async function w(r){let a=`${S}/groups:lookup?groupKey.id=${encodeURIComponent(r.externalKey)}`,m=await C(a,e,(c,f)=>i(["name"],c,f)),u=y(m.name);if(u==="")throw new d(`Cloud Identity did not return a resource name for ${r.externalKey}`,a);if(!/^groups\/[A-Za-z0-9_-]+$/.test(u))throw new d(`Cloud Identity returned a resource name this adapter will not put in a URL: ${JSON.stringify(u)}`,a);let o=await D({buildUrl:c=>`${S}/${u}/memberships:searchTransitiveMemberships?pageSize=${t}${c?`&pageToken=${encodeURIComponent(c)}`:""}`,extract:(c,f)=>{let b=i(["memberships","nextPageToken"],c,f);return{items:b.memberships??[],nextPageToken:y(b.nextPageToken)||void 0}},maxPages:n},e),g=[];for(let c of o){let f=y(c.member);if(f.startsWith("groups/"))continue;if(!f.startsWith("users/"))throw new d(`Cloud Identity reported a member resource ${JSON.stringify(f)}, which this adapter cannot classify; refusing to guess whether it is a person`,S);let b=Array.isArray(c.preferredMemberKey)?c.preferredMemberKey:[];if(b.length!==1)throw new d(`Cloud Identity reported ${b.length} member keys for ${f}; refusing to pick one`,S);if(y(b[0]?.namespace)!=="")throw new d(`Cloud Identity reported an external-identity member (${y(b[0]?.namespace)}); its id is not an address`,S);let $=f.slice(6).trim();if($==="")throw new d(`Cloud Identity reported an empty user id in ${JSON.stringify(f)}`,S);g.push(s(y(b[0]?.id),$))}return g}return{getGroup:h,async listGroupMembers(r,a){return a==="direct"?p(r):w(r)}}}var M=["https://www.googleapis.com/auth/admin.directory.group.readonly","https://www.googleapis.com/auth/admin.directory.group.member.readonly"],N="https://www.googleapis.com/auth/cloud-identity.groups.readonly";function Z(e){let t=e.filter(n=>!n.endsWith(".readonly"));if(t.length>0)throw new TypeError(`[@aiquants/auth-google-directory] only read-only scopes are allowed, received ${t.join(", ")}`)}function V(e){let t=e.subject.trim();if(t==="")throw new TypeError("[@aiquants/auth-google-directory] subject must be a non-empty Workspace user to impersonate");let n=[...M,...e.includeNestedScope?[N]:[],...e.additionalScopes??[]];Z(n);let l=e.createClient({subject:t,scopes:n});return async()=>{let{token:i}=await l.getAccessToken();if(!i)throw new Error(`[@aiquants/auth-google-directory] could not obtain an access token for ${t}`);return i}}0&&(module.exports={GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE,GOOGLE_DIRECTORY_READONLY_SCOPES,GoogleDirectoryAuthError,GoogleDirectoryHttpError,GoogleDirectoryShapeError,createDomainWideDelegationTokenProvider,createGoogleDirectoryCatalog,createGoogleDirectoryProvider,fetchAllPages,fetchJson});
1
+ "use strict";var A=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var z=(e,t)=>{for(var n in t)A(e,n,{get:t[n],enumerable:!0})},q=(e,t,n,l)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of _(t))!L.call(e,o)&&o!==n&&A(e,o,{get:()=>t[o],enumerable:!(l=U(t,o))||l.enumerable});return e};var J=e=>q(A({},"__esModule",{value:!0}),e);var ee={};z(ee,{GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE:()=>O,GOOGLE_DIRECTORY_READONLY_SCOPES:()=>v,GoogleDirectoryAuthError:()=>S,GoogleDirectoryHttpError:()=>k,GoogleDirectoryShapeError:()=>d,createDomainWideDelegationTokenProvider:()=>Q,createGoogleDirectoryCatalog:()=>R,createGoogleDirectoryProvider:()=>V,describeUntrusted:()=>$,fetchAllPages:()=>T,fetchJson:()=>C});module.exports=J(ee);var k=class extends Error{constructor(t,n,l,o){super(t),this.name="GoogleDirectoryHttpError",this.status=n,this.url=l,o&&(this.reason=o)}},S=class extends Error{constructor(t){super(t),this.name="GoogleDirectoryAuthError"}},d=class extends Error{constructor(t,n){super(t),this.name="GoogleDirectoryShapeError",this.url=n}},K=new Set(["userRateLimitExceeded","quotaExceeded","rateLimitExceeded","backendError"]);function B(e){try{let t=JSON.parse(e);return t.error?.errors?.[0]?.reason??t.error?.status}catch{return}}function I(e,t){return e===429||e===408||e>=500?!0:e===403&&t!==void 0&&K.has(t)}var j=8192;function P(e){return e.slice(0,j).replace(/\bBearer\s+[\w.\-~+/]+=*/g,"Bearer [redacted]").replace(/\bBasic\s+[A-Za-z0-9+/]+={0,2}/g,"Basic [redacted]").replace(/eyJ[\w-]*\.[\w-]+\.[\w-]+/g,"[redacted-jwt]").replace(/ya29\.[\w.\-~+/]+=*/g,"[redacted-access-token]").replace(/1\/\/[\w-]{10,}/g,"[redacted-refresh-token]").replace(/-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----[\s\S]{0,4000}?-----END [A-Z ]{0,20}PRIVATE KEY-----/g,"[redacted-private-key]").replace(/-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----[\s\S]{0,4000}/g,"[redacted-private-key]").replace(/\b((?:access|refresh|id)_token|assertion|client_secret|private_key)("?[ \t]*[:=][ \t]*"?)(?!\[redacted)[^\s"',&)\]}]+/g,"$1$2[redacted]")}function $(e){return P(typeof e=="string"?e:String(e)).slice(0,120)}var H={attempts:4,baseDelayMs:500,timeoutMs:3e4};function Y(e){if(!Number.isInteger(e.attempts)||e.attempts<1)throw new TypeError(`[@aiquants/auth-google-directory] retry.attempts must be a positive integer, received ${String(e.attempts)}`);if(!Number.isFinite(e.baseDelayMs)||e.baseDelayMs<0)throw new TypeError(`[@aiquants/auth-google-directory] retry.baseDelayMs must be a non-negative number, received ${String(e.baseDelayMs)}`);if(!Number.isFinite(e.timeoutMs)||e.timeoutMs<=0)throw new TypeError(`[@aiquants/auth-google-directory] retry.timeoutMs must be a positive number, received ${String(e.timeoutMs)}`)}async function C(e,t,n){let l=t.fetch??globalThis.fetch,o=t.retry??H;Y(o);let i=t.sleep??(h=>new Promise(r=>setTimeout(r,h))),f=t.random??Math.random,p=new k(`Google Directory request was never attempted: ${e}`,0,e);for(let h=1;h<=o.attempts;h++){try{let r;try{r=await t.getAccessToken()}catch(s){throw new S(`could not obtain an access token: ${P(s instanceof Error?s.message:String(s))}`)}let a=new AbortController,m=setTimeout(()=>a.abort(),o.timeoutMs),g,c;try{g=await l(e,{headers:{Authorization:`Bearer ${r}`,Accept:"application/json"},signal:a.signal}),c=await g.text()}finally{clearTimeout(m)}if(g.ok){let s;try{s=JSON.parse(c)}catch{throw new d(`Google Directory returned a non-JSON body (${P(c).slice(0,120)})`,e)}if(s===null||typeof s!="object"||Array.isArray(s))throw new d(`Google Directory returned ${Array.isArray(s)?"an array":String(s)} where a JSON object was expected`,e);return n(s,e)}else{let s=P(c).slice(0,500),u=B(s),w=new k(`Google Directory request failed: ${g.status} ${s}`,g.status,e,u);if(!I(g.status,u))throw w;p=w}}catch(r){if(r instanceof S||r instanceof d||r instanceof k&&!I(r.status,r.reason))throw r;p=r instanceof Error?r:new Error(String(r))}if(h<o.attempts){let r=o.baseDelayMs*2**(h-1);await i(r+Math.floor(f()*o.baseDelayMs))}}throw p}async function T(e,t){let n=[],l=new Set,o;for(let i=0;i<e.maxPages;i++){let{items:f,nextPageToken:p}=await C(e.buildUrl(o),t,e.extract);if(n.push(...f),!p)return n;if(l.has(p))throw new Error(`Google Directory paging revisited a page token; refusing to loop (${e.buildUrl(o)})`);l.add(p),o=p}throw new Error(`Google Directory paging exceeded ${e.maxPages} pages; refusing to report a partial member list`)}var Z="https://admin.googleapis.com/admin/directory/v1";function x(e){return typeof e=="string"?e.trim():""}function R(e){let t=e.pageSize??200,n=e.maxPages??200,l=(e.customerId??"my_customer").trim();if(!Number.isInteger(t)||t<1||t>200)throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(e.pageSize)}`);if(!Number.isInteger(n)||n<1)throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(e.maxPages)}`);if(l==="")throw new TypeError("[@aiquants/auth-google-directory] customerId must not be blank");return{async listGroups(){return(await T({buildUrl:i=>`${Z}/groups?customer=${encodeURIComponent(l)}&maxResults=${t}${i?`&pageToken=${encodeURIComponent(i)}`:""}`,extract:(i,f)=>{if(i.kind!=="admin#directory#groups")throw new d(`expected an admin#directory#groups document but the body carried kind=${$(i.kind)}`,f);let p=i.groups;if(p===void 0)return{items:[],nextPageToken:x(i.nextPageToken)||void 0};if(!Array.isArray(p))throw new d("expected groups to be an array",f);return{items:p,nextPageToken:x(i.nextPageToken)||void 0}},maxPages:n},e)).map(i=>{let f=x(i.id),p=x(i.email);if(f===""||p==="")return null;let h=Number(x(i.directMembersCount)||i.directMembersCount),r=Number.isInteger(h)&&h>=0?h:void 0;return{externalId:f,externalKey:p,name:x(i.name)||p,...r===void 0?{}:{memberCount:r},...x(i.description)===""?{}:{description:x(i.description)}}}).filter(i=>i!==null)}}}var D="https://admin.googleapis.com/admin/directory/v1",E="https://cloudidentity.googleapis.com/v1",M={person:new Set(["USER","EXTERNAL"]),group:new Set(["GROUP"])},F=new Set(["ACTIVE",""]),W=new Set(["SUSPENDED","ARCHIVED","INVITED","PENDING"]);function N(e){let t=e.trim();if(!/^[A-Za-z0-9][A-Za-z0-9._%+-]*(@[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,})?$/.test(t))throw new TypeError(`[@aiquants/auth-google-directory] externalId must be a plain identifier or address, received ${JSON.stringify(e)}`);return t}var y=e=>typeof e=="string"?e.trim():"";function V(e){let t=e.pageSize??200,n=e.maxPages??200;if(!Number.isInteger(t)||t<1||t>200)throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(e.pageSize)}`);if(!Number.isInteger(n)||n<1)throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(e.maxPages)}`);function l(r,a,m){if(y(a.kind)!==r)throw new d(`expected a ${r} document but the body carried kind=${JSON.stringify(a.kind??null)}; refusing to treat it as an empty result`,m);return a}function o(r,a,m){let g=Object.keys(a).filter(c=>!r.includes(c));if(g.length>0)throw new d(`body carried unexpected keys (${g.join(", ")}); refusing to treat it as an empty result`,m);return a}function i(r,a){if(r==="")throw new d("Google Directory returned a member row without an address; refusing to report an incomplete member list",D);return a?{email:r,externalUserId:a}:{email:r}}async function f(r){let a=N(r);try{let m=await C(`${D}/groups/${encodeURIComponent(a)}`,e,(s,u)=>l("admin#directory#group",s,u)),g=y(m.id),c=y(m.email);if(g===""||c==="")throw new d(`Google Directory returned an incomplete group resource for ${a}`,`${D}/groups/${a}`);return{externalId:g,externalKey:c,name:y(m.name)||c,...y(m.description)?{description:y(m.description)}:{}}}catch(m){if(m instanceof k&&m.status===404)return null;throw m}}async function p(r){let a=N(r.externalId),m=await T({buildUrl:c=>`${D}/groups/${encodeURIComponent(a)}/members?maxResults=${t}${c?`&pageToken=${encodeURIComponent(c)}`:""}`,extract:(c,s)=>{let u=l("admin#directory#members",c,s);return{items:u.members??[],nextPageToken:y(u.nextPageToken)||void 0}},maxPages:n},e),g=[];for(let c of m){let s=y(c.type)||"USER";if(M.group.has(s))continue;if(!M.person.has(s))throw new d(`Google Directory reported member type ${JSON.stringify(s)}, which this adapter cannot classify; refusing to guess whether it is a person`,D);let u=y(c.status);if(!W.has(u)){if(!F.has(u))throw new d(`Google Directory reported member status ${JSON.stringify(u)}, which this adapter cannot classify`,D);g.push(i(y(c.email),y(c.id)||void 0))}}return g}async function h(r){let a=`${E}/groups:lookup?groupKey.id=${encodeURIComponent(r.externalKey)}`,m=await C(a,e,(u,w)=>o(["name"],u,w)),g=y(m.name);if(g==="")throw new d(`Cloud Identity did not return a resource name for ${r.externalKey}`,a);if(!/^groups\/[A-Za-z0-9_-]+$/.test(g))throw new d(`Cloud Identity returned a resource name this adapter will not put in a URL: ${JSON.stringify(g)}`,a);let c=await T({buildUrl:u=>`${E}/${g}/memberships:searchTransitiveMemberships?pageSize=${t}${u?`&pageToken=${encodeURIComponent(u)}`:""}`,extract:(u,w)=>{let b=o(["memberships","nextPageToken"],u,w);return{items:b.memberships??[],nextPageToken:y(b.nextPageToken)||void 0}},maxPages:n},e),s=[];for(let u of c){let w=y(u.member);if(w.startsWith("groups/"))continue;if(!w.startsWith("users/"))throw new d(`Cloud Identity reported a member resource ${JSON.stringify(w)}, which this adapter cannot classify; refusing to guess whether it is a person`,E);let b=Array.isArray(u.preferredMemberKey)?u.preferredMemberKey:[];if(b.length!==1)throw new d(`Cloud Identity reported ${b.length} member keys for ${w}; refusing to pick one`,E);if(y(b[0]?.namespace)!=="")throw new d(`Cloud Identity reported an external-identity member (${y(b[0]?.namespace)}); its id is not an address`,E);let G=w.slice(6).trim();if(G==="")throw new d(`Cloud Identity reported an empty user id in ${JSON.stringify(w)}`,E);s.push(i(y(b[0]?.id),G))}return s}return{getGroup:f,async listGroupMembers(r,a){return a==="direct"?p(r):h(r)}}}var v=["https://www.googleapis.com/auth/admin.directory.group.readonly","https://www.googleapis.com/auth/admin.directory.group.member.readonly"],O="https://www.googleapis.com/auth/cloud-identity.groups.readonly";function X(e){let t=e.filter(n=>!n.endsWith(".readonly"));if(t.length>0)throw new TypeError(`[@aiquants/auth-google-directory] only read-only scopes are allowed, received ${t.join(", ")}`)}function Q(e){let t=e.subject.trim();if(t==="")throw new TypeError("[@aiquants/auth-google-directory] subject must be a non-empty Workspace user to impersonate");let n=[...v,...e.includeNestedScope?[O]:[],...e.additionalScopes??[]];X(n);let l=e.createClient({subject:t,scopes:n});return async()=>{let{token:o}=await l.getAccessToken();if(!o)throw new Error(`[@aiquants/auth-google-directory] could not obtain an access token for ${t}`);return o}}0&&(module.exports={GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE,GOOGLE_DIRECTORY_READONLY_SCOPES,GoogleDirectoryAuthError,GoogleDirectoryHttpError,GoogleDirectoryShapeError,createDomainWideDelegationTokenProvider,createGoogleDirectoryCatalog,createGoogleDirectoryProvider,describeUntrusted,fetchAllPages,fetchJson});
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/http.ts","../src/catalog.ts","../src/provider.ts","../src/token.ts"],"sourcesContent":["/**\n * @module @aiquants/auth-google-directory\n * @description Google Workspace implementation of the `DirectoryProvider` port.\n * `DirectoryProvider` ポートの Google Workspace 実装。\n *\n * このリポジトリで \"Google\" という語を知る唯一のパッケージである。ほかのどのパッケージも\n * provider 非依存のまま保たれ、Google Workspace を使わない配備はこれを結線しなければよい。\n */\n\nexport { createGoogleDirectoryCatalog, type DirectoryCatalogGroup, type GoogleDirectoryCatalogConfig } from \"./catalog\"\nexport * from \"./http\"\nexport * from \"./provider\"\nexport * from \"./token\"\n","/**\n * @module http\n * @description Authenticated JSON GET with bounded retries, used by the Google adapters.\n * Google アダプタが使う、認証付き JSON GET (再試行は有界)。\n *\n * **失敗を空配列へ倒さないことが本モジュールの要点である。** 呼び出し側は返ってきた一覧を\n * 「上流の全て」と解釈して差分を削除するため、取得できなかったことを「0 件だった」と報告すると、\n * その差分がそのまま大量剥奪になる。取れなければ必ず throw する。\n *\n * ⚠️ 「取れなかった」には **本文の形が想定と違う** 場合も含む。企業プロキシは遮断や\n * セッション切れを HTTP 200 + JSON で返すことがあり、その本文には当然 `members` が無い。\n * 型を主張するだけの `as T` では、その応答が「空のグループ」と区別できない。\n */\n\n/**\n * Minimal fetch shape, injected so the adapter is testable without a network.\n * 注入可能な fetch 面。\n *\n * メソッドも本文も渡せない形にしてある。読み取り専用であることを「送っていない」ではなく\n * 「送れない」で担保するためであり、ホスト側の実装にも書込可能な継ぎ目を作らせない。\n */\nexport type FetchLike = (\n url: string,\n init?: { headers?: Record<string, string>; signal?: AbortSignal },\n) => Promise<{\n ok: boolean\n status: number\n text(): Promise<string>\n}>\n\n/** Bounded retry policy. 有界な再試行方針。 */\nexport type RetryPolicy = {\n /** Total attempts including the first. 初回を含む試行回数。 */\n attempts: number\n /** Base delay; grows exponentially. 基準待機時間 (指数的に伸びる)。 */\n baseDelayMs: number\n /**\n * Deadline for one attempt. 1 回の試行の制限時間。\n *\n * 試行回数を有界にしても、1 回の試行が終わらなければ実行は止まったままになる。同期は\n * リンクを逐次処理するため、吊られた 1 本がテナント全体の同期を無期限に止める。\n */\n timeoutMs: number\n}\n\n/** Error carrying the upstream status, so callers can tell \"denied\" from \"unavailable\". 上流の状態を運ぶ例外。 */\nexport class GoogleDirectoryHttpError extends Error {\n readonly status: number\n readonly url: string\n /** Google's machine-readable reason, when the body carried one. 本文が持っていた reason。 */\n readonly reason?: string\n constructor(message: string, status: number, url: string, reason?: string) {\n super(message)\n this.name = \"GoogleDirectoryHttpError\"\n this.status = status\n this.url = url\n if (reason) this.reason = reason\n }\n}\n\n/** Raised when the credential could not be obtained. 資格情報を取得できなかったことを表す例外。 */\nexport class GoogleDirectoryAuthError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"GoogleDirectoryAuthError\"\n }\n}\n\n/** Raised when a 200 response is not the document this endpoint returns. 応答が想定の文書でない。 */\nexport class GoogleDirectoryShapeError extends Error {\n readonly url: string\n constructor(message: string, url: string) {\n super(message)\n this.name = \"GoogleDirectoryShapeError\"\n this.url = url\n }\n}\n\n/**\n * Reasons Google returns with HTTP 403 that are rate limits, not permission failures.\n * Google が HTTP 403 とともに返す、権限ではなくレート制限を意味する reason。\n *\n * Directory API の「Limits and Quotas」は 3 つのレート制限エラーを挙げており、うち 2 つは 403\n * である。403 を一律に恒久扱いすると、クォータに触れた瞬間から実行が全滅し、しかも待てば直る\n * はずのものを待たずに諦めるため、その間ずっと陳腐化が進む。\n */\nconst RATE_LIMIT_REASONS: ReadonlySet<string> = new Set([\"userRateLimitExceeded\", \"quotaExceeded\", \"rateLimitExceeded\", \"backendError\"])\n\n/** Extracts Google's machine-readable reason from an error body. 本文から reason を取り出す。 */\nfunction extractReason(body: string): string | undefined {\n try {\n const parsed = JSON.parse(body) as { error?: { errors?: Array<{ reason?: string }>; status?: string } }\n return parsed.error?.errors?.[0]?.reason ?? parsed.error?.status\n } catch {\n return undefined\n }\n}\n\n/** Whether waiting could plausibly make this attempt succeed. 待てば直る見込みがあるか。 */\nfunction isTransient(status: number, reason: string | undefined): boolean {\n if (status === 429 || status === 408 || status >= 500) return true\n // 403 はレート制限と権限不足の両方に使われる。reason を見ないと区別できない\n return status === 403 && reason !== undefined && RATE_LIMIT_REASONS.has(reason)\n}\n\n/** Hides a bearer token that an intermediary may have echoed into the body. 本文中の資格情報を伏せる。 */\nconst redact = (body: string): string => body.replace(/Bearer\\s+[\\w.\\-~+/]+=*/gi, \"Bearer [redacted]\")\n\n/** Configuration shared by the Google API clients. Google API クライアント共通の構成。 */\nexport type GoogleHttpConfig = {\n /** Supplies a bearer token for each call. 呼び出しごとに Bearer トークンを供給する処理。 */\n getAccessToken: () => Promise<string>\n fetch?: FetchLike\n retry?: RetryPolicy\n /** Injected so tests do not actually wait. テストが実際に待たないよう注入する。 */\n sleep?: (ms: number) => Promise<void>\n /** Injected so backoff jitter is reproducible in tests. ジッタを再現可能にするため注入する。 */\n random?: () => number\n}\n\nconst defaultRetry: RetryPolicy = { attempts: 4, baseDelayMs: 500, timeoutMs: 30_000 }\n\n/** Rejects a retry policy that cannot bound anything. 何も有界にしない再試行方針を拒否する。 */\nfunction assertRetry(retry: RetryPolicy): void {\n if (!Number.isInteger(retry.attempts) || retry.attempts < 1) throw new TypeError(`[@aiquants/auth-google-directory] retry.attempts must be a positive integer, received ${String(retry.attempts)}`)\n if (!Number.isFinite(retry.baseDelayMs) || retry.baseDelayMs < 0) throw new TypeError(`[@aiquants/auth-google-directory] retry.baseDelayMs must be a non-negative number, received ${String(retry.baseDelayMs)}`)\n if (!Number.isFinite(retry.timeoutMs) || retry.timeoutMs <= 0) throw new TypeError(`[@aiquants/auth-google-directory] retry.timeoutMs must be a positive number, received ${String(retry.timeoutMs)}`)\n}\n\n/**\n * Performs an authenticated JSON GET, retrying transient failures a bounded number of times.\n * 認証付き JSON GET を行い、一時的な失敗のみ有界回数だけ再試行する処理。\n *\n * @param url Absolute request URL. 絶対 URL。\n * @param config Token supplier and injected collaborators. トークン供給と注入した協調要素。\n * @param assertShape Rejects a 200 whose body is not this endpoint's document. 応答形の検証。\n * @returns The parsed JSON body. 解析済み JSON ボディ。\n * @throws {GoogleDirectoryHttpError} When the request ultimately failed. 最終的に失敗した場合。\n * @throws {GoogleDirectoryShapeError} When the body is not the expected document. 想定外の本文。\n * @throws {GoogleDirectoryAuthError} When no token could be obtained. トークンを取得できない場合。\n */\nexport async function fetchJson<T>(url: string, config: GoogleHttpConfig, assertShape: (body: Record<string, unknown>, url: string) => T): Promise<T> {\n const doFetch = config.fetch ?? (globalThis.fetch as unknown as FetchLike)\n const retry = config.retry ?? defaultRetry\n assertRetry(retry)\n const sleep = config.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n const random = config.random ?? Math.random\n\n let lastError: Error = new GoogleDirectoryHttpError(`Google Directory request was never attempted: ${url}`, 0, url)\n for (let attempt = 1; attempt <= retry.attempts; attempt++) {\n try {\n let token: string\n try {\n token = await config.getAccessToken()\n } catch (error) {\n // 資格情報の失敗は待っても直らない。再試行に混ぜると、設定ミスの発覚が遅れるだけ\n throw new GoogleDirectoryAuthError(`could not obtain an access token: ${error instanceof Error ? error.message : String(error)}`)\n }\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), retry.timeoutMs)\n let response: Awaited<ReturnType<FetchLike>>\n try {\n response = await doFetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: \"application/json\" }, signal: controller.signal })\n } finally {\n clearTimeout(timer)\n }\n\n if (!response.ok) {\n const body = redact((await response.text()).slice(0, 500))\n const reason = extractReason(body)\n const error = new GoogleDirectoryHttpError(`Google Directory request failed: ${response.status} ${body}`, response.status, url, reason)\n // 恒久的な失敗は即座に諦める。待っても直らないうえ、待つ分だけ陳腐化が進む\n if (!isTransient(response.status, reason)) throw error\n lastError = error\n } else {\n const raw = await response.text()\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n // 200 だが JSON ですらない。中間装置が差し込んだ応答であり、待っても直らない\n throw new GoogleDirectoryShapeError(`Google Directory returned a non-JSON body (${raw.slice(0, 120)})`, url)\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new GoogleDirectoryShapeError(`Google Directory returned ${Array.isArray(parsed) ? \"an array\" : String(parsed)} where a JSON object was expected`, url)\n }\n return assertShape(parsed as Record<string, unknown>, url)\n }\n } catch (error) {\n if (error instanceof GoogleDirectoryAuthError || error instanceof GoogleDirectoryShapeError) throw error\n if (error instanceof GoogleDirectoryHttpError && !isTransient(error.status, error.reason)) throw error\n lastError = error instanceof Error ? error : new Error(String(error))\n }\n if (attempt < retry.attempts) {\n // ジッタを入れる。入れないと、同時に弾かれた呼び出しが揃って同じ時刻に再送し、\n // レート制限を同じ形で踏み直す\n const backoff = retry.baseDelayMs * 2 ** (attempt - 1)\n await sleep(backoff + Math.floor(random() * retry.baseDelayMs))\n }\n }\n throw lastError\n}\n\n/** How a paginated listing is read. ページング API の読み方。 */\nexport type PagedReader<TItem> = {\n /** Builds the URL for a page token. ページトークンから URL を組み立てる処理。 */\n buildUrl: (pageToken: string | undefined) => string\n /** Pulls the items and the next token out of one page. 1 ページから要素と次トークンを取り出す。 */\n extract: (body: Record<string, unknown>, url: string) => { items: TItem[]; nextPageToken?: string }\n /**\n * Upper bound so a broken cursor cannot loop forever. 壊れたカーソルで無限ループしない上限。\n *\n * 同時に、このアダプタが扱えるグループの大きさの上限でもある。到達したら例外になるため、\n * 大きなグループを黙って切り捨てることはない。\n */\n maxPages: number\n}\n\n/**\n * Walks every page of a paginated list endpoint. ページング API の全ページを辿る処理。\n *\n * ⚠️ **途中で失敗したら例外を送出し、それまでに集めた分は捨てる。** 部分的な一覧を返すと、\n * 呼び出し側はそれを「上流の全て」と解釈して残りを削除する。\n *\n * @param reader How to build, parse and bound the listing. 一覧の組み立て・解析・上限。\n * @param config Token supplier and injected collaborators. トークン供給と注入した協調要素。\n * @returns Every item across every page. 全ページの全要素。\n * @throws {Error} On any page failure, a cursor that revisits a token, or the page bound.\n * ページ失敗・トークンの再訪・上限超過のいずれでも throw。\n */\nexport async function fetchAllPages<TItem>(reader: PagedReader<TItem>, config: GoogleHttpConfig): Promise<TItem[]> {\n const all: TItem[] = []\n // 直前のトークンとの比較だけでは A→B→A→B の循環を検出できず、上限まで往復してから落ちる。\n // レート制限のある API に対して 200 往復を空費するのは、それ自体が次の失敗の原因になる\n const seenTokens = new Set<string>()\n let pageToken: string | undefined\n for (let page = 0; page < reader.maxPages; page++) {\n const { items, nextPageToken } = await fetchJson(reader.buildUrl(pageToken), config, reader.extract)\n all.push(...items)\n if (!nextPageToken) return all\n if (seenTokens.has(nextPageToken)) throw new Error(`Google Directory paging revisited a page token; refusing to loop (${reader.buildUrl(pageToken)})`)\n seenTokens.add(nextPageToken)\n pageToken = nextPageToken\n }\n throw new Error(`Google Directory paging exceeded ${reader.maxPages} pages; refusing to report a partial member list`)\n}\n","/**\n * @module catalog\n * @description Lists the domain's groups so an admin surface can offer them for selection.\n * ドメインのグループを列挙し、管理面が選択肢として提示できるようにするモジュール。\n *\n * ⚠️ **同期には使わない。** 同期は「連携済みの 1 グループの所属」を読む ({@link createGoogleDirectoryProvider})。\n * こちらは「ドメインにどんなグループがあるか」を読む。用途が違えば必要な権限も失敗の意味も違うため、\n * ポートを分けてある — 同期ジョブは候補一覧を必要とせず、管理面はメンバー一覧を必要としない。\n *\n * ⚠️ **これは利便のための読み取りである。** 失敗しても管理面は開かなければならない。呼び出し側は\n * 例外を「選択欄を出さない」状態へ写すこと。ここで既定値へ倒す (空配列を返す) と、上流に\n * グループが 1 件も無いことと、上流へ届かないことが区別できなくなる。\n */\nimport { fetchAllPages, type GoogleHttpConfig } from \"./http\"\n\n/** Admin SDK Directory API v1. */\nconst DIRECTORY_BASE = \"https://admin.googleapis.com/admin/directory/v1\"\n\n/** One group as the catalog reports it. 候補一覧が報告するグループ 1 件。 */\nexport type DirectoryCatalogGroup = {\n /** Stable upstream identifier. 上流の安定識別子。 */\n externalId: string\n /** Operator-facing key such as the group address. 運用者向けキー (グループアドレス等)。 */\n externalKey: string\n name: string\n /** Members the directory reports for the group. 上流が報告するメンバー数。 */\n memberCount?: number\n description?: string\n}\n\n/** Configuration of the catalog reader. 候補一覧リーダーの構成。 */\nexport type GoogleDirectoryCatalogConfig = GoogleHttpConfig & {\n /**\n * Customer whose groups are listed. 列挙対象の顧客。\n *\n * 既定の `my_customer` は「借用先ユーザーが属する顧客」を指す。複数顧客を扱う配備だけが\n * 明示する必要がある。\n */\n customerId?: string\n /** Page size, 1..200 (the Admin SDK maximum). ページ長 (Admin SDK の上限は 200)。 */\n pageSize?: number\n /** Page limit, so a runaway cursor cannot loop forever. ページ数上限 (暴走の歯止め)。 */\n maxPages?: number\n}\n\n/**\n * Reads a string field, treating anything else as absent. 文字列項目を読む処理 (他の型は不在扱い)。\n *\n * @param value Raw field value. 生の項目値。\n * @returns The trimmed string, or `\"\"`. 前後の空白を除いた文字列、または空文字。\n */\nfunction str(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\"\n}\n\n/**\n * Creates the catalog reader. 候補一覧リーダーを生成する処理。\n *\n * @param config Transport and paging configuration. トランスポートとページングの構成。\n * @returns An object exposing `listGroups`. `listGroups` を持つオブジェクト。\n * @throws {TypeError} When the paging configuration is unusable. ページング構成が使えない場合。\n */\nexport function createGoogleDirectoryCatalog(config: GoogleDirectoryCatalogConfig): { listGroups: () => Promise<DirectoryCatalogGroup[]> } {\n const pageSize = config.pageSize ?? 200\n const maxPages = config.maxPages ?? 200\n const customerId = (config.customerId ?? \"my_customer\").trim()\n // 既定へ倒さず結線時点で落とす。0 を渡すと Admin SDK の最小値 1 を下回り、毎回 400 になる\n if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 200) throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(config.pageSize)}`)\n if (!Number.isInteger(maxPages) || maxPages < 1) throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(config.maxPages)}`)\n if (customerId === \"\") throw new TypeError(\"[@aiquants/auth-google-directory] customerId must not be blank\")\n\n return {\n async listGroups(): Promise<DirectoryCatalogGroup[]> {\n const rows = await fetchAllPages<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown; directMembersCount?: unknown }>(\n {\n buildUrl: (pageToken) => `${DIRECTORY_BASE}/groups?customer=${encodeURIComponent(customerId)}&maxResults=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n // `kind` を要求するのは、企業プロキシが遮断やセッション切れを HTTP 200 + JSON で\n // 返すためである。その本文には `groups` が無いので、検証しないと「グループが\n // 1 件も無いドメイン」と区別できない\n if (body.kind !== \"admin#directory#groups\") throw new Error(`Google Directory returned an unexpected document for ${url}: kind=${String(body.kind)}`)\n const groups = body.groups\n // グループが 1 件も無いドメインでは `groups` が現れない。空配列として扱う\n if (groups === undefined) return { items: [], nextPageToken: str(body.nextPageToken) || undefined }\n if (!Array.isArray(groups)) throw new Error(`Google Directory returned a non-array group list for ${url}`)\n return { items: groups as Array<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown; directMembersCount?: unknown }>, nextPageToken: str(body.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n return rows\n .map((row) => {\n const externalId = str(row.id)\n const externalKey = str(row.email)\n // 識別子かアドレスを欠く行は選択肢にならない。落とさず残すと、選んだ瞬間に失敗する\n if (externalId === \"\" || externalKey === \"\") return null\n // `directMembersCount` は文字列で返ることがある。数として読めないなら伏せる\n const rawCount = Number(str(row.directMembersCount) || row.directMembersCount)\n const memberCount = Number.isInteger(rawCount) && rawCount >= 0 ? rawCount : undefined\n return {\n externalId,\n externalKey,\n name: str(row.name) || externalKey,\n ...(memberCount === undefined ? {} : { memberCount }),\n ...(str(row.description) === \"\" ? {} : { description: str(row.description) }),\n }\n })\n .filter((row): row is DirectoryCatalogGroup => row !== null)\n },\n }\n}\n","/**\n * @module provider\n * @description Google Workspace implementation of the `DirectoryProvider` port.\n * `DirectoryProvider` ポートの Google Workspace 実装。\n *\n * 直接所属は Admin SDK Directory、入れ子を展開した所属は Cloud Identity から取る。**書込\n * メソッドは 1 つも持たない** — 同期が上流を書き換える経路を、このパッケージに存在させない。\n *\n * 資格情報はトークン供給関数として注入する。おかげで本モジュールは Google の SDK に一切\n * 依存せず、ネットワークもトークンも無しで全分岐をテストできる。\n */\nimport type { AuthDirectoryMembershipMode, DirectoryGroup, DirectoryMember, DirectoryProvider } from \"@aiquants/auth-directory-core\"\nimport { fetchAllPages, fetchJson, GoogleDirectoryHttpError, GoogleDirectoryShapeError, type GoogleHttpConfig } from \"./http\"\n\nconst DIRECTORY_BASE = \"https://admin.googleapis.com/admin/directory/v1\"\nconst CLOUD_IDENTITY_BASE = \"https://cloudidentity.googleapis.com/v1\"\n\n/** Configuration of the Google directory provider. Google ディレクトリ provider の構成。 */\nexport type GoogleDirectoryConfig = GoogleHttpConfig & {\n /**\n * Page size for member listings. メンバー一覧のページサイズ。\n *\n * Admin SDK の `maxResults` は 1..200。Cloud Identity の `pageSize` は既定 200・最大 1000 で\n * 上限が違うため、両者で共有できる範囲に制限する。\n */\n pageSize?: number\n /**\n * Page bound, and therefore the largest group this adapter will report. ページ上限。\n *\n * 既定 200 ページ × 200 件 = 40,000 人。超えるグループは切り捨てではなく例外になる。\n * 実際にそれより大きなグループを扱う配備は、ここを明示的に引き上げること。\n */\n maxPages?: number\n}\n\n/** Member kinds the Admin SDK reports. Admin SDK が報告するメンバー種別。 */\nconst MEMBER_TYPES = { person: new Set([\"USER\", \"EXTERNAL\"]), group: new Set([\"GROUP\"]) }\n/** Member statuses that mean the account can actually be used. 実際に使えることを意味する状態。 */\nconst USABLE_STATUSES = new Set([\"ACTIVE\", \"\"])\n/** Statuses that mean the account exists but cannot be used. 存在するが使えないことを意味する状態。 */\nconst UNUSABLE_STATUSES = new Set([\"SUSPENDED\", \"ARCHIVED\", \"INVITED\", \"PENDING\"])\n\n/**\n * Rejects a group identifier that would change the meaning of the request URL.\n * リクエスト URL の意味を変えてしまうグループ識別子を拒否する処理。\n *\n * 否定リストではなく **肯定的な文字クラス** で検証する。以前は `/` `?` `#` だけを弾いており、\n * `..` が通っていた。`encodeURIComponent(\"..\")` は `..` のままなので、URL 解析器が経路を\n * 畳んで別のエンドポイントを叩き、Google が 404 を返し、呼び出し側が「上流でグループが消えた」と\n * 解釈してローカルのグループを無効化していた。\n *\n * @throws {TypeError} When the identifier is not a plain id or address. 素の ID・アドレスでない場合。\n */\nfunction assertExternalId(externalId: string): string {\n const trimmed = externalId.trim()\n // Google のグループ ID は英数字、アドレスはメール形式。どちらもこの集合に収まる\n if (!/^[A-Za-z0-9][A-Za-z0-9._%+-]*(@[A-Za-z0-9][A-Za-z0-9.-]*\\.[A-Za-z]{2,})?$/.test(trimmed)) {\n throw new TypeError(`[@aiquants/auth-google-directory] externalId must be a plain identifier or address, received ${JSON.stringify(externalId)}`)\n }\n return trimmed\n}\n\n/** Reads a string field, treating absent and non-string alike. 文字列項目の読み取り。 */\nconst str = (v: unknown): string => (typeof v === \"string\" ? v.trim() : \"\")\n\n/**\n * Creates a read-only Google Workspace directory provider.\n * 読み取り専用の Google Workspace ディレクトリ provider を生成する処理。\n *\n * @param config Token supplier, injected fetch/sleep, retry policy and bounds. 構成。\n * @returns The provider. provider。\n * @throws {TypeError} When a bound is outside its usable range. 上限が使用可能な範囲外の場合。\n */\nexport function createGoogleDirectoryProvider(config: GoogleDirectoryConfig): DirectoryProvider {\n const pageSize = config.pageSize ?? 200\n const maxPages = config.maxPages ?? 200\n // 既定へ倒さず結線時点で落とす。0 を渡すと Admin SDK の最小値 1 を下回り、毎回 400 になる\n if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 200) throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(config.pageSize)}`)\n if (!Number.isInteger(maxPages) || maxPages < 1) throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(config.maxPages)}`)\n\n /**\n * Refuses a body that is not the document this endpoint returns.\n * このエンドポイントが返す文書でない本文を拒否する処理。\n *\n * `kind` を要求するのは、企業プロキシが遮断やセッション切れを HTTP 200 + JSON で返すため\n * である。その本文には `members` が無いので、検証しないと「空のグループ」と区別できず、\n * 全員が削除対象になる。Admin SDK は空のグループでも `kind` を必ず返す。\n */\n function assertKind<T>(expected: string, body: Record<string, unknown>, url: string): T {\n if (str(body.kind) !== expected) {\n throw new GoogleDirectoryShapeError(`expected a ${expected} document but the body carried kind=${JSON.stringify(body.kind ?? null)}; refusing to treat it as an empty result`, url)\n }\n return body as T\n }\n\n /**\n * Refuses a Cloud Identity body carrying keys this endpoint never returns.\n * このエンドポイントが返さないキーを持つ Cloud Identity の本文を拒否する処理。\n *\n * Cloud Identity には `kind` に相当する判別子が無い。代わりに、想定外のキーを持つ本文を\n * 拒否する。遮断ページは `error` や `message` を持つため、これで区別できる。\n */\n function assertKeys<T>(allowed: readonly string[], body: Record<string, unknown>, url: string): T {\n const unexpected = Object.keys(body).filter((k) => !allowed.includes(k))\n if (unexpected.length > 0) {\n throw new GoogleDirectoryShapeError(`body carried unexpected keys (${unexpected.join(\", \")}); refusing to treat it as an empty result`, url)\n }\n return body as T\n }\n\n /**\n * Converts an upstream row into a member, refusing rows it cannot classify.\n * 上流の行をメンバーへ変換する処理。分類できない行は拒否する。\n *\n * 黙って捨てるとメンバーが 1 人減り、その差分が削除として実行される。\n */\n function toMember(email: string, externalUserId?: string): DirectoryMember {\n if (email === \"\") throw new GoogleDirectoryShapeError(\"Google Directory returned a member row without an address; refusing to report an incomplete member list\", DIRECTORY_BASE)\n return externalUserId ? { email, externalUserId } : { email }\n }\n\n /**\n * Fetches one group's metadata. 1 グループのメタデータを取得する処理。\n *\n * `this` を使わずクロージャとして持つ。オブジェクトのメソッドとして `this.getGroup` を\n * 呼ぶと、分割代入やコールバック経由で渡された時点で実行時に落ちる。\n */\n async function getGroup(externalId: string): Promise<DirectoryGroup | null> {\n const id = assertExternalId(externalId)\n try {\n const group = await fetchJson<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown }>(`${DIRECTORY_BASE}/groups/${encodeURIComponent(id)}`, config, (body, url) => assertKind(\"admin#directory#group\", body, url))\n const stableId = str(group.id)\n const email = str(group.email)\n if (stableId === \"\" || email === \"\") throw new GoogleDirectoryShapeError(`Google Directory returned an incomplete group resource for ${id}`, `${DIRECTORY_BASE}/groups/${id}`)\n return { externalId: stableId, externalKey: email, name: str(group.name) || email, ...(str(group.description) ? { description: str(group.description) } : {}) }\n } catch (error) {\n // 404 だけが「上流に無い」。他の失敗を null に畳むと、到達できないことが\n // 「グループが消えた」に化けて、生きているグループが無効化されてしまう\n if (error instanceof GoogleDirectoryHttpError && error.status === 404) return null\n throw error\n }\n }\n\n /** Direct membership via the Admin SDK. Admin SDK による直接所属。 */\n async function listDirectMembers(group: DirectoryGroup): Promise<DirectoryMember[]> {\n const id = assertExternalId(group.externalId)\n const rows = await fetchAllPages<{ id?: unknown; email?: unknown; type?: unknown; status?: unknown }>(\n {\n buildUrl: (pageToken) => `${DIRECTORY_BASE}/groups/${encodeURIComponent(id)}/members?maxResults=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n const page = assertKind<{ members?: Array<Record<string, unknown>>; nextPageToken?: string }>(\"admin#directory#members\", body, url)\n return { items: page.members ?? [], nextPageToken: str(page.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n const members: DirectoryMember[] = []\n for (const row of rows) {\n const type = str(row.type) || \"USER\"\n // 入れ子グループ自身の行は「人」ではない。direct では展開しない約束なので落とす\n if (MEMBER_TYPES.group.has(type)) continue\n // 未知の種別を人として扱うのも、黙って落とすのも誤り。CUSTOMER (ドメイン全体) を\n // 落とすとグループが 0 人に見え、そのまま全員削除の plan になる\n if (!MEMBER_TYPES.person.has(type)) throw new GoogleDirectoryShapeError(`Google Directory reported member type ${JSON.stringify(type)}, which this adapter cannot classify; refusing to guess whether it is a person`, DIRECTORY_BASE)\n const status = str(row.status)\n // 外部メンバーは status が空で返ることがある。空を「使える」と読むのは、Google が\n // 使えない状態には必ず名前を付けるからである\n if (UNUSABLE_STATUSES.has(status)) continue\n if (!USABLE_STATUSES.has(status)) throw new GoogleDirectoryShapeError(`Google Directory reported member status ${JSON.stringify(status)}, which this adapter cannot classify`, DIRECTORY_BASE)\n members.push(toMember(str(row.email), str(row.id) || undefined))\n }\n return members\n }\n\n /** Nested membership flattened via Cloud Identity. Cloud Identity による入れ子展開所属。 */\n async function listTransitiveMembers(group: DirectoryGroup): Promise<DirectoryMember[]> {\n const lookupUrl = `${CLOUD_IDENTITY_BASE}/groups:lookup?groupKey.id=${encodeURIComponent(group.externalKey)}`\n const looked = await fetchJson<{ name?: unknown }>(lookupUrl, config, (body, url) => assertKeys([\"name\"], body, url))\n const resourceName = str(looked.name)\n if (resourceName === \"\") throw new GoogleDirectoryShapeError(`Cloud Identity did not return a resource name for ${group.externalKey}`, lookupUrl)\n // URL のパスへそのまま差し込む唯一の値なので、形を検査する。応答本文が汚染されたとき\n // に `..` が入ると、Bearer トークンを載せた要求が別のパスへ向かう\n if (!/^groups\\/[A-Za-z0-9_-]+$/.test(resourceName)) {\n throw new GoogleDirectoryShapeError(`Cloud Identity returned a resource name this adapter will not put in a URL: ${JSON.stringify(resourceName)}`, lookupUrl)\n }\n\n const rows = await fetchAllPages<{ preferredMemberKey?: unknown; member?: unknown; relationType?: unknown }>(\n {\n buildUrl: (pageToken) => `${CLOUD_IDENTITY_BASE}/${resourceName}/memberships:searchTransitiveMemberships?pageSize=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n const page = assertKeys<{ memberships?: Array<Record<string, unknown>>; nextPageToken?: string }>([\"memberships\", \"nextPageToken\"], body, url)\n return { items: page.memberships ?? [], nextPageToken: str(page.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n\n const members: DirectoryMember[] = []\n for (const row of rows) {\n const member = str(row.member)\n // 入れ子グループ自身も所属として返る。人として数えると、グループアドレスと同じ\n // ローカルユーザーが居た場合に親グループのロールを継承してしまう\n if (member.startsWith(\"groups/\")) continue\n if (!member.startsWith(\"users/\")) throw new GoogleDirectoryShapeError(`Cloud Identity reported a member resource ${JSON.stringify(member)}, which this adapter cannot classify; refusing to guess whether it is a person`, CLOUD_IDENTITY_BASE)\n const keys = Array.isArray(row.preferredMemberKey) ? (row.preferredMemberKey as Array<Record<string, unknown>>) : []\n if (keys.length !== 1) throw new GoogleDirectoryShapeError(`Cloud Identity reported ${keys.length} member keys for ${member}; refusing to pick one`, CLOUD_IDENTITY_BASE)\n // namespace 付きは外部 identity 由来で、id はアドレスではない。アドレスとして扱うと\n // 台帳に人でない文字列が入る\n if (str(keys[0]?.namespace) !== \"\") throw new GoogleDirectoryShapeError(`Cloud Identity reported an external-identity member (${str(keys[0]?.namespace)}); its id is not an address`, CLOUD_IDENTITY_BASE)\n // `users/<id>` の id を捨ててはならない。捨てると台帳の `external_user_id` が\n // 全行 NULL になり、「上流 ID 第一キー・アドレス第二キー」の防御が入れ子展開の\n // リンクでだけ無効化される — アドレスを引き継いだ後任者が前任者の所属を継承する\n const upstreamUserId = member.slice(\"users/\".length).trim()\n if (upstreamUserId === \"\") throw new GoogleDirectoryShapeError(`Cloud Identity reported an empty user id in ${JSON.stringify(member)}`, CLOUD_IDENTITY_BASE)\n members.push(toMember(str(keys[0]?.id), upstreamUserId))\n }\n return members\n }\n\n return {\n getGroup,\n async listGroupMembers(group: DirectoryGroup, mode: AuthDirectoryMembershipMode): Promise<DirectoryMember[]> {\n return mode === \"direct\" ? listDirectMembers(group) : listTransitiveMembers(group)\n },\n }\n}\n","/**\n * @module token\n * @description Domain-wide delegation token provider.\n * ドメイン全体の委任によるトークン供給。\n *\n * ⚠️ **借用先 (`subject`) には特権管理者を指定しないこと。** ドメイン全体の委任は「指定した\n * ユーザーとして振る舞う」機能であり、特権管理者を借用すると、この鍵は実質的に特権管理者の鍵に\n * なる。「グループ > 読み取り」だけを持つカスタム管理者ロールを割り当てた専用ユーザーを用意し、\n * それを指定すること。\n *\n * ⚠️ **スコープは readonly のみ。** 書込スコープを渡すと、このパッケージが読み取り専用である\n * という契約を、パッケージの外側で破ることになる。\n *\n * ただしこの検査が守れるのは、ここで組み立てるスコープ集合までである。**実際の境界は\n * `createClient` であり、呼び出し側は渡された scopes を無視して自前の JWT を作れる。** 読み取り\n * 専用であることは、このガードと、借用先ユーザーに与える管理者ロールの狭さの両方で担保する。\n */\nimport type { GoogleHttpConfig } from \"./http\"\n\n/** Read-only scopes this integration needs. 本連携が必要とする読み取り専用スコープ。 */\nexport const GOOGLE_DIRECTORY_READONLY_SCOPES = [\"https://www.googleapis.com/auth/admin.directory.group.readonly\", \"https://www.googleapis.com/auth/admin.directory.group.member.readonly\"] as const\n\n/** Additional read-only scope required to expand nested groups. 入れ子展開に要る追加スコープ。 */\nexport const GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE = \"https://www.googleapis.com/auth/cloud-identity.groups.readonly\"\n\n/** Minimal shape of `google-auth-library`'s JWT client. JWT クライアントの最小形。 */\nexport type JwtClientLike = {\n getAccessToken(): Promise<{ token?: string | null }>\n}\n\n/** Configuration of the delegation token provider. 委任トークン供給の構成。 */\nexport type DomainWideDelegationConfig = {\n /**\n * Builds the JWT client. Injected so this package does not depend on a Google SDK.\n * JWT クライアントを構築する処理。Google SDK への依存を持ち込まないため注入する。\n *\n * 呼び出し側の実装例:\n * `({ subject, scopes }) => new JWT({ keyFile, subject, scopes })`\n */\n createClient: (args: { subject: string; scopes: string[] }) => JwtClientLike\n /** Workspace user to impersonate. Must NOT be a super administrator. 借用先ユーザー。 */\n subject: string\n /** True when nested membership expansion is used anywhere. 入れ子展開を使う場合に true。 */\n includeNestedScope: boolean\n /**\n * Extra read-only scopes, when a deployment needs more than the defaults.\n * 既定に加えて要る読み取り専用スコープ (必要な配備のみ)。\n *\n * 受け取れるようにしてあるのは、読み取り専用の検査を **到達可能** にするためでもある。\n * 定数だけを検査する形では、検査は決して発火せず、契約を守っているように見えるだけになる。\n */\n additionalScopes?: readonly string[]\n}\n\n/** Rejects a scope that would grant more than reading. 読み取りを超える権限を与えるスコープを拒否する。 */\nfunction assertReadOnly(scopes: readonly string[]): void {\n const writable = scopes.filter((s) => !s.endsWith(\".readonly\"))\n if (writable.length > 0) {\n throw new TypeError(`[@aiquants/auth-google-directory] only read-only scopes are allowed, received ${writable.join(\", \")}`)\n }\n}\n\n/**\n * Creates a token supplier that impersonates a least-privileged directory reader.\n * 最小権限のディレクトリ読み取り専用ユーザーを借用するトークン供給を生成する処理。\n *\n * @param config Client factory, impersonated subject and nested-scope flag. 構成。\n * @returns A `getAccessToken` suitable for {@link GoogleHttpConfig}. トークン供給関数。\n * @throws {TypeError} When the subject is blank. 借用先が空の場合。\n *\n * トークンのキャッシュは `google-auth-library` の JWT クライアントが行う。ここで二重に\n * 貯めると、失効の扱いが 2 箇所に分かれて食い違う。\n */\nexport function createDomainWideDelegationTokenProvider(config: DomainWideDelegationConfig): GoogleHttpConfig[\"getAccessToken\"] {\n const subject = config.subject.trim()\n if (subject === \"\") throw new TypeError(\"[@aiquants/auth-google-directory] subject must be a non-empty Workspace user to impersonate\")\n const scopes = [...GOOGLE_DIRECTORY_READONLY_SCOPES, ...(config.includeNestedScope ? [GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE] : []), ...(config.additionalScopes ?? [])]\n assertReadOnly(scopes)\n const client = config.createClient({ subject, scopes })\n\n return async () => {\n const { token } = await client.getAccessToken()\n // トークンが取れないまま空文字で続行すると、401 が「メンバー 0 件」に化ける経路が生まれる\n if (!token) throw new Error(`[@aiquants/auth-google-directory] could not obtain an access token for ${subject}`)\n return token\n }\n}\n"],"mappings":"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","names":["index_exports","__export","GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE","GOOGLE_DIRECTORY_READONLY_SCOPES","GoogleDirectoryAuthError","GoogleDirectoryHttpError","GoogleDirectoryShapeError","createDomainWideDelegationTokenProvider","createGoogleDirectoryCatalog","createGoogleDirectoryProvider","fetchAllPages","fetchJson","__toCommonJS","GoogleDirectoryHttpError","message","status","url","reason","GoogleDirectoryAuthError","GoogleDirectoryShapeError","RATE_LIMIT_REASONS","extractReason","body","parsed","isTransient","redact","defaultRetry","assertRetry","retry","fetchJson","config","assertShape","doFetch","sleep","ms","random","lastError","attempt","token","error","controller","timer","response","raw","backoff","fetchAllPages","reader","all","seenTokens","pageToken","page","items","nextPageToken","DIRECTORY_BASE","str","value","createGoogleDirectoryCatalog","config","pageSize","maxPages","customerId","fetchAllPages","pageToken","body","url","groups","row","externalId","externalKey","rawCount","memberCount","DIRECTORY_BASE","CLOUD_IDENTITY_BASE","MEMBER_TYPES","USABLE_STATUSES","UNUSABLE_STATUSES","assertExternalId","externalId","trimmed","str","v","createGoogleDirectoryProvider","config","pageSize","maxPages","assertKind","expected","body","url","GoogleDirectoryShapeError","assertKeys","allowed","unexpected","k","toMember","email","externalUserId","getGroup","id","group","fetchJson","stableId","error","GoogleDirectoryHttpError","listDirectMembers","rows","fetchAllPages","pageToken","page","members","row","type","status","listTransitiveMembers","lookupUrl","looked","resourceName","member","keys","upstreamUserId","mode","GOOGLE_DIRECTORY_READONLY_SCOPES","GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE","assertReadOnly","scopes","writable","s","createDomainWideDelegationTokenProvider","config","subject","client","token"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/http.ts","../src/catalog.ts","../src/provider.ts","../src/token.ts"],"sourcesContent":["/**\n * @module @aiquants/auth-google-directory\n * @description Google Workspace implementation of the `DirectoryProvider` port.\n * `DirectoryProvider` ポートの Google Workspace 実装。\n *\n * このリポジトリで \"Google\" という語を知る唯一のパッケージである。ほかのどのパッケージも\n * provider 非依存のまま保たれ、Google Workspace を使わない配備はこれを結線しなければよい。\n */\n\nexport { createGoogleDirectoryCatalog, type DirectoryCatalogGroup, type GoogleDirectoryCatalogConfig } from \"./catalog\"\nexport * from \"./http\"\nexport * from \"./provider\"\nexport * from \"./token\"\n","/**\n * @module http\n * @description Authenticated JSON GET with bounded retries, used by the Google adapters.\n * Google アダプタが使う、認証付き JSON GET (再試行は有界)。\n *\n * **失敗を空配列へ倒さないことが本モジュールの要点である。** 呼び出し側は返ってきた一覧を\n * 「上流の全て」と解釈して差分を削除するため、取得できなかったことを「0 件だった」と報告すると、\n * その差分がそのまま大量剥奪になる。取れなければ必ず throw する。\n *\n * ⚠️ 「取れなかった」には **本文の形が想定と違う** 場合も含む。企業プロキシは遮断や\n * セッション切れを HTTP 200 + JSON で返すことがあり、その本文には当然 `members` が無い。\n * 型を主張するだけの `as T` では、その応答が「空のグループ」と区別できない。\n */\n\n/**\n * Minimal fetch shape, injected so the adapter is testable without a network.\n * 注入可能な fetch 面。\n *\n * メソッドも本文も渡せない形にしてある。読み取り専用であることを「送っていない」ではなく\n * 「送れない」で担保するためであり、ホスト側の実装にも書込可能な継ぎ目を作らせない。\n */\nexport type FetchLike = (\n url: string,\n init?: { headers?: Record<string, string>; signal?: AbortSignal },\n) => Promise<{\n ok: boolean\n status: number\n text(): Promise<string>\n}>\n\n/** Bounded retry policy. 有界な再試行方針。 */\nexport type RetryPolicy = {\n /** Total attempts including the first. 初回を含む試行回数。 */\n attempts: number\n /** Base delay; grows exponentially. 基準待機時間 (指数的に伸びる)。 */\n baseDelayMs: number\n /**\n * Deadline for one attempt. 1 回の試行の制限時間。\n *\n * 試行回数を有界にしても、1 回の試行が終わらなければ実行は止まったままになる。同期は\n * リンクを逐次処理するため、吊られた 1 本がテナント全体の同期を無期限に止める。\n */\n timeoutMs: number\n}\n\n/** Error carrying the upstream status, so callers can tell \"denied\" from \"unavailable\". 上流の状態を運ぶ例外。 */\nexport class GoogleDirectoryHttpError extends Error {\n readonly status: number\n readonly url: string\n /** Google's machine-readable reason, when the body carried one. 本文が持っていた reason。 */\n readonly reason?: string\n constructor(message: string, status: number, url: string, reason?: string) {\n super(message)\n this.name = \"GoogleDirectoryHttpError\"\n this.status = status\n this.url = url\n if (reason) this.reason = reason\n }\n}\n\n/** Raised when the credential could not be obtained. 資格情報を取得できなかったことを表す例外。 */\nexport class GoogleDirectoryAuthError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"GoogleDirectoryAuthError\"\n }\n}\n\n/** Raised when a 200 response is not the document this endpoint returns. 応答が想定の文書でない。 */\nexport class GoogleDirectoryShapeError extends Error {\n readonly url: string\n constructor(message: string, url: string) {\n super(message)\n this.name = \"GoogleDirectoryShapeError\"\n this.url = url\n }\n}\n\n/**\n * Reasons Google returns with HTTP 403 that are rate limits, not permission failures.\n * Google が HTTP 403 とともに返す、権限ではなくレート制限を意味する reason。\n *\n * Directory API の「Limits and Quotas」は 3 つのレート制限エラーを挙げており、うち 2 つは 403\n * である。403 を一律に恒久扱いすると、クォータに触れた瞬間から実行が全滅し、しかも待てば直る\n * はずのものを待たずに諦めるため、その間ずっと陳腐化が進む。\n */\nconst RATE_LIMIT_REASONS: ReadonlySet<string> = new Set([\"userRateLimitExceeded\", \"quotaExceeded\", \"rateLimitExceeded\", \"backendError\"])\n\n/** Extracts Google's machine-readable reason from an error body. 本文から reason を取り出す。 */\nfunction extractReason(body: string): string | undefined {\n try {\n const parsed = JSON.parse(body) as { error?: { errors?: Array<{ reason?: string }>; status?: string } }\n return parsed.error?.errors?.[0]?.reason ?? parsed.error?.status\n } catch {\n return undefined\n }\n}\n\n/** Whether waiting could plausibly make this attempt succeed. 待てば直る見込みがあるか。 */\nfunction isTransient(status: number, reason: string | undefined): boolean {\n if (status === 429 || status === 408 || status >= 500) return true\n // 403 はレート制限と権限不足の両方に使われる。reason を見ないと区別できない\n return status === 403 && reason !== undefined && RATE_LIMIT_REASONS.has(reason)\n}\n\n/**\n * Longest text the redaction rules will scan. 伏せ字規則が走査する最大長。\n *\n * ⚠️ 走査対象は **先に** 上限で切る。切らずに正規表現へ渡すと、開始マーカーだけを繰り返す本文\n * (企業プロキシが返す巨大な 502 など) で PEM 規則の遅延量指定子が末尾まで再走査を繰り返し、\n * 1 MB で 17 秒、4 MB で 2 分以上かかる。同期はリンクを逐次処理するため、汚染された応答\n * 1 本がテナント全体の同期を止める — 上流の応答長を信用してはならない。\n * JWT が 3 区間そろう長さは十分に確保してある。\n */\nconst MAX_REDACTED_LENGTH = 8192\n\n/**\n * Hides credentials an intermediary may have echoed into the text.\n * 中間装置が本文へ echo し得る資格情報を伏せる処理。\n *\n * @param text - Text that may carry credentials. 資格情報を含み得る文字列。\n * @returns The same text with credentials replaced. 資格情報を置き換えた文字列。\n */\nfunction redact(text: string): string {\n return (\n text\n .slice(0, MAX_REDACTED_LENGTH)\n .replace(/\\bBearer\\s+[\\w.\\-~+/]+=*/g, \"Bearer [redacted]\")\n // Basic 認証の資格情報も同じ経路 (プロキシの 407) で echo される\n .replace(/\\bBasic\\s+[A-Za-z0-9+/]+={0,2}/g, \"Basic [redacted]\")\n // 署名済み JWT (`eyJ` は `{\"` の base64url)。トークン取得の失敗は assertion を丸ごと\n // echo することがあり、その中身は秘密鍵で署名した主張そのものである。\n // ⚠️ 語境界を要求しない。上流やライブラリは区切りを入れずに値を連結することがあり\n // (`...tokenxeyJhbGci...`)、`\\b` を付けるとその形だけが素通りする\n .replace(/eyJ[\\w-]*\\.[\\w-]+\\.[\\w-]+/g, \"[redacted-jwt]\")\n // ⚠️ 鍵名を伴わない裸のトークンも伏せる。中間装置は `Bearer` を落として値だけを\n // echo することがあり、`ya29.` (アクセストークン) と `1//` (リフレッシュトークン)\n // はそのままで資格情報として使える。前置文字は限定しない — ログは括弧や角括弧で\n // 値を囲むため、区切り文字を列挙する形にすると囲まれた値だけが素通りする\n .replace(/ya29\\.[\\w.\\-~+/]+=*/g, \"[redacted-access-token]\")\n .replace(/1\\/\\/[\\w-]{10,}/g, \"[redacted-refresh-token]\")\n // PEM。鍵そのものが例外文へ載る経路は無いはずだが、無いことに依存しない。\n // 本体の長さは有界にする (無界の遅延量指定子は上の切り詰めが在っても二乗で効く)\n .replace(/-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----[\\s\\S]{0,4000}?-----END [A-Z ]{0,20}PRIVATE KEY-----/g, \"[redacted-private-key]\")\n // 終端マーカーを欠く (切り詰められた) 鍵も伏せる。上の規則は両端がそろわないと当たらず、\n // 切り詰めの結果として鍵材料の先頭が丸ごと残る\n .replace(/-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----[\\s\\S]{0,4000}/g, \"[redacted-private-key]\")\n // ⚠️ 鍵名の規則は **PEM の後** に置く。先に置くと `\"private_key\":\"-----BEGIN ...` の\n // 値に当たって `-----BEGIN` までを伏せ字へ置き換え、PEM 規則が要求する開始マーカーが\n // 消える — 鍵本体だけが平文で残る。伏せる規則どうしが打ち消し合う形は、何も伏せない\n // より悪い (伏せたつもりになる)。既に伏せた値へ二重に当てないよう先読みで除く。\n // JSON でもクエリ形式でも同じ鍵名で運ばれる。`client_secret` はトークン端点の本文で\n // `assertion` と隣り合って現れる。区切りに改行を含めない — 含めると鍵名で終わる行の\n // 次の語まで飲み込み、診断に必要な文言が消える\n .replace(/\\b((?:access|refresh|id)_token|assertion|client_secret|private_key)(\"?[ \\t]*[:=][ \\t]*\"?)(?!\\[redacted)[^\\s\"',&)\\]}]+/g, \"$1$2[redacted]\")\n )\n}\n\n/**\n * Renders an untrusted value for a diagnostic message. 信用できない値を診断文へ載せる形にする処理。\n *\n * ⚠️ 上流 (あるいは割り込んだ中間装置) が決めた値をそのまま文へ入れない。長さは相手が決められる\n * ため、20 KB の文言がログにも `sync_error` 列にも流れる。資格情報が混じっていれば伏せる。\n *\n * @param value - Whatever the body carried. 本文が運んできた値。\n * @returns A short, redacted rendering. 短く、伏せ字を通した表現。\n */\nexport function describeUntrusted(value: unknown): string {\n return redact(typeof value === \"string\" ? value : String(value)).slice(0, 120)\n}\n\n/** Configuration shared by the Google API clients. Google API クライアント共通の構成。 */\nexport type GoogleHttpConfig = {\n /** Supplies a bearer token for each call. 呼び出しごとに Bearer トークンを供給する処理。 */\n getAccessToken: () => Promise<string>\n fetch?: FetchLike\n retry?: RetryPolicy\n /** Injected so tests do not actually wait. テストが実際に待たないよう注入する。 */\n sleep?: (ms: number) => Promise<void>\n /** Injected so backoff jitter is reproducible in tests. ジッタを再現可能にするため注入する。 */\n random?: () => number\n}\n\nconst defaultRetry: RetryPolicy = { attempts: 4, baseDelayMs: 500, timeoutMs: 30_000 }\n\n/** Rejects a retry policy that cannot bound anything. 何も有界にしない再試行方針を拒否する。 */\nfunction assertRetry(retry: RetryPolicy): void {\n if (!Number.isInteger(retry.attempts) || retry.attempts < 1) throw new TypeError(`[@aiquants/auth-google-directory] retry.attempts must be a positive integer, received ${String(retry.attempts)}`)\n if (!Number.isFinite(retry.baseDelayMs) || retry.baseDelayMs < 0) throw new TypeError(`[@aiquants/auth-google-directory] retry.baseDelayMs must be a non-negative number, received ${String(retry.baseDelayMs)}`)\n if (!Number.isFinite(retry.timeoutMs) || retry.timeoutMs <= 0) throw new TypeError(`[@aiquants/auth-google-directory] retry.timeoutMs must be a positive number, received ${String(retry.timeoutMs)}`)\n}\n\n/**\n * Performs an authenticated JSON GET, retrying transient failures a bounded number of times.\n * 認証付き JSON GET を行い、一時的な失敗のみ有界回数だけ再試行する処理。\n *\n * @param url Absolute request URL. 絶対 URL。\n * @param config Token supplier and injected collaborators. トークン供給と注入した協調要素。\n * @param assertShape Rejects a 200 whose body is not this endpoint's document. 応答形の検証。\n * @returns The parsed JSON body. 解析済み JSON ボディ。\n * @throws {GoogleDirectoryHttpError} When the request ultimately failed. 最終的に失敗した場合。\n * @throws {GoogleDirectoryShapeError} When the body is not the expected document. 想定外の本文。\n * @throws {GoogleDirectoryAuthError} When no token could be obtained. トークンを取得できない場合。\n */\nexport async function fetchJson<T>(url: string, config: GoogleHttpConfig, assertShape: (body: Record<string, unknown>, url: string) => T): Promise<T> {\n const doFetch = config.fetch ?? (globalThis.fetch as unknown as FetchLike)\n const retry = config.retry ?? defaultRetry\n assertRetry(retry)\n const sleep = config.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n const random = config.random ?? Math.random\n\n let lastError: Error = new GoogleDirectoryHttpError(`Google Directory request was never attempted: ${url}`, 0, url)\n for (let attempt = 1; attempt <= retry.attempts; attempt++) {\n try {\n let token: string\n try {\n token = await config.getAccessToken()\n } catch (error) {\n // 資格情報の失敗は待っても直らない。再試行に混ぜると、設定ミスの発覚が遅れるだけ\n // ⚠️ トークン取得の失敗は、資格情報そのものを載せた例外を投げてくることがある\n // (署名済み assertion、client_secret、鍵ファイルの中身)。ここを素通しにすると、\n // ログにも `sync_error` にも運用通知にも、その全部が流れる\n throw new GoogleDirectoryAuthError(`could not obtain an access token: ${redact(error instanceof Error ? error.message : String(error))}`)\n }\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), retry.timeoutMs)\n let response: Awaited<ReturnType<FetchLike>>\n let body: string\n try {\n response = await doFetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: \"application/json\" }, signal: controller.signal })\n // ⚠️ 本文の読み取りも時限の内側に置く。応答ヘッダだけ返して本体を送り続ける相手\n // (遅い中間装置、切れかけた接続) では、ヘッダ受信で時計を止めると **待ち時間に\n // 上限が無くなる**。同期はリンクを逐次処理するため、1 本が全体を止める\n body = await response.text()\n } finally {\n clearTimeout(timer)\n }\n\n if (!response.ok) {\n // ⚠️ **伏せてから切る。** 先に切ると、境界をまたいだ JWT は 3 区間がそろわず規則が\n // 当たらない — 前半 (発行者を含む) が平文で残る。走査量は `redact` 自身が上限で\n // 抑えるので、ここで先に切って守るものは無い\n const failure = redact(body).slice(0, 500)\n const reason = extractReason(failure)\n const error = new GoogleDirectoryHttpError(`Google Directory request failed: ${response.status} ${failure}`, response.status, url, reason)\n // 恒久的な失敗は即座に諦める。待っても直らないうえ、待つ分だけ陳腐化が進む\n if (!isTransient(response.status, reason)) throw error\n lastError = error\n } else {\n let parsed: unknown\n try {\n parsed = JSON.parse(body)\n } catch {\n // 200 だが JSON ですらない。中間装置が差し込んだ応答であり、待っても直らない。\n // ⚠️ その中間装置は資格情報を echo し得る (プロキシの認証画面が典型) ので、\n // 状態が 200 でも本文をそのまま載せない\n throw new GoogleDirectoryShapeError(`Google Directory returned a non-JSON body (${redact(body).slice(0, 120)})`, url)\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new GoogleDirectoryShapeError(`Google Directory returned ${Array.isArray(parsed) ? \"an array\" : String(parsed)} where a JSON object was expected`, url)\n }\n return assertShape(parsed as Record<string, unknown>, url)\n }\n } catch (error) {\n if (error instanceof GoogleDirectoryAuthError || error instanceof GoogleDirectoryShapeError) throw error\n if (error instanceof GoogleDirectoryHttpError && !isTransient(error.status, error.reason)) throw error\n lastError = error instanceof Error ? error : new Error(String(error))\n }\n if (attempt < retry.attempts) {\n // ジッタを入れる。入れないと、同時に弾かれた呼び出しが揃って同じ時刻に再送し、\n // レート制限を同じ形で踏み直す\n const backoff = retry.baseDelayMs * 2 ** (attempt - 1)\n await sleep(backoff + Math.floor(random() * retry.baseDelayMs))\n }\n }\n throw lastError\n}\n\n/** How a paginated listing is read. ページング API の読み方。 */\nexport type PagedReader<TItem> = {\n /** Builds the URL for a page token. ページトークンから URL を組み立てる処理。 */\n buildUrl: (pageToken: string | undefined) => string\n /** Pulls the items and the next token out of one page. 1 ページから要素と次トークンを取り出す。 */\n extract: (body: Record<string, unknown>, url: string) => { items: TItem[]; nextPageToken?: string }\n /**\n * Upper bound so a broken cursor cannot loop forever. 壊れたカーソルで無限ループしない上限。\n *\n * 同時に、このアダプタが扱えるグループの大きさの上限でもある。到達したら例外になるため、\n * 大きなグループを黙って切り捨てることはない。\n */\n maxPages: number\n}\n\n/**\n * Walks every page of a paginated list endpoint. ページング API の全ページを辿る処理。\n *\n * ⚠️ **途中で失敗したら例外を送出し、それまでに集めた分は捨てる。** 部分的な一覧を返すと、\n * 呼び出し側はそれを「上流の全て」と解釈して残りを削除する。\n *\n * @param reader How to build, parse and bound the listing. 一覧の組み立て・解析・上限。\n * @param config Token supplier and injected collaborators. トークン供給と注入した協調要素。\n * @returns Every item across every page. 全ページの全要素。\n * @throws {Error} On any page failure, a cursor that revisits a token, or the page bound.\n * ページ失敗・トークンの再訪・上限超過のいずれでも throw。\n */\nexport async function fetchAllPages<TItem>(reader: PagedReader<TItem>, config: GoogleHttpConfig): Promise<TItem[]> {\n const all: TItem[] = []\n // 直前のトークンとの比較だけでは A→B→A→B の循環を検出できず、上限まで往復してから落ちる。\n // レート制限のある API に対して 200 往復を空費するのは、それ自体が次の失敗の原因になる\n const seenTokens = new Set<string>()\n let pageToken: string | undefined\n for (let page = 0; page < reader.maxPages; page++) {\n const { items, nextPageToken } = await fetchJson(reader.buildUrl(pageToken), config, reader.extract)\n all.push(...items)\n if (!nextPageToken) return all\n if (seenTokens.has(nextPageToken)) throw new Error(`Google Directory paging revisited a page token; refusing to loop (${reader.buildUrl(pageToken)})`)\n seenTokens.add(nextPageToken)\n pageToken = nextPageToken\n }\n throw new Error(`Google Directory paging exceeded ${reader.maxPages} pages; refusing to report a partial member list`)\n}\n","/**\n * @module catalog\n * @description Lists the domain's groups so an admin surface can offer them for selection.\n * ドメインのグループを列挙し、管理面が選択肢として提示できるようにするモジュール。\n *\n * ⚠️ **同期には使わない。** 同期は「連携済みの 1 グループの所属」を読む ({@link createGoogleDirectoryProvider})。\n * こちらは「ドメインにどんなグループがあるか」を読む。用途が違えば必要な権限も失敗の意味も違うため、\n * ポートを分けてある — 同期ジョブは候補一覧を必要とせず、管理面はメンバー一覧を必要としない。\n *\n * ⚠️ **これは利便のための読み取りである。** 失敗しても管理面は開かなければならない。呼び出し側は\n * 例外を「選択欄を出さない」状態へ写すこと。ここで既定値へ倒す (空配列を返す) と、上流に\n * グループが 1 件も無いことと、上流へ届かないことが区別できなくなる。\n */\nimport type { AuthDirectoryCatalogGroup } from \"@aiquants/auth-core\"\nimport { describeUntrusted, fetchAllPages, GoogleDirectoryShapeError, type GoogleHttpConfig } from \"./http\"\n\n/** Admin SDK Directory API v1. */\nconst DIRECTORY_BASE = \"https://admin.googleapis.com/admin/directory/v1\"\n\n/**\n * One group as the catalog reports it. 候補一覧が報告するグループ 1 件。\n *\n * ⚠️ **別名ではなく `@aiquants/auth-core` の型そのものである。** この形は上流アダプタ (ここ)・\n * 永続化層・管理 UI の 3 層を通る。層ごとに「同じ意味の別の型」を宣言すると、構造的部分型の\n * せいでどの境界も通ってしまい、**上流へ足した欄が途中の層で黙って捨てられる** — 読み手側の\n * 境界は型エラーを出さないので、足した本人も気付けない。定義は 1 つに置き、各層が import する。\n */\nexport type DirectoryCatalogGroup = AuthDirectoryCatalogGroup\n\n/** Configuration of the catalog reader. 候補一覧リーダーの構成。 */\nexport type GoogleDirectoryCatalogConfig = GoogleHttpConfig & {\n /**\n * Customer whose groups are listed. 列挙対象の顧客。\n *\n * 既定の `my_customer` は「借用先ユーザーが属する顧客」を指す。複数顧客を扱う配備だけが\n * 明示する必要がある。\n */\n customerId?: string\n /** Page size, 1..200 (the Admin SDK maximum). ページ長 (Admin SDK の上限は 200)。 */\n pageSize?: number\n /** Page limit, so a runaway cursor cannot loop forever. ページ数上限 (暴走の歯止め)。 */\n maxPages?: number\n}\n\n/**\n * Reads a string field, treating anything else as absent. 文字列項目を読む処理 (他の型は不在扱い)。\n *\n * @param value Raw field value. 生の項目値。\n * @returns The trimmed string, or `\"\"`. 前後の空白を除いた文字列、または空文字。\n */\nfunction str(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\"\n}\n\n/**\n * Creates the catalog reader. 候補一覧リーダーを生成する処理。\n *\n * @param config Transport and paging configuration. トランスポートとページングの構成。\n * @returns An object exposing `listGroups`. `listGroups` を持つオブジェクト。\n * @throws {TypeError} When the paging configuration is unusable. ページング構成が使えない場合。\n */\nexport function createGoogleDirectoryCatalog(config: GoogleDirectoryCatalogConfig): { listGroups: () => Promise<DirectoryCatalogGroup[]> } {\n const pageSize = config.pageSize ?? 200\n const maxPages = config.maxPages ?? 200\n const customerId = (config.customerId ?? \"my_customer\").trim()\n // 既定へ倒さず結線時点で落とす。0 を渡すと Admin SDK の最小値 1 を下回り、毎回 400 になる\n if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 200) throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(config.pageSize)}`)\n if (!Number.isInteger(maxPages) || maxPages < 1) throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(config.maxPages)}`)\n if (customerId === \"\") throw new TypeError(\"[@aiquants/auth-google-directory] customerId must not be blank\")\n\n return {\n async listGroups(): Promise<DirectoryCatalogGroup[]> {\n const rows = await fetchAllPages<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown; directMembersCount?: unknown }>(\n {\n buildUrl: (pageToken) => `${DIRECTORY_BASE}/groups?customer=${encodeURIComponent(customerId)}&maxResults=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n // `kind` を要求するのは、企業プロキシが遮断やセッション切れを HTTP 200 + JSON で\n // 返すためである。その本文には `groups` が無いので、検証しないと「グループが\n // 1 件も無いドメイン」と区別できない\n // ⚠️ `GoogleDirectoryShapeError` を投げる。素の `Error` は `fetchJson` から\n // 見て「一時的な失敗」であり、待っても直らないこの応答を 4 回再送する。\n // ⚠️ `kind` は上流 (あるいは割り込んだプロキシ) が決めた値なので、そのまま\n // 文へ入れない。長さを切り、資格情報が混じっていれば伏せる\n if (body.kind !== \"admin#directory#groups\") throw new GoogleDirectoryShapeError(`expected an admin#directory#groups document but the body carried kind=${describeUntrusted(body.kind)}`, url)\n const groups = body.groups\n // グループが 1 件も無いドメインでは `groups` が現れない。空配列として扱う\n if (groups === undefined) return { items: [], nextPageToken: str(body.nextPageToken) || undefined }\n if (!Array.isArray(groups)) throw new GoogleDirectoryShapeError(\"expected groups to be an array\", url)\n return { items: groups as Array<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown; directMembersCount?: unknown }>, nextPageToken: str(body.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n return rows\n .map((row) => {\n const externalId = str(row.id)\n const externalKey = str(row.email)\n // 識別子かアドレスを欠く行は選択肢にならない。落とさず残すと、選んだ瞬間に失敗する\n if (externalId === \"\" || externalKey === \"\") return null\n // `directMembersCount` は文字列で返ることがある。数として読めないなら伏せる\n const rawCount = Number(str(row.directMembersCount) || row.directMembersCount)\n const memberCount = Number.isInteger(rawCount) && rawCount >= 0 ? rawCount : undefined\n return {\n externalId,\n externalKey,\n name: str(row.name) || externalKey,\n ...(memberCount === undefined ? {} : { memberCount }),\n ...(str(row.description) === \"\" ? {} : { description: str(row.description) }),\n }\n })\n .filter((row): row is DirectoryCatalogGroup => row !== null)\n },\n }\n}\n","/**\n * @module provider\n * @description Google Workspace implementation of the `DirectoryProvider` port.\n * `DirectoryProvider` ポートの Google Workspace 実装。\n *\n * 直接所属は Admin SDK Directory、入れ子を展開した所属は Cloud Identity から取る。**書込\n * メソッドは 1 つも持たない** — 同期が上流を書き換える経路を、このパッケージに存在させない。\n *\n * 資格情報はトークン供給関数として注入する。おかげで本モジュールは Google の SDK に一切\n * 依存せず、ネットワークもトークンも無しで全分岐をテストできる。\n */\nimport type { AuthDirectoryMembershipMode, DirectoryGroup, DirectoryMember, DirectoryProvider } from \"@aiquants/auth-directory-core\"\nimport { fetchAllPages, fetchJson, GoogleDirectoryHttpError, GoogleDirectoryShapeError, type GoogleHttpConfig } from \"./http\"\n\nconst DIRECTORY_BASE = \"https://admin.googleapis.com/admin/directory/v1\"\nconst CLOUD_IDENTITY_BASE = \"https://cloudidentity.googleapis.com/v1\"\n\n/** Configuration of the Google directory provider. Google ディレクトリ provider の構成。 */\nexport type GoogleDirectoryConfig = GoogleHttpConfig & {\n /**\n * Page size for member listings. メンバー一覧のページサイズ。\n *\n * Admin SDK の `maxResults` は 1..200。Cloud Identity の `pageSize` は既定 200・最大 1000 で\n * 上限が違うため、両者で共有できる範囲に制限する。\n */\n pageSize?: number\n /**\n * Page bound, and therefore the largest group this adapter will report. ページ上限。\n *\n * 既定 200 ページ × 200 件 = 40,000 人。超えるグループは切り捨てではなく例外になる。\n * 実際にそれより大きなグループを扱う配備は、ここを明示的に引き上げること。\n */\n maxPages?: number\n}\n\n/** Member kinds the Admin SDK reports. Admin SDK が報告するメンバー種別。 */\nconst MEMBER_TYPES = { person: new Set([\"USER\", \"EXTERNAL\"]), group: new Set([\"GROUP\"]) }\n/** Member statuses that mean the account can actually be used. 実際に使えることを意味する状態。 */\nconst USABLE_STATUSES = new Set([\"ACTIVE\", \"\"])\n/** Statuses that mean the account exists but cannot be used. 存在するが使えないことを意味する状態。 */\nconst UNUSABLE_STATUSES = new Set([\"SUSPENDED\", \"ARCHIVED\", \"INVITED\", \"PENDING\"])\n\n/**\n * Rejects a group identifier that would change the meaning of the request URL.\n * リクエスト URL の意味を変えてしまうグループ識別子を拒否する処理。\n *\n * 否定リストではなく **肯定的な文字クラス** で検証する。以前は `/` `?` `#` だけを弾いており、\n * `..` が通っていた。`encodeURIComponent(\"..\")` は `..` のままなので、URL 解析器が経路を\n * 畳んで別のエンドポイントを叩き、Google が 404 を返し、呼び出し側が「上流でグループが消えた」と\n * 解釈してローカルのグループを無効化していた。\n *\n * @throws {TypeError} When the identifier is not a plain id or address. 素の ID・アドレスでない場合。\n */\nfunction assertExternalId(externalId: string): string {\n const trimmed = externalId.trim()\n // Google のグループ ID は英数字、アドレスはメール形式。どちらもこの集合に収まる\n if (!/^[A-Za-z0-9][A-Za-z0-9._%+-]*(@[A-Za-z0-9][A-Za-z0-9.-]*\\.[A-Za-z]{2,})?$/.test(trimmed)) {\n throw new TypeError(`[@aiquants/auth-google-directory] externalId must be a plain identifier or address, received ${JSON.stringify(externalId)}`)\n }\n return trimmed\n}\n\n/** Reads a string field, treating absent and non-string alike. 文字列項目の読み取り。 */\nconst str = (v: unknown): string => (typeof v === \"string\" ? v.trim() : \"\")\n\n/**\n * Creates a read-only Google Workspace directory provider.\n * 読み取り専用の Google Workspace ディレクトリ provider を生成する処理。\n *\n * @param config Token supplier, injected fetch/sleep, retry policy and bounds. 構成。\n * @returns The provider. provider。\n * @throws {TypeError} When a bound is outside its usable range. 上限が使用可能な範囲外の場合。\n */\nexport function createGoogleDirectoryProvider(config: GoogleDirectoryConfig): DirectoryProvider {\n const pageSize = config.pageSize ?? 200\n const maxPages = config.maxPages ?? 200\n // 既定へ倒さず結線時点で落とす。0 を渡すと Admin SDK の最小値 1 を下回り、毎回 400 になる\n if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 200) throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(config.pageSize)}`)\n if (!Number.isInteger(maxPages) || maxPages < 1) throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(config.maxPages)}`)\n\n /**\n * Refuses a body that is not the document this endpoint returns.\n * このエンドポイントが返す文書でない本文を拒否する処理。\n *\n * `kind` を要求するのは、企業プロキシが遮断やセッション切れを HTTP 200 + JSON で返すため\n * である。その本文には `members` が無いので、検証しないと「空のグループ」と区別できず、\n * 全員が削除対象になる。Admin SDK は空のグループでも `kind` を必ず返す。\n */\n function assertKind<T>(expected: string, body: Record<string, unknown>, url: string): T {\n if (str(body.kind) !== expected) {\n throw new GoogleDirectoryShapeError(`expected a ${expected} document but the body carried kind=${JSON.stringify(body.kind ?? null)}; refusing to treat it as an empty result`, url)\n }\n return body as T\n }\n\n /**\n * Refuses a Cloud Identity body carrying keys this endpoint never returns.\n * このエンドポイントが返さないキーを持つ Cloud Identity の本文を拒否する処理。\n *\n * Cloud Identity には `kind` に相当する判別子が無い。代わりに、想定外のキーを持つ本文を\n * 拒否する。遮断ページは `error` や `message` を持つため、これで区別できる。\n */\n function assertKeys<T>(allowed: readonly string[], body: Record<string, unknown>, url: string): T {\n const unexpected = Object.keys(body).filter((k) => !allowed.includes(k))\n if (unexpected.length > 0) {\n throw new GoogleDirectoryShapeError(`body carried unexpected keys (${unexpected.join(\", \")}); refusing to treat it as an empty result`, url)\n }\n return body as T\n }\n\n /**\n * Converts an upstream row into a member, refusing rows it cannot classify.\n * 上流の行をメンバーへ変換する処理。分類できない行は拒否する。\n *\n * 黙って捨てるとメンバーが 1 人減り、その差分が削除として実行される。\n */\n function toMember(email: string, externalUserId?: string): DirectoryMember {\n if (email === \"\") throw new GoogleDirectoryShapeError(\"Google Directory returned a member row without an address; refusing to report an incomplete member list\", DIRECTORY_BASE)\n return externalUserId ? { email, externalUserId } : { email }\n }\n\n /**\n * Fetches one group's metadata. 1 グループのメタデータを取得する処理。\n *\n * `this` を使わずクロージャとして持つ。オブジェクトのメソッドとして `this.getGroup` を\n * 呼ぶと、分割代入やコールバック経由で渡された時点で実行時に落ちる。\n */\n async function getGroup(externalId: string): Promise<DirectoryGroup | null> {\n const id = assertExternalId(externalId)\n try {\n const group = await fetchJson<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown }>(`${DIRECTORY_BASE}/groups/${encodeURIComponent(id)}`, config, (body, url) => assertKind(\"admin#directory#group\", body, url))\n const stableId = str(group.id)\n const email = str(group.email)\n if (stableId === \"\" || email === \"\") throw new GoogleDirectoryShapeError(`Google Directory returned an incomplete group resource for ${id}`, `${DIRECTORY_BASE}/groups/${id}`)\n return { externalId: stableId, externalKey: email, name: str(group.name) || email, ...(str(group.description) ? { description: str(group.description) } : {}) }\n } catch (error) {\n // 404 だけが「上流に無い」。他の失敗を null に畳むと、到達できないことが\n // 「グループが消えた」に化けて、生きているグループが無効化されてしまう\n if (error instanceof GoogleDirectoryHttpError && error.status === 404) return null\n throw error\n }\n }\n\n /** Direct membership via the Admin SDK. Admin SDK による直接所属。 */\n async function listDirectMembers(group: DirectoryGroup): Promise<DirectoryMember[]> {\n const id = assertExternalId(group.externalId)\n const rows = await fetchAllPages<{ id?: unknown; email?: unknown; type?: unknown; status?: unknown }>(\n {\n buildUrl: (pageToken) => `${DIRECTORY_BASE}/groups/${encodeURIComponent(id)}/members?maxResults=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n const page = assertKind<{ members?: Array<Record<string, unknown>>; nextPageToken?: string }>(\"admin#directory#members\", body, url)\n return { items: page.members ?? [], nextPageToken: str(page.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n const members: DirectoryMember[] = []\n for (const row of rows) {\n const type = str(row.type) || \"USER\"\n // 入れ子グループ自身の行は「人」ではない。direct では展開しない約束なので落とす\n if (MEMBER_TYPES.group.has(type)) continue\n // 未知の種別を人として扱うのも、黙って落とすのも誤り。CUSTOMER (ドメイン全体) を\n // 落とすとグループが 0 人に見え、そのまま全員削除の plan になる\n if (!MEMBER_TYPES.person.has(type)) throw new GoogleDirectoryShapeError(`Google Directory reported member type ${JSON.stringify(type)}, which this adapter cannot classify; refusing to guess whether it is a person`, DIRECTORY_BASE)\n const status = str(row.status)\n // 外部メンバーは status が空で返ることがある。空を「使える」と読むのは、Google が\n // 使えない状態には必ず名前を付けるからである\n if (UNUSABLE_STATUSES.has(status)) continue\n if (!USABLE_STATUSES.has(status)) throw new GoogleDirectoryShapeError(`Google Directory reported member status ${JSON.stringify(status)}, which this adapter cannot classify`, DIRECTORY_BASE)\n members.push(toMember(str(row.email), str(row.id) || undefined))\n }\n return members\n }\n\n /** Nested membership flattened via Cloud Identity. Cloud Identity による入れ子展開所属。 */\n async function listTransitiveMembers(group: DirectoryGroup): Promise<DirectoryMember[]> {\n const lookupUrl = `${CLOUD_IDENTITY_BASE}/groups:lookup?groupKey.id=${encodeURIComponent(group.externalKey)}`\n const looked = await fetchJson<{ name?: unknown }>(lookupUrl, config, (body, url) => assertKeys([\"name\"], body, url))\n const resourceName = str(looked.name)\n if (resourceName === \"\") throw new GoogleDirectoryShapeError(`Cloud Identity did not return a resource name for ${group.externalKey}`, lookupUrl)\n // URL のパスへそのまま差し込む唯一の値なので、形を検査する。応答本文が汚染されたとき\n // に `..` が入ると、Bearer トークンを載せた要求が別のパスへ向かう\n if (!/^groups\\/[A-Za-z0-9_-]+$/.test(resourceName)) {\n throw new GoogleDirectoryShapeError(`Cloud Identity returned a resource name this adapter will not put in a URL: ${JSON.stringify(resourceName)}`, lookupUrl)\n }\n\n const rows = await fetchAllPages<{ preferredMemberKey?: unknown; member?: unknown; relationType?: unknown }>(\n {\n buildUrl: (pageToken) => `${CLOUD_IDENTITY_BASE}/${resourceName}/memberships:searchTransitiveMemberships?pageSize=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n const page = assertKeys<{ memberships?: Array<Record<string, unknown>>; nextPageToken?: string }>([\"memberships\", \"nextPageToken\"], body, url)\n return { items: page.memberships ?? [], nextPageToken: str(page.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n\n const members: DirectoryMember[] = []\n for (const row of rows) {\n const member = str(row.member)\n // 入れ子グループ自身も所属として返る。人として数えると、グループアドレスと同じ\n // ローカルユーザーが居た場合に親グループのロールを継承してしまう\n if (member.startsWith(\"groups/\")) continue\n if (!member.startsWith(\"users/\")) throw new GoogleDirectoryShapeError(`Cloud Identity reported a member resource ${JSON.stringify(member)}, which this adapter cannot classify; refusing to guess whether it is a person`, CLOUD_IDENTITY_BASE)\n const keys = Array.isArray(row.preferredMemberKey) ? (row.preferredMemberKey as Array<Record<string, unknown>>) : []\n if (keys.length !== 1) throw new GoogleDirectoryShapeError(`Cloud Identity reported ${keys.length} member keys for ${member}; refusing to pick one`, CLOUD_IDENTITY_BASE)\n // namespace 付きは外部 identity 由来で、id はアドレスではない。アドレスとして扱うと\n // 台帳に人でない文字列が入る\n if (str(keys[0]?.namespace) !== \"\") throw new GoogleDirectoryShapeError(`Cloud Identity reported an external-identity member (${str(keys[0]?.namespace)}); its id is not an address`, CLOUD_IDENTITY_BASE)\n // `users/<id>` の id を捨ててはならない。捨てると台帳の `external_user_id` が\n // 全行 NULL になり、「上流 ID 第一キー・アドレス第二キー」の防御が入れ子展開の\n // リンクでだけ無効化される — アドレスを引き継いだ後任者が前任者の所属を継承する\n const upstreamUserId = member.slice(\"users/\".length).trim()\n if (upstreamUserId === \"\") throw new GoogleDirectoryShapeError(`Cloud Identity reported an empty user id in ${JSON.stringify(member)}`, CLOUD_IDENTITY_BASE)\n members.push(toMember(str(keys[0]?.id), upstreamUserId))\n }\n return members\n }\n\n return {\n getGroup,\n async listGroupMembers(group: DirectoryGroup, mode: AuthDirectoryMembershipMode): Promise<DirectoryMember[]> {\n return mode === \"direct\" ? listDirectMembers(group) : listTransitiveMembers(group)\n },\n }\n}\n","/**\n * @module token\n * @description Domain-wide delegation token provider.\n * ドメイン全体の委任によるトークン供給。\n *\n * ⚠️ **借用先 (`subject`) には特権管理者を指定しないこと。** ドメイン全体の委任は「指定した\n * ユーザーとして振る舞う」機能であり、特権管理者を借用すると、この鍵は実質的に特権管理者の鍵に\n * なる。「グループ > 読み取り」だけを持つカスタム管理者ロールを割り当てた専用ユーザーを用意し、\n * それを指定すること。\n *\n * ⚠️ **スコープは readonly のみ。** 書込スコープを渡すと、このパッケージが読み取り専用である\n * という契約を、パッケージの外側で破ることになる。\n *\n * ただしこの検査が守れるのは、ここで組み立てるスコープ集合までである。**実際の境界は\n * `createClient` であり、呼び出し側は渡された scopes を無視して自前の JWT を作れる。** 読み取り\n * 専用であることは、このガードと、借用先ユーザーに与える管理者ロールの狭さの両方で担保する。\n */\nimport type { GoogleHttpConfig } from \"./http\"\n\n/** Read-only scopes this integration needs. 本連携が必要とする読み取り専用スコープ。 */\nexport const GOOGLE_DIRECTORY_READONLY_SCOPES = [\"https://www.googleapis.com/auth/admin.directory.group.readonly\", \"https://www.googleapis.com/auth/admin.directory.group.member.readonly\"] as const\n\n/** Additional read-only scope required to expand nested groups. 入れ子展開に要る追加スコープ。 */\nexport const GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE = \"https://www.googleapis.com/auth/cloud-identity.groups.readonly\"\n\n/** Minimal shape of `google-auth-library`'s JWT client. JWT クライアントの最小形。 */\nexport type JwtClientLike = {\n getAccessToken(): Promise<{ token?: string | null }>\n}\n\n/** Configuration of the delegation token provider. 委任トークン供給の構成。 */\nexport type DomainWideDelegationConfig = {\n /**\n * Builds the JWT client. Injected so this package does not depend on a Google SDK.\n * JWT クライアントを構築する処理。Google SDK への依存を持ち込まないため注入する。\n *\n * 呼び出し側の実装例:\n * `({ subject, scopes }) => new JWT({ keyFile, subject, scopes })`\n */\n createClient: (args: { subject: string; scopes: string[] }) => JwtClientLike\n /** Workspace user to impersonate. Must NOT be a super administrator. 借用先ユーザー。 */\n subject: string\n /** True when nested membership expansion is used anywhere. 入れ子展開を使う場合に true。 */\n includeNestedScope: boolean\n /**\n * Extra read-only scopes, when a deployment needs more than the defaults.\n * 既定に加えて要る読み取り専用スコープ (必要な配備のみ)。\n *\n * 受け取れるようにしてあるのは、読み取り専用の検査を **到達可能** にするためでもある。\n * 定数だけを検査する形では、検査は決して発火せず、契約を守っているように見えるだけになる。\n */\n additionalScopes?: readonly string[]\n}\n\n/** Rejects a scope that would grant more than reading. 読み取りを超える権限を与えるスコープを拒否する。 */\nfunction assertReadOnly(scopes: readonly string[]): void {\n const writable = scopes.filter((s) => !s.endsWith(\".readonly\"))\n if (writable.length > 0) {\n throw new TypeError(`[@aiquants/auth-google-directory] only read-only scopes are allowed, received ${writable.join(\", \")}`)\n }\n}\n\n/**\n * Creates a token supplier that impersonates a least-privileged directory reader.\n * 最小権限のディレクトリ読み取り専用ユーザーを借用するトークン供給を生成する処理。\n *\n * @param config Client factory, impersonated subject and nested-scope flag. 構成。\n * @returns A `getAccessToken` suitable for {@link GoogleHttpConfig}. トークン供給関数。\n * @throws {TypeError} When the subject is blank. 借用先が空の場合。\n *\n * トークンのキャッシュは `google-auth-library` の JWT クライアントが行う。ここで二重に\n * 貯めると、失効の扱いが 2 箇所に分かれて食い違う。\n */\nexport function createDomainWideDelegationTokenProvider(config: DomainWideDelegationConfig): GoogleHttpConfig[\"getAccessToken\"] {\n const subject = config.subject.trim()\n if (subject === \"\") throw new TypeError(\"[@aiquants/auth-google-directory] subject must be a non-empty Workspace user to impersonate\")\n const scopes = [...GOOGLE_DIRECTORY_READONLY_SCOPES, ...(config.includeNestedScope ? [GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE] : []), ...(config.additionalScopes ?? [])]\n assertReadOnly(scopes)\n const client = config.createClient({ subject, scopes })\n\n return async () => {\n const { token } = await client.getAccessToken()\n // トークンが取れないまま空文字で続行すると、401 が「メンバー 0 件」に化ける経路が生まれる\n if (!token) throw new Error(`[@aiquants/auth-google-directory] could not obtain an access token for ${subject}`)\n return token\n }\n}\n"],"mappings":"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","names":["index_exports","__export","GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE","GOOGLE_DIRECTORY_READONLY_SCOPES","GoogleDirectoryAuthError","GoogleDirectoryHttpError","GoogleDirectoryShapeError","createDomainWideDelegationTokenProvider","createGoogleDirectoryCatalog","createGoogleDirectoryProvider","describeUntrusted","fetchAllPages","fetchJson","__toCommonJS","GoogleDirectoryHttpError","message","status","url","reason","GoogleDirectoryAuthError","GoogleDirectoryShapeError","RATE_LIMIT_REASONS","extractReason","body","parsed","isTransient","MAX_REDACTED_LENGTH","redact","text","describeUntrusted","value","defaultRetry","assertRetry","retry","fetchJson","config","assertShape","doFetch","sleep","ms","random","lastError","attempt","token","error","controller","timer","response","failure","backoff","fetchAllPages","reader","all","seenTokens","pageToken","page","items","nextPageToken","DIRECTORY_BASE","str","value","createGoogleDirectoryCatalog","config","pageSize","maxPages","customerId","fetchAllPages","pageToken","body","url","GoogleDirectoryShapeError","describeUntrusted","groups","row","externalId","externalKey","rawCount","memberCount","DIRECTORY_BASE","CLOUD_IDENTITY_BASE","MEMBER_TYPES","USABLE_STATUSES","UNUSABLE_STATUSES","assertExternalId","externalId","trimmed","str","v","createGoogleDirectoryProvider","config","pageSize","maxPages","assertKind","expected","body","url","GoogleDirectoryShapeError","assertKeys","allowed","unexpected","k","toMember","email","externalUserId","getGroup","id","group","fetchJson","stableId","error","GoogleDirectoryHttpError","listDirectMembers","rows","fetchAllPages","pageToken","page","members","row","type","status","listTransitiveMembers","lookupUrl","looked","resourceName","member","keys","upstreamUserId","mode","GOOGLE_DIRECTORY_READONLY_SCOPES","GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE","assertReadOnly","scopes","writable","s","createDomainWideDelegationTokenProvider","config","subject","client","token"]}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var x=class extends Error{constructor(t,s,y,g){super(t),this.name="GoogleDirectoryHttpError",this.status=s,this.url=y,g&&(this.reason=g)}},E=class extends Error{constructor(t){super(t),this.name="GoogleDirectoryAuthError"}},p=class extends Error{constructor(t,s){super(t),this.name="GoogleDirectoryShapeError",this.url=s}},I=new Set(["userRateLimitExceeded","quotaExceeded","rateLimitExceeded","backendError"]);function R(e){try{let t=JSON.parse(e);return t.error?.errors?.[0]?.reason??t.error?.status}catch{return}}function $(e,t){return e===429||e===408||e>=500?!0:e===403&&t!==void 0&&I.has(t)}var M=e=>e.replace(/Bearer\s+[\w.\-~+/]+=*/gi,"Bearer [redacted]"),N={attempts:4,baseDelayMs:500,timeoutMs:3e4};function O(e){if(!Number.isInteger(e.attempts)||e.attempts<1)throw new TypeError(`[@aiquants/auth-google-directory] retry.attempts must be a positive integer, received ${String(e.attempts)}`);if(!Number.isFinite(e.baseDelayMs)||e.baseDelayMs<0)throw new TypeError(`[@aiquants/auth-google-directory] retry.baseDelayMs must be a non-negative number, received ${String(e.baseDelayMs)}`);if(!Number.isFinite(e.timeoutMs)||e.timeoutMs<=0)throw new TypeError(`[@aiquants/auth-google-directory] retry.timeoutMs must be a positive number, received ${String(e.timeoutMs)}`)}async function C(e,t,s){let y=t.fetch??globalThis.fetch,g=t.retry??N;O(g);let o=t.sleep??(w=>new Promise(r=>setTimeout(r,w))),h=t.random??Math.random,d=new x(`Google Directory request was never attempted: ${e}`,0,e);for(let w=1;w<=g.attempts;w++){try{let r;try{r=await t.getAccessToken()}catch(n){throw new E(`could not obtain an access token: ${n instanceof Error?n.message:String(n)}`)}let i=new AbortController,m=setTimeout(()=>i.abort(),g.timeoutMs),c;try{c=await y(e,{headers:{Authorization:`Bearer ${r}`,Accept:"application/json"},signal:i.signal})}finally{clearTimeout(m)}if(c.ok){let n=await c.text(),u;try{u=JSON.parse(n)}catch{throw new p(`Google Directory returned a non-JSON body (${n.slice(0,120)})`,e)}if(u===null||typeof u!="object"||Array.isArray(u))throw new p(`Google Directory returned ${Array.isArray(u)?"an array":String(u)} where a JSON object was expected`,e);return s(u,e)}else{let n=M((await c.text()).slice(0,500)),u=R(n),a=new x(`Google Directory request failed: ${c.status} ${n}`,c.status,e,u);if(!$(c.status,u))throw a;d=a}}catch(r){if(r instanceof E||r instanceof p||r instanceof x&&!$(r.status,r.reason))throw r;d=r instanceof Error?r:new Error(String(r))}if(w<g.attempts){let r=g.baseDelayMs*2**(w-1);await o(r+Math.floor(h()*g.baseDelayMs))}}throw d}async function S(e,t){let s=[],y=new Set,g;for(let o=0;o<e.maxPages;o++){let{items:h,nextPageToken:d}=await C(e.buildUrl(g),t,e.extract);if(s.push(...h),!d)return s;if(y.has(d))throw new Error(`Google Directory paging revisited a page token; refusing to loop (${e.buildUrl(g)})`);y.add(d),g=d}throw new Error(`Google Directory paging exceeded ${e.maxPages} pages; refusing to report a partial member list`)}var v="https://admin.googleapis.com/admin/directory/v1";function k(e){return typeof e=="string"?e.trim():""}function U(e){let t=e.pageSize??200,s=e.maxPages??200,y=(e.customerId??"my_customer").trim();if(!Number.isInteger(t)||t<1||t>200)throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(e.pageSize)}`);if(!Number.isInteger(s)||s<1)throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(e.maxPages)}`);if(y==="")throw new TypeError("[@aiquants/auth-google-directory] customerId must not be blank");return{async listGroups(){return(await S({buildUrl:o=>`${v}/groups?customer=${encodeURIComponent(y)}&maxResults=${t}${o?`&pageToken=${encodeURIComponent(o)}`:""}`,extract:(o,h)=>{if(o.kind!=="admin#directory#groups")throw new Error(`Google Directory returned an unexpected document for ${h}: kind=${String(o.kind)}`);let d=o.groups;if(d===void 0)return{items:[],nextPageToken:k(o.nextPageToken)||void 0};if(!Array.isArray(d))throw new Error(`Google Directory returned a non-array group list for ${h}`);return{items:d,nextPageToken:k(o.nextPageToken)||void 0}},maxPages:s},e)).map(o=>{let h=k(o.id),d=k(o.email);if(h===""||d==="")return null;let w=Number(k(o.directMembersCount)||o.directMembersCount),r=Number.isInteger(w)&&w>=0?w:void 0;return{externalId:h,externalKey:d,name:k(o.name)||d,...r===void 0?{}:{memberCount:r},...k(o.description)===""?{}:{description:k(o.description)}}}).filter(o=>o!==null)}}}var D="https://admin.googleapis.com/admin/directory/v1",T="https://cloudidentity.googleapis.com/v1",G={person:new Set(["USER","EXTERNAL"]),group:new Set(["GROUP"])},_=new Set(["ACTIVE",""]),L=new Set(["SUSPENDED","ARCHIVED","INVITED","PENDING"]);function A(e){let t=e.trim();if(!/^[A-Za-z0-9][A-Za-z0-9._%+-]*(@[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,})?$/.test(t))throw new TypeError(`[@aiquants/auth-google-directory] externalId must be a plain identifier or address, received ${JSON.stringify(e)}`);return t}var l=e=>typeof e=="string"?e.trim():"";function Y(e){let t=e.pageSize??200,s=e.maxPages??200;if(!Number.isInteger(t)||t<1||t>200)throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(e.pageSize)}`);if(!Number.isInteger(s)||s<1)throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(e.maxPages)}`);function y(r,i,m){if(l(i.kind)!==r)throw new p(`expected a ${r} document but the body carried kind=${JSON.stringify(i.kind??null)}; refusing to treat it as an empty result`,m);return i}function g(r,i,m){let c=Object.keys(i).filter(n=>!r.includes(n));if(c.length>0)throw new p(`body carried unexpected keys (${c.join(", ")}); refusing to treat it as an empty result`,m);return i}function o(r,i){if(r==="")throw new p("Google Directory returned a member row without an address; refusing to report an incomplete member list",D);return i?{email:r,externalUserId:i}:{email:r}}async function h(r){let i=A(r);try{let m=await C(`${D}/groups/${encodeURIComponent(i)}`,e,(u,a)=>y("admin#directory#group",u,a)),c=l(m.id),n=l(m.email);if(c===""||n==="")throw new p(`Google Directory returned an incomplete group resource for ${i}`,`${D}/groups/${i}`);return{externalId:c,externalKey:n,name:l(m.name)||n,...l(m.description)?{description:l(m.description)}:{}}}catch(m){if(m instanceof x&&m.status===404)return null;throw m}}async function d(r){let i=A(r.externalId),m=await S({buildUrl:n=>`${D}/groups/${encodeURIComponent(i)}/members?maxResults=${t}${n?`&pageToken=${encodeURIComponent(n)}`:""}`,extract:(n,u)=>{let a=y("admin#directory#members",n,u);return{items:a.members??[],nextPageToken:l(a.nextPageToken)||void 0}},maxPages:s},e),c=[];for(let n of m){let u=l(n.type)||"USER";if(G.group.has(u))continue;if(!G.person.has(u))throw new p(`Google Directory reported member type ${JSON.stringify(u)}, which this adapter cannot classify; refusing to guess whether it is a person`,D);let a=l(n.status);if(!L.has(a)){if(!_.has(a))throw new p(`Google Directory reported member status ${JSON.stringify(a)}, which this adapter cannot classify`,D);c.push(o(l(n.email),l(n.id)||void 0))}}return c}async function w(r){let i=`${T}/groups:lookup?groupKey.id=${encodeURIComponent(r.externalKey)}`,m=await C(i,e,(a,f)=>g(["name"],a,f)),c=l(m.name);if(c==="")throw new p(`Cloud Identity did not return a resource name for ${r.externalKey}`,i);if(!/^groups\/[A-Za-z0-9_-]+$/.test(c))throw new p(`Cloud Identity returned a resource name this adapter will not put in a URL: ${JSON.stringify(c)}`,i);let n=await S({buildUrl:a=>`${T}/${c}/memberships:searchTransitiveMemberships?pageSize=${t}${a?`&pageToken=${encodeURIComponent(a)}`:""}`,extract:(a,f)=>{let b=g(["memberships","nextPageToken"],a,f);return{items:b.memberships??[],nextPageToken:l(b.nextPageToken)||void 0}},maxPages:s},e),u=[];for(let a of n){let f=l(a.member);if(f.startsWith("groups/"))continue;if(!f.startsWith("users/"))throw new p(`Cloud Identity reported a member resource ${JSON.stringify(f)}, which this adapter cannot classify; refusing to guess whether it is a person`,T);let b=Array.isArray(a.preferredMemberKey)?a.preferredMemberKey:[];if(b.length!==1)throw new p(`Cloud Identity reported ${b.length} member keys for ${f}; refusing to pick one`,T);if(l(b[0]?.namespace)!=="")throw new p(`Cloud Identity reported an external-identity member (${l(b[0]?.namespace)}); its id is not an address`,T);let P=f.slice(6).trim();if(P==="")throw new p(`Cloud Identity reported an empty user id in ${JSON.stringify(f)}`,T);u.push(o(l(b[0]?.id),P))}return u}return{getGroup:h,async listGroupMembers(r,i){return i==="direct"?d(r):w(r)}}}var z=["https://www.googleapis.com/auth/admin.directory.group.readonly","https://www.googleapis.com/auth/admin.directory.group.member.readonly"],q="https://www.googleapis.com/auth/cloud-identity.groups.readonly";function J(e){let t=e.filter(s=>!s.endsWith(".readonly"));if(t.length>0)throw new TypeError(`[@aiquants/auth-google-directory] only read-only scopes are allowed, received ${t.join(", ")}`)}function W(e){let t=e.subject.trim();if(t==="")throw new TypeError("[@aiquants/auth-google-directory] subject must be a non-empty Workspace user to impersonate");let s=[...z,...e.includeNestedScope?[q]:[],...e.additionalScopes??[]];J(s);let y=e.createClient({subject:t,scopes:s});return async()=>{let{token:g}=await y.getAccessToken();if(!g)throw new Error(`[@aiquants/auth-google-directory] could not obtain an access token for ${t}`);return g}}export{q as GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE,z as GOOGLE_DIRECTORY_READONLY_SCOPES,E as GoogleDirectoryAuthError,x as GoogleDirectoryHttpError,p as GoogleDirectoryShapeError,W as createDomainWideDelegationTokenProvider,U as createGoogleDirectoryCatalog,Y as createGoogleDirectoryProvider,S as fetchAllPages,C as fetchJson};
1
+ var x=class extends Error{constructor(t,s,y,d){super(t),this.name="GoogleDirectoryHttpError",this.status=s,this.url=y,d&&(this.reason=d)}},C=class extends Error{constructor(t){super(t),this.name="GoogleDirectoryAuthError"}},u=class extends Error{constructor(t,s){super(t),this.name="GoogleDirectoryShapeError",this.url=s}},M=new Set(["userRateLimitExceeded","quotaExceeded","rateLimitExceeded","backendError"]);function N(e){try{let t=JSON.parse(e);return t.error?.errors?.[0]?.reason??t.error?.status}catch{return}}function $(e,t){return e===429||e===408||e>=500?!0:e===403&&t!==void 0&&M.has(t)}var v=8192;function S(e){return e.slice(0,v).replace(/\bBearer\s+[\w.\-~+/]+=*/g,"Bearer [redacted]").replace(/\bBasic\s+[A-Za-z0-9+/]+={0,2}/g,"Basic [redacted]").replace(/eyJ[\w-]*\.[\w-]+\.[\w-]+/g,"[redacted-jwt]").replace(/ya29\.[\w.\-~+/]+=*/g,"[redacted-access-token]").replace(/1\/\/[\w-]{10,}/g,"[redacted-refresh-token]").replace(/-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----[\s\S]{0,4000}?-----END [A-Z ]{0,20}PRIVATE KEY-----/g,"[redacted-private-key]").replace(/-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----[\s\S]{0,4000}/g,"[redacted-private-key]").replace(/\b((?:access|refresh|id)_token|assertion|client_secret|private_key)("?[ \t]*[:=][ \t]*"?)(?!\[redacted)[^\s"',&)\]}]+/g,"$1$2[redacted]")}function G(e){return S(typeof e=="string"?e:String(e)).slice(0,120)}var O={attempts:4,baseDelayMs:500,timeoutMs:3e4};function U(e){if(!Number.isInteger(e.attempts)||e.attempts<1)throw new TypeError(`[@aiquants/auth-google-directory] retry.attempts must be a positive integer, received ${String(e.attempts)}`);if(!Number.isFinite(e.baseDelayMs)||e.baseDelayMs<0)throw new TypeError(`[@aiquants/auth-google-directory] retry.baseDelayMs must be a non-negative number, received ${String(e.baseDelayMs)}`);if(!Number.isFinite(e.timeoutMs)||e.timeoutMs<=0)throw new TypeError(`[@aiquants/auth-google-directory] retry.timeoutMs must be a positive number, received ${String(e.timeoutMs)}`)}async function P(e,t,s){let y=t.fetch??globalThis.fetch,d=t.retry??O;U(d);let n=t.sleep??(h=>new Promise(r=>setTimeout(r,h))),f=t.random??Math.random,p=new x(`Google Directory request was never attempted: ${e}`,0,e);for(let h=1;h<=d.attempts;h++){try{let r;try{r=await t.getAccessToken()}catch(o){throw new C(`could not obtain an access token: ${S(o instanceof Error?o.message:String(o))}`)}let i=new AbortController,m=setTimeout(()=>i.abort(),d.timeoutMs),g,a;try{g=await y(e,{headers:{Authorization:`Bearer ${r}`,Accept:"application/json"},signal:i.signal}),a=await g.text()}finally{clearTimeout(m)}if(g.ok){let o;try{o=JSON.parse(a)}catch{throw new u(`Google Directory returned a non-JSON body (${S(a).slice(0,120)})`,e)}if(o===null||typeof o!="object"||Array.isArray(o))throw new u(`Google Directory returned ${Array.isArray(o)?"an array":String(o)} where a JSON object was expected`,e);return s(o,e)}else{let o=S(a).slice(0,500),c=N(o),w=new x(`Google Directory request failed: ${g.status} ${o}`,g.status,e,c);if(!$(g.status,c))throw w;p=w}}catch(r){if(r instanceof C||r instanceof u||r instanceof x&&!$(r.status,r.reason))throw r;p=r instanceof Error?r:new Error(String(r))}if(h<d.attempts){let r=d.baseDelayMs*2**(h-1);await n(r+Math.floor(f()*d.baseDelayMs))}}throw p}async function E(e,t){let s=[],y=new Set,d;for(let n=0;n<e.maxPages;n++){let{items:f,nextPageToken:p}=await P(e.buildUrl(d),t,e.extract);if(s.push(...f),!p)return s;if(y.has(p))throw new Error(`Google Directory paging revisited a page token; refusing to loop (${e.buildUrl(d)})`);y.add(p),d=p}throw new Error(`Google Directory paging exceeded ${e.maxPages} pages; refusing to report a partial member list`)}var _="https://admin.googleapis.com/admin/directory/v1";function k(e){return typeof e=="string"?e.trim():""}function L(e){let t=e.pageSize??200,s=e.maxPages??200,y=(e.customerId??"my_customer").trim();if(!Number.isInteger(t)||t<1||t>200)throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(e.pageSize)}`);if(!Number.isInteger(s)||s<1)throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(e.maxPages)}`);if(y==="")throw new TypeError("[@aiquants/auth-google-directory] customerId must not be blank");return{async listGroups(){return(await E({buildUrl:n=>`${_}/groups?customer=${encodeURIComponent(y)}&maxResults=${t}${n?`&pageToken=${encodeURIComponent(n)}`:""}`,extract:(n,f)=>{if(n.kind!=="admin#directory#groups")throw new u(`expected an admin#directory#groups document but the body carried kind=${G(n.kind)}`,f);let p=n.groups;if(p===void 0)return{items:[],nextPageToken:k(n.nextPageToken)||void 0};if(!Array.isArray(p))throw new u("expected groups to be an array",f);return{items:p,nextPageToken:k(n.nextPageToken)||void 0}},maxPages:s},e)).map(n=>{let f=k(n.id),p=k(n.email);if(f===""||p==="")return null;let h=Number(k(n.directMembersCount)||n.directMembersCount),r=Number.isInteger(h)&&h>=0?h:void 0;return{externalId:f,externalKey:p,name:k(n.name)||p,...r===void 0?{}:{memberCount:r},...k(n.description)===""?{}:{description:k(n.description)}}}).filter(n=>n!==null)}}}var T="https://admin.googleapis.com/admin/directory/v1",D="https://cloudidentity.googleapis.com/v1",I={person:new Set(["USER","EXTERNAL"]),group:new Set(["GROUP"])},z=new Set(["ACTIVE",""]),q=new Set(["SUSPENDED","ARCHIVED","INVITED","PENDING"]);function R(e){let t=e.trim();if(!/^[A-Za-z0-9][A-Za-z0-9._%+-]*(@[A-Za-z0-9][A-Za-z0-9.-]*\.[A-Za-z]{2,})?$/.test(t))throw new TypeError(`[@aiquants/auth-google-directory] externalId must be a plain identifier or address, received ${JSON.stringify(e)}`);return t}var l=e=>typeof e=="string"?e.trim():"";function F(e){let t=e.pageSize??200,s=e.maxPages??200;if(!Number.isInteger(t)||t<1||t>200)throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(e.pageSize)}`);if(!Number.isInteger(s)||s<1)throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(e.maxPages)}`);function y(r,i,m){if(l(i.kind)!==r)throw new u(`expected a ${r} document but the body carried kind=${JSON.stringify(i.kind??null)}; refusing to treat it as an empty result`,m);return i}function d(r,i,m){let g=Object.keys(i).filter(a=>!r.includes(a));if(g.length>0)throw new u(`body carried unexpected keys (${g.join(", ")}); refusing to treat it as an empty result`,m);return i}function n(r,i){if(r==="")throw new u("Google Directory returned a member row without an address; refusing to report an incomplete member list",T);return i?{email:r,externalUserId:i}:{email:r}}async function f(r){let i=R(r);try{let m=await P(`${T}/groups/${encodeURIComponent(i)}`,e,(o,c)=>y("admin#directory#group",o,c)),g=l(m.id),a=l(m.email);if(g===""||a==="")throw new u(`Google Directory returned an incomplete group resource for ${i}`,`${T}/groups/${i}`);return{externalId:g,externalKey:a,name:l(m.name)||a,...l(m.description)?{description:l(m.description)}:{}}}catch(m){if(m instanceof x&&m.status===404)return null;throw m}}async function p(r){let i=R(r.externalId),m=await E({buildUrl:a=>`${T}/groups/${encodeURIComponent(i)}/members?maxResults=${t}${a?`&pageToken=${encodeURIComponent(a)}`:""}`,extract:(a,o)=>{let c=y("admin#directory#members",a,o);return{items:c.members??[],nextPageToken:l(c.nextPageToken)||void 0}},maxPages:s},e),g=[];for(let a of m){let o=l(a.type)||"USER";if(I.group.has(o))continue;if(!I.person.has(o))throw new u(`Google Directory reported member type ${JSON.stringify(o)}, which this adapter cannot classify; refusing to guess whether it is a person`,T);let c=l(a.status);if(!q.has(c)){if(!z.has(c))throw new u(`Google Directory reported member status ${JSON.stringify(c)}, which this adapter cannot classify`,T);g.push(n(l(a.email),l(a.id)||void 0))}}return g}async function h(r){let i=`${D}/groups:lookup?groupKey.id=${encodeURIComponent(r.externalKey)}`,m=await P(i,e,(c,w)=>d(["name"],c,w)),g=l(m.name);if(g==="")throw new u(`Cloud Identity did not return a resource name for ${r.externalKey}`,i);if(!/^groups\/[A-Za-z0-9_-]+$/.test(g))throw new u(`Cloud Identity returned a resource name this adapter will not put in a URL: ${JSON.stringify(g)}`,i);let a=await E({buildUrl:c=>`${D}/${g}/memberships:searchTransitiveMemberships?pageSize=${t}${c?`&pageToken=${encodeURIComponent(c)}`:""}`,extract:(c,w)=>{let b=d(["memberships","nextPageToken"],c,w);return{items:b.memberships??[],nextPageToken:l(b.nextPageToken)||void 0}},maxPages:s},e),o=[];for(let c of a){let w=l(c.member);if(w.startsWith("groups/"))continue;if(!w.startsWith("users/"))throw new u(`Cloud Identity reported a member resource ${JSON.stringify(w)}, which this adapter cannot classify; refusing to guess whether it is a person`,D);let b=Array.isArray(c.preferredMemberKey)?c.preferredMemberKey:[];if(b.length!==1)throw new u(`Cloud Identity reported ${b.length} member keys for ${w}; refusing to pick one`,D);if(l(b[0]?.namespace)!=="")throw new u(`Cloud Identity reported an external-identity member (${l(b[0]?.namespace)}); its id is not an address`,D);let A=w.slice(6).trim();if(A==="")throw new u(`Cloud Identity reported an empty user id in ${JSON.stringify(w)}`,D);o.push(n(l(b[0]?.id),A))}return o}return{getGroup:f,async listGroupMembers(r,i){return i==="direct"?p(r):h(r)}}}var J=["https://www.googleapis.com/auth/admin.directory.group.readonly","https://www.googleapis.com/auth/admin.directory.group.member.readonly"],K="https://www.googleapis.com/auth/cloud-identity.groups.readonly";function B(e){let t=e.filter(s=>!s.endsWith(".readonly"));if(t.length>0)throw new TypeError(`[@aiquants/auth-google-directory] only read-only scopes are allowed, received ${t.join(", ")}`)}function V(e){let t=e.subject.trim();if(t==="")throw new TypeError("[@aiquants/auth-google-directory] subject must be a non-empty Workspace user to impersonate");let s=[...J,...e.includeNestedScope?[K]:[],...e.additionalScopes??[]];B(s);let y=e.createClient({subject:t,scopes:s});return async()=>{let{token:d}=await y.getAccessToken();if(!d)throw new Error(`[@aiquants/auth-google-directory] could not obtain an access token for ${t}`);return d}}export{K as GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE,J as GOOGLE_DIRECTORY_READONLY_SCOPES,C as GoogleDirectoryAuthError,x as GoogleDirectoryHttpError,u as GoogleDirectoryShapeError,V as createDomainWideDelegationTokenProvider,L as createGoogleDirectoryCatalog,F as createGoogleDirectoryProvider,G as describeUntrusted,E as fetchAllPages,P as fetchJson};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/http.ts","../src/catalog.ts","../src/provider.ts","../src/token.ts"],"sourcesContent":["/**\n * @module http\n * @description Authenticated JSON GET with bounded retries, used by the Google adapters.\n * Google アダプタが使う、認証付き JSON GET (再試行は有界)。\n *\n * **失敗を空配列へ倒さないことが本モジュールの要点である。** 呼び出し側は返ってきた一覧を\n * 「上流の全て」と解釈して差分を削除するため、取得できなかったことを「0 件だった」と報告すると、\n * その差分がそのまま大量剥奪になる。取れなければ必ず throw する。\n *\n * ⚠️ 「取れなかった」には **本文の形が想定と違う** 場合も含む。企業プロキシは遮断や\n * セッション切れを HTTP 200 + JSON で返すことがあり、その本文には当然 `members` が無い。\n * 型を主張するだけの `as T` では、その応答が「空のグループ」と区別できない。\n */\n\n/**\n * Minimal fetch shape, injected so the adapter is testable without a network.\n * 注入可能な fetch 面。\n *\n * メソッドも本文も渡せない形にしてある。読み取り専用であることを「送っていない」ではなく\n * 「送れない」で担保するためであり、ホスト側の実装にも書込可能な継ぎ目を作らせない。\n */\nexport type FetchLike = (\n url: string,\n init?: { headers?: Record<string, string>; signal?: AbortSignal },\n) => Promise<{\n ok: boolean\n status: number\n text(): Promise<string>\n}>\n\n/** Bounded retry policy. 有界な再試行方針。 */\nexport type RetryPolicy = {\n /** Total attempts including the first. 初回を含む試行回数。 */\n attempts: number\n /** Base delay; grows exponentially. 基準待機時間 (指数的に伸びる)。 */\n baseDelayMs: number\n /**\n * Deadline for one attempt. 1 回の試行の制限時間。\n *\n * 試行回数を有界にしても、1 回の試行が終わらなければ実行は止まったままになる。同期は\n * リンクを逐次処理するため、吊られた 1 本がテナント全体の同期を無期限に止める。\n */\n timeoutMs: number\n}\n\n/** Error carrying the upstream status, so callers can tell \"denied\" from \"unavailable\". 上流の状態を運ぶ例外。 */\nexport class GoogleDirectoryHttpError extends Error {\n readonly status: number\n readonly url: string\n /** Google's machine-readable reason, when the body carried one. 本文が持っていた reason。 */\n readonly reason?: string\n constructor(message: string, status: number, url: string, reason?: string) {\n super(message)\n this.name = \"GoogleDirectoryHttpError\"\n this.status = status\n this.url = url\n if (reason) this.reason = reason\n }\n}\n\n/** Raised when the credential could not be obtained. 資格情報を取得できなかったことを表す例外。 */\nexport class GoogleDirectoryAuthError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"GoogleDirectoryAuthError\"\n }\n}\n\n/** Raised when a 200 response is not the document this endpoint returns. 応答が想定の文書でない。 */\nexport class GoogleDirectoryShapeError extends Error {\n readonly url: string\n constructor(message: string, url: string) {\n super(message)\n this.name = \"GoogleDirectoryShapeError\"\n this.url = url\n }\n}\n\n/**\n * Reasons Google returns with HTTP 403 that are rate limits, not permission failures.\n * Google が HTTP 403 とともに返す、権限ではなくレート制限を意味する reason。\n *\n * Directory API の「Limits and Quotas」は 3 つのレート制限エラーを挙げており、うち 2 つは 403\n * である。403 を一律に恒久扱いすると、クォータに触れた瞬間から実行が全滅し、しかも待てば直る\n * はずのものを待たずに諦めるため、その間ずっと陳腐化が進む。\n */\nconst RATE_LIMIT_REASONS: ReadonlySet<string> = new Set([\"userRateLimitExceeded\", \"quotaExceeded\", \"rateLimitExceeded\", \"backendError\"])\n\n/** Extracts Google's machine-readable reason from an error body. 本文から reason を取り出す。 */\nfunction extractReason(body: string): string | undefined {\n try {\n const parsed = JSON.parse(body) as { error?: { errors?: Array<{ reason?: string }>; status?: string } }\n return parsed.error?.errors?.[0]?.reason ?? parsed.error?.status\n } catch {\n return undefined\n }\n}\n\n/** Whether waiting could plausibly make this attempt succeed. 待てば直る見込みがあるか。 */\nfunction isTransient(status: number, reason: string | undefined): boolean {\n if (status === 429 || status === 408 || status >= 500) return true\n // 403 はレート制限と権限不足の両方に使われる。reason を見ないと区別できない\n return status === 403 && reason !== undefined && RATE_LIMIT_REASONS.has(reason)\n}\n\n/** Hides a bearer token that an intermediary may have echoed into the body. 本文中の資格情報を伏せる。 */\nconst redact = (body: string): string => body.replace(/Bearer\\s+[\\w.\\-~+/]+=*/gi, \"Bearer [redacted]\")\n\n/** Configuration shared by the Google API clients. Google API クライアント共通の構成。 */\nexport type GoogleHttpConfig = {\n /** Supplies a bearer token for each call. 呼び出しごとに Bearer トークンを供給する処理。 */\n getAccessToken: () => Promise<string>\n fetch?: FetchLike\n retry?: RetryPolicy\n /** Injected so tests do not actually wait. テストが実際に待たないよう注入する。 */\n sleep?: (ms: number) => Promise<void>\n /** Injected so backoff jitter is reproducible in tests. ジッタを再現可能にするため注入する。 */\n random?: () => number\n}\n\nconst defaultRetry: RetryPolicy = { attempts: 4, baseDelayMs: 500, timeoutMs: 30_000 }\n\n/** Rejects a retry policy that cannot bound anything. 何も有界にしない再試行方針を拒否する。 */\nfunction assertRetry(retry: RetryPolicy): void {\n if (!Number.isInteger(retry.attempts) || retry.attempts < 1) throw new TypeError(`[@aiquants/auth-google-directory] retry.attempts must be a positive integer, received ${String(retry.attempts)}`)\n if (!Number.isFinite(retry.baseDelayMs) || retry.baseDelayMs < 0) throw new TypeError(`[@aiquants/auth-google-directory] retry.baseDelayMs must be a non-negative number, received ${String(retry.baseDelayMs)}`)\n if (!Number.isFinite(retry.timeoutMs) || retry.timeoutMs <= 0) throw new TypeError(`[@aiquants/auth-google-directory] retry.timeoutMs must be a positive number, received ${String(retry.timeoutMs)}`)\n}\n\n/**\n * Performs an authenticated JSON GET, retrying transient failures a bounded number of times.\n * 認証付き JSON GET を行い、一時的な失敗のみ有界回数だけ再試行する処理。\n *\n * @param url Absolute request URL. 絶対 URL。\n * @param config Token supplier and injected collaborators. トークン供給と注入した協調要素。\n * @param assertShape Rejects a 200 whose body is not this endpoint's document. 応答形の検証。\n * @returns The parsed JSON body. 解析済み JSON ボディ。\n * @throws {GoogleDirectoryHttpError} When the request ultimately failed. 最終的に失敗した場合。\n * @throws {GoogleDirectoryShapeError} When the body is not the expected document. 想定外の本文。\n * @throws {GoogleDirectoryAuthError} When no token could be obtained. トークンを取得できない場合。\n */\nexport async function fetchJson<T>(url: string, config: GoogleHttpConfig, assertShape: (body: Record<string, unknown>, url: string) => T): Promise<T> {\n const doFetch = config.fetch ?? (globalThis.fetch as unknown as FetchLike)\n const retry = config.retry ?? defaultRetry\n assertRetry(retry)\n const sleep = config.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n const random = config.random ?? Math.random\n\n let lastError: Error = new GoogleDirectoryHttpError(`Google Directory request was never attempted: ${url}`, 0, url)\n for (let attempt = 1; attempt <= retry.attempts; attempt++) {\n try {\n let token: string\n try {\n token = await config.getAccessToken()\n } catch (error) {\n // 資格情報の失敗は待っても直らない。再試行に混ぜると、設定ミスの発覚が遅れるだけ\n throw new GoogleDirectoryAuthError(`could not obtain an access token: ${error instanceof Error ? error.message : String(error)}`)\n }\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), retry.timeoutMs)\n let response: Awaited<ReturnType<FetchLike>>\n try {\n response = await doFetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: \"application/json\" }, signal: controller.signal })\n } finally {\n clearTimeout(timer)\n }\n\n if (!response.ok) {\n const body = redact((await response.text()).slice(0, 500))\n const reason = extractReason(body)\n const error = new GoogleDirectoryHttpError(`Google Directory request failed: ${response.status} ${body}`, response.status, url, reason)\n // 恒久的な失敗は即座に諦める。待っても直らないうえ、待つ分だけ陳腐化が進む\n if (!isTransient(response.status, reason)) throw error\n lastError = error\n } else {\n const raw = await response.text()\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n // 200 だが JSON ですらない。中間装置が差し込んだ応答であり、待っても直らない\n throw new GoogleDirectoryShapeError(`Google Directory returned a non-JSON body (${raw.slice(0, 120)})`, url)\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new GoogleDirectoryShapeError(`Google Directory returned ${Array.isArray(parsed) ? \"an array\" : String(parsed)} where a JSON object was expected`, url)\n }\n return assertShape(parsed as Record<string, unknown>, url)\n }\n } catch (error) {\n if (error instanceof GoogleDirectoryAuthError || error instanceof GoogleDirectoryShapeError) throw error\n if (error instanceof GoogleDirectoryHttpError && !isTransient(error.status, error.reason)) throw error\n lastError = error instanceof Error ? error : new Error(String(error))\n }\n if (attempt < retry.attempts) {\n // ジッタを入れる。入れないと、同時に弾かれた呼び出しが揃って同じ時刻に再送し、\n // レート制限を同じ形で踏み直す\n const backoff = retry.baseDelayMs * 2 ** (attempt - 1)\n await sleep(backoff + Math.floor(random() * retry.baseDelayMs))\n }\n }\n throw lastError\n}\n\n/** How a paginated listing is read. ページング API の読み方。 */\nexport type PagedReader<TItem> = {\n /** Builds the URL for a page token. ページトークンから URL を組み立てる処理。 */\n buildUrl: (pageToken: string | undefined) => string\n /** Pulls the items and the next token out of one page. 1 ページから要素と次トークンを取り出す。 */\n extract: (body: Record<string, unknown>, url: string) => { items: TItem[]; nextPageToken?: string }\n /**\n * Upper bound so a broken cursor cannot loop forever. 壊れたカーソルで無限ループしない上限。\n *\n * 同時に、このアダプタが扱えるグループの大きさの上限でもある。到達したら例外になるため、\n * 大きなグループを黙って切り捨てることはない。\n */\n maxPages: number\n}\n\n/**\n * Walks every page of a paginated list endpoint. ページング API の全ページを辿る処理。\n *\n * ⚠️ **途中で失敗したら例外を送出し、それまでに集めた分は捨てる。** 部分的な一覧を返すと、\n * 呼び出し側はそれを「上流の全て」と解釈して残りを削除する。\n *\n * @param reader How to build, parse and bound the listing. 一覧の組み立て・解析・上限。\n * @param config Token supplier and injected collaborators. トークン供給と注入した協調要素。\n * @returns Every item across every page. 全ページの全要素。\n * @throws {Error} On any page failure, a cursor that revisits a token, or the page bound.\n * ページ失敗・トークンの再訪・上限超過のいずれでも throw。\n */\nexport async function fetchAllPages<TItem>(reader: PagedReader<TItem>, config: GoogleHttpConfig): Promise<TItem[]> {\n const all: TItem[] = []\n // 直前のトークンとの比較だけでは A→B→A→B の循環を検出できず、上限まで往復してから落ちる。\n // レート制限のある API に対して 200 往復を空費するのは、それ自体が次の失敗の原因になる\n const seenTokens = new Set<string>()\n let pageToken: string | undefined\n for (let page = 0; page < reader.maxPages; page++) {\n const { items, nextPageToken } = await fetchJson(reader.buildUrl(pageToken), config, reader.extract)\n all.push(...items)\n if (!nextPageToken) return all\n if (seenTokens.has(nextPageToken)) throw new Error(`Google Directory paging revisited a page token; refusing to loop (${reader.buildUrl(pageToken)})`)\n seenTokens.add(nextPageToken)\n pageToken = nextPageToken\n }\n throw new Error(`Google Directory paging exceeded ${reader.maxPages} pages; refusing to report a partial member list`)\n}\n","/**\n * @module catalog\n * @description Lists the domain's groups so an admin surface can offer them for selection.\n * ドメインのグループを列挙し、管理面が選択肢として提示できるようにするモジュール。\n *\n * ⚠️ **同期には使わない。** 同期は「連携済みの 1 グループの所属」を読む ({@link createGoogleDirectoryProvider})。\n * こちらは「ドメインにどんなグループがあるか」を読む。用途が違えば必要な権限も失敗の意味も違うため、\n * ポートを分けてある — 同期ジョブは候補一覧を必要とせず、管理面はメンバー一覧を必要としない。\n *\n * ⚠️ **これは利便のための読み取りである。** 失敗しても管理面は開かなければならない。呼び出し側は\n * 例外を「選択欄を出さない」状態へ写すこと。ここで既定値へ倒す (空配列を返す) と、上流に\n * グループが 1 件も無いことと、上流へ届かないことが区別できなくなる。\n */\nimport { fetchAllPages, type GoogleHttpConfig } from \"./http\"\n\n/** Admin SDK Directory API v1. */\nconst DIRECTORY_BASE = \"https://admin.googleapis.com/admin/directory/v1\"\n\n/** One group as the catalog reports it. 候補一覧が報告するグループ 1 件。 */\nexport type DirectoryCatalogGroup = {\n /** Stable upstream identifier. 上流の安定識別子。 */\n externalId: string\n /** Operator-facing key such as the group address. 運用者向けキー (グループアドレス等)。 */\n externalKey: string\n name: string\n /** Members the directory reports for the group. 上流が報告するメンバー数。 */\n memberCount?: number\n description?: string\n}\n\n/** Configuration of the catalog reader. 候補一覧リーダーの構成。 */\nexport type GoogleDirectoryCatalogConfig = GoogleHttpConfig & {\n /**\n * Customer whose groups are listed. 列挙対象の顧客。\n *\n * 既定の `my_customer` は「借用先ユーザーが属する顧客」を指す。複数顧客を扱う配備だけが\n * 明示する必要がある。\n */\n customerId?: string\n /** Page size, 1..200 (the Admin SDK maximum). ページ長 (Admin SDK の上限は 200)。 */\n pageSize?: number\n /** Page limit, so a runaway cursor cannot loop forever. ページ数上限 (暴走の歯止め)。 */\n maxPages?: number\n}\n\n/**\n * Reads a string field, treating anything else as absent. 文字列項目を読む処理 (他の型は不在扱い)。\n *\n * @param value Raw field value. 生の項目値。\n * @returns The trimmed string, or `\"\"`. 前後の空白を除いた文字列、または空文字。\n */\nfunction str(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\"\n}\n\n/**\n * Creates the catalog reader. 候補一覧リーダーを生成する処理。\n *\n * @param config Transport and paging configuration. トランスポートとページングの構成。\n * @returns An object exposing `listGroups`. `listGroups` を持つオブジェクト。\n * @throws {TypeError} When the paging configuration is unusable. ページング構成が使えない場合。\n */\nexport function createGoogleDirectoryCatalog(config: GoogleDirectoryCatalogConfig): { listGroups: () => Promise<DirectoryCatalogGroup[]> } {\n const pageSize = config.pageSize ?? 200\n const maxPages = config.maxPages ?? 200\n const customerId = (config.customerId ?? \"my_customer\").trim()\n // 既定へ倒さず結線時点で落とす。0 を渡すと Admin SDK の最小値 1 を下回り、毎回 400 になる\n if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 200) throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(config.pageSize)}`)\n if (!Number.isInteger(maxPages) || maxPages < 1) throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(config.maxPages)}`)\n if (customerId === \"\") throw new TypeError(\"[@aiquants/auth-google-directory] customerId must not be blank\")\n\n return {\n async listGroups(): Promise<DirectoryCatalogGroup[]> {\n const rows = await fetchAllPages<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown; directMembersCount?: unknown }>(\n {\n buildUrl: (pageToken) => `${DIRECTORY_BASE}/groups?customer=${encodeURIComponent(customerId)}&maxResults=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n // `kind` を要求するのは、企業プロキシが遮断やセッション切れを HTTP 200 + JSON で\n // 返すためである。その本文には `groups` が無いので、検証しないと「グループが\n // 1 件も無いドメイン」と区別できない\n if (body.kind !== \"admin#directory#groups\") throw new Error(`Google Directory returned an unexpected document for ${url}: kind=${String(body.kind)}`)\n const groups = body.groups\n // グループが 1 件も無いドメインでは `groups` が現れない。空配列として扱う\n if (groups === undefined) return { items: [], nextPageToken: str(body.nextPageToken) || undefined }\n if (!Array.isArray(groups)) throw new Error(`Google Directory returned a non-array group list for ${url}`)\n return { items: groups as Array<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown; directMembersCount?: unknown }>, nextPageToken: str(body.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n return rows\n .map((row) => {\n const externalId = str(row.id)\n const externalKey = str(row.email)\n // 識別子かアドレスを欠く行は選択肢にならない。落とさず残すと、選んだ瞬間に失敗する\n if (externalId === \"\" || externalKey === \"\") return null\n // `directMembersCount` は文字列で返ることがある。数として読めないなら伏せる\n const rawCount = Number(str(row.directMembersCount) || row.directMembersCount)\n const memberCount = Number.isInteger(rawCount) && rawCount >= 0 ? rawCount : undefined\n return {\n externalId,\n externalKey,\n name: str(row.name) || externalKey,\n ...(memberCount === undefined ? {} : { memberCount }),\n ...(str(row.description) === \"\" ? {} : { description: str(row.description) }),\n }\n })\n .filter((row): row is DirectoryCatalogGroup => row !== null)\n },\n }\n}\n","/**\n * @module provider\n * @description Google Workspace implementation of the `DirectoryProvider` port.\n * `DirectoryProvider` ポートの Google Workspace 実装。\n *\n * 直接所属は Admin SDK Directory、入れ子を展開した所属は Cloud Identity から取る。**書込\n * メソッドは 1 つも持たない** — 同期が上流を書き換える経路を、このパッケージに存在させない。\n *\n * 資格情報はトークン供給関数として注入する。おかげで本モジュールは Google の SDK に一切\n * 依存せず、ネットワークもトークンも無しで全分岐をテストできる。\n */\nimport type { AuthDirectoryMembershipMode, DirectoryGroup, DirectoryMember, DirectoryProvider } from \"@aiquants/auth-directory-core\"\nimport { fetchAllPages, fetchJson, GoogleDirectoryHttpError, GoogleDirectoryShapeError, type GoogleHttpConfig } from \"./http\"\n\nconst DIRECTORY_BASE = \"https://admin.googleapis.com/admin/directory/v1\"\nconst CLOUD_IDENTITY_BASE = \"https://cloudidentity.googleapis.com/v1\"\n\n/** Configuration of the Google directory provider. Google ディレクトリ provider の構成。 */\nexport type GoogleDirectoryConfig = GoogleHttpConfig & {\n /**\n * Page size for member listings. メンバー一覧のページサイズ。\n *\n * Admin SDK の `maxResults` は 1..200。Cloud Identity の `pageSize` は既定 200・最大 1000 で\n * 上限が違うため、両者で共有できる範囲に制限する。\n */\n pageSize?: number\n /**\n * Page bound, and therefore the largest group this adapter will report. ページ上限。\n *\n * 既定 200 ページ × 200 件 = 40,000 人。超えるグループは切り捨てではなく例外になる。\n * 実際にそれより大きなグループを扱う配備は、ここを明示的に引き上げること。\n */\n maxPages?: number\n}\n\n/** Member kinds the Admin SDK reports. Admin SDK が報告するメンバー種別。 */\nconst MEMBER_TYPES = { person: new Set([\"USER\", \"EXTERNAL\"]), group: new Set([\"GROUP\"]) }\n/** Member statuses that mean the account can actually be used. 実際に使えることを意味する状態。 */\nconst USABLE_STATUSES = new Set([\"ACTIVE\", \"\"])\n/** Statuses that mean the account exists but cannot be used. 存在するが使えないことを意味する状態。 */\nconst UNUSABLE_STATUSES = new Set([\"SUSPENDED\", \"ARCHIVED\", \"INVITED\", \"PENDING\"])\n\n/**\n * Rejects a group identifier that would change the meaning of the request URL.\n * リクエスト URL の意味を変えてしまうグループ識別子を拒否する処理。\n *\n * 否定リストではなく **肯定的な文字クラス** で検証する。以前は `/` `?` `#` だけを弾いており、\n * `..` が通っていた。`encodeURIComponent(\"..\")` は `..` のままなので、URL 解析器が経路を\n * 畳んで別のエンドポイントを叩き、Google が 404 を返し、呼び出し側が「上流でグループが消えた」と\n * 解釈してローカルのグループを無効化していた。\n *\n * @throws {TypeError} When the identifier is not a plain id or address. 素の ID・アドレスでない場合。\n */\nfunction assertExternalId(externalId: string): string {\n const trimmed = externalId.trim()\n // Google のグループ ID は英数字、アドレスはメール形式。どちらもこの集合に収まる\n if (!/^[A-Za-z0-9][A-Za-z0-9._%+-]*(@[A-Za-z0-9][A-Za-z0-9.-]*\\.[A-Za-z]{2,})?$/.test(trimmed)) {\n throw new TypeError(`[@aiquants/auth-google-directory] externalId must be a plain identifier or address, received ${JSON.stringify(externalId)}`)\n }\n return trimmed\n}\n\n/** Reads a string field, treating absent and non-string alike. 文字列項目の読み取り。 */\nconst str = (v: unknown): string => (typeof v === \"string\" ? v.trim() : \"\")\n\n/**\n * Creates a read-only Google Workspace directory provider.\n * 読み取り専用の Google Workspace ディレクトリ provider を生成する処理。\n *\n * @param config Token supplier, injected fetch/sleep, retry policy and bounds. 構成。\n * @returns The provider. provider。\n * @throws {TypeError} When a bound is outside its usable range. 上限が使用可能な範囲外の場合。\n */\nexport function createGoogleDirectoryProvider(config: GoogleDirectoryConfig): DirectoryProvider {\n const pageSize = config.pageSize ?? 200\n const maxPages = config.maxPages ?? 200\n // 既定へ倒さず結線時点で落とす。0 を渡すと Admin SDK の最小値 1 を下回り、毎回 400 になる\n if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 200) throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(config.pageSize)}`)\n if (!Number.isInteger(maxPages) || maxPages < 1) throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(config.maxPages)}`)\n\n /**\n * Refuses a body that is not the document this endpoint returns.\n * このエンドポイントが返す文書でない本文を拒否する処理。\n *\n * `kind` を要求するのは、企業プロキシが遮断やセッション切れを HTTP 200 + JSON で返すため\n * である。その本文には `members` が無いので、検証しないと「空のグループ」と区別できず、\n * 全員が削除対象になる。Admin SDK は空のグループでも `kind` を必ず返す。\n */\n function assertKind<T>(expected: string, body: Record<string, unknown>, url: string): T {\n if (str(body.kind) !== expected) {\n throw new GoogleDirectoryShapeError(`expected a ${expected} document but the body carried kind=${JSON.stringify(body.kind ?? null)}; refusing to treat it as an empty result`, url)\n }\n return body as T\n }\n\n /**\n * Refuses a Cloud Identity body carrying keys this endpoint never returns.\n * このエンドポイントが返さないキーを持つ Cloud Identity の本文を拒否する処理。\n *\n * Cloud Identity には `kind` に相当する判別子が無い。代わりに、想定外のキーを持つ本文を\n * 拒否する。遮断ページは `error` や `message` を持つため、これで区別できる。\n */\n function assertKeys<T>(allowed: readonly string[], body: Record<string, unknown>, url: string): T {\n const unexpected = Object.keys(body).filter((k) => !allowed.includes(k))\n if (unexpected.length > 0) {\n throw new GoogleDirectoryShapeError(`body carried unexpected keys (${unexpected.join(\", \")}); refusing to treat it as an empty result`, url)\n }\n return body as T\n }\n\n /**\n * Converts an upstream row into a member, refusing rows it cannot classify.\n * 上流の行をメンバーへ変換する処理。分類できない行は拒否する。\n *\n * 黙って捨てるとメンバーが 1 人減り、その差分が削除として実行される。\n */\n function toMember(email: string, externalUserId?: string): DirectoryMember {\n if (email === \"\") throw new GoogleDirectoryShapeError(\"Google Directory returned a member row without an address; refusing to report an incomplete member list\", DIRECTORY_BASE)\n return externalUserId ? { email, externalUserId } : { email }\n }\n\n /**\n * Fetches one group's metadata. 1 グループのメタデータを取得する処理。\n *\n * `this` を使わずクロージャとして持つ。オブジェクトのメソッドとして `this.getGroup` を\n * 呼ぶと、分割代入やコールバック経由で渡された時点で実行時に落ちる。\n */\n async function getGroup(externalId: string): Promise<DirectoryGroup | null> {\n const id = assertExternalId(externalId)\n try {\n const group = await fetchJson<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown }>(`${DIRECTORY_BASE}/groups/${encodeURIComponent(id)}`, config, (body, url) => assertKind(\"admin#directory#group\", body, url))\n const stableId = str(group.id)\n const email = str(group.email)\n if (stableId === \"\" || email === \"\") throw new GoogleDirectoryShapeError(`Google Directory returned an incomplete group resource for ${id}`, `${DIRECTORY_BASE}/groups/${id}`)\n return { externalId: stableId, externalKey: email, name: str(group.name) || email, ...(str(group.description) ? { description: str(group.description) } : {}) }\n } catch (error) {\n // 404 だけが「上流に無い」。他の失敗を null に畳むと、到達できないことが\n // 「グループが消えた」に化けて、生きているグループが無効化されてしまう\n if (error instanceof GoogleDirectoryHttpError && error.status === 404) return null\n throw error\n }\n }\n\n /** Direct membership via the Admin SDK. Admin SDK による直接所属。 */\n async function listDirectMembers(group: DirectoryGroup): Promise<DirectoryMember[]> {\n const id = assertExternalId(group.externalId)\n const rows = await fetchAllPages<{ id?: unknown; email?: unknown; type?: unknown; status?: unknown }>(\n {\n buildUrl: (pageToken) => `${DIRECTORY_BASE}/groups/${encodeURIComponent(id)}/members?maxResults=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n const page = assertKind<{ members?: Array<Record<string, unknown>>; nextPageToken?: string }>(\"admin#directory#members\", body, url)\n return { items: page.members ?? [], nextPageToken: str(page.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n const members: DirectoryMember[] = []\n for (const row of rows) {\n const type = str(row.type) || \"USER\"\n // 入れ子グループ自身の行は「人」ではない。direct では展開しない約束なので落とす\n if (MEMBER_TYPES.group.has(type)) continue\n // 未知の種別を人として扱うのも、黙って落とすのも誤り。CUSTOMER (ドメイン全体) を\n // 落とすとグループが 0 人に見え、そのまま全員削除の plan になる\n if (!MEMBER_TYPES.person.has(type)) throw new GoogleDirectoryShapeError(`Google Directory reported member type ${JSON.stringify(type)}, which this adapter cannot classify; refusing to guess whether it is a person`, DIRECTORY_BASE)\n const status = str(row.status)\n // 外部メンバーは status が空で返ることがある。空を「使える」と読むのは、Google が\n // 使えない状態には必ず名前を付けるからである\n if (UNUSABLE_STATUSES.has(status)) continue\n if (!USABLE_STATUSES.has(status)) throw new GoogleDirectoryShapeError(`Google Directory reported member status ${JSON.stringify(status)}, which this adapter cannot classify`, DIRECTORY_BASE)\n members.push(toMember(str(row.email), str(row.id) || undefined))\n }\n return members\n }\n\n /** Nested membership flattened via Cloud Identity. Cloud Identity による入れ子展開所属。 */\n async function listTransitiveMembers(group: DirectoryGroup): Promise<DirectoryMember[]> {\n const lookupUrl = `${CLOUD_IDENTITY_BASE}/groups:lookup?groupKey.id=${encodeURIComponent(group.externalKey)}`\n const looked = await fetchJson<{ name?: unknown }>(lookupUrl, config, (body, url) => assertKeys([\"name\"], body, url))\n const resourceName = str(looked.name)\n if (resourceName === \"\") throw new GoogleDirectoryShapeError(`Cloud Identity did not return a resource name for ${group.externalKey}`, lookupUrl)\n // URL のパスへそのまま差し込む唯一の値なので、形を検査する。応答本文が汚染されたとき\n // に `..` が入ると、Bearer トークンを載せた要求が別のパスへ向かう\n if (!/^groups\\/[A-Za-z0-9_-]+$/.test(resourceName)) {\n throw new GoogleDirectoryShapeError(`Cloud Identity returned a resource name this adapter will not put in a URL: ${JSON.stringify(resourceName)}`, lookupUrl)\n }\n\n const rows = await fetchAllPages<{ preferredMemberKey?: unknown; member?: unknown; relationType?: unknown }>(\n {\n buildUrl: (pageToken) => `${CLOUD_IDENTITY_BASE}/${resourceName}/memberships:searchTransitiveMemberships?pageSize=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n const page = assertKeys<{ memberships?: Array<Record<string, unknown>>; nextPageToken?: string }>([\"memberships\", \"nextPageToken\"], body, url)\n return { items: page.memberships ?? [], nextPageToken: str(page.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n\n const members: DirectoryMember[] = []\n for (const row of rows) {\n const member = str(row.member)\n // 入れ子グループ自身も所属として返る。人として数えると、グループアドレスと同じ\n // ローカルユーザーが居た場合に親グループのロールを継承してしまう\n if (member.startsWith(\"groups/\")) continue\n if (!member.startsWith(\"users/\")) throw new GoogleDirectoryShapeError(`Cloud Identity reported a member resource ${JSON.stringify(member)}, which this adapter cannot classify; refusing to guess whether it is a person`, CLOUD_IDENTITY_BASE)\n const keys = Array.isArray(row.preferredMemberKey) ? (row.preferredMemberKey as Array<Record<string, unknown>>) : []\n if (keys.length !== 1) throw new GoogleDirectoryShapeError(`Cloud Identity reported ${keys.length} member keys for ${member}; refusing to pick one`, CLOUD_IDENTITY_BASE)\n // namespace 付きは外部 identity 由来で、id はアドレスではない。アドレスとして扱うと\n // 台帳に人でない文字列が入る\n if (str(keys[0]?.namespace) !== \"\") throw new GoogleDirectoryShapeError(`Cloud Identity reported an external-identity member (${str(keys[0]?.namespace)}); its id is not an address`, CLOUD_IDENTITY_BASE)\n // `users/<id>` の id を捨ててはならない。捨てると台帳の `external_user_id` が\n // 全行 NULL になり、「上流 ID 第一キー・アドレス第二キー」の防御が入れ子展開の\n // リンクでだけ無効化される — アドレスを引き継いだ後任者が前任者の所属を継承する\n const upstreamUserId = member.slice(\"users/\".length).trim()\n if (upstreamUserId === \"\") throw new GoogleDirectoryShapeError(`Cloud Identity reported an empty user id in ${JSON.stringify(member)}`, CLOUD_IDENTITY_BASE)\n members.push(toMember(str(keys[0]?.id), upstreamUserId))\n }\n return members\n }\n\n return {\n getGroup,\n async listGroupMembers(group: DirectoryGroup, mode: AuthDirectoryMembershipMode): Promise<DirectoryMember[]> {\n return mode === \"direct\" ? listDirectMembers(group) : listTransitiveMembers(group)\n },\n }\n}\n","/**\n * @module token\n * @description Domain-wide delegation token provider.\n * ドメイン全体の委任によるトークン供給。\n *\n * ⚠️ **借用先 (`subject`) には特権管理者を指定しないこと。** ドメイン全体の委任は「指定した\n * ユーザーとして振る舞う」機能であり、特権管理者を借用すると、この鍵は実質的に特権管理者の鍵に\n * なる。「グループ > 読み取り」だけを持つカスタム管理者ロールを割り当てた専用ユーザーを用意し、\n * それを指定すること。\n *\n * ⚠️ **スコープは readonly のみ。** 書込スコープを渡すと、このパッケージが読み取り専用である\n * という契約を、パッケージの外側で破ることになる。\n *\n * ただしこの検査が守れるのは、ここで組み立てるスコープ集合までである。**実際の境界は\n * `createClient` であり、呼び出し側は渡された scopes を無視して自前の JWT を作れる。** 読み取り\n * 専用であることは、このガードと、借用先ユーザーに与える管理者ロールの狭さの両方で担保する。\n */\nimport type { GoogleHttpConfig } from \"./http\"\n\n/** Read-only scopes this integration needs. 本連携が必要とする読み取り専用スコープ。 */\nexport const GOOGLE_DIRECTORY_READONLY_SCOPES = [\"https://www.googleapis.com/auth/admin.directory.group.readonly\", \"https://www.googleapis.com/auth/admin.directory.group.member.readonly\"] as const\n\n/** Additional read-only scope required to expand nested groups. 入れ子展開に要る追加スコープ。 */\nexport const GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE = \"https://www.googleapis.com/auth/cloud-identity.groups.readonly\"\n\n/** Minimal shape of `google-auth-library`'s JWT client. JWT クライアントの最小形。 */\nexport type JwtClientLike = {\n getAccessToken(): Promise<{ token?: string | null }>\n}\n\n/** Configuration of the delegation token provider. 委任トークン供給の構成。 */\nexport type DomainWideDelegationConfig = {\n /**\n * Builds the JWT client. Injected so this package does not depend on a Google SDK.\n * JWT クライアントを構築する処理。Google SDK への依存を持ち込まないため注入する。\n *\n * 呼び出し側の実装例:\n * `({ subject, scopes }) => new JWT({ keyFile, subject, scopes })`\n */\n createClient: (args: { subject: string; scopes: string[] }) => JwtClientLike\n /** Workspace user to impersonate. Must NOT be a super administrator. 借用先ユーザー。 */\n subject: string\n /** True when nested membership expansion is used anywhere. 入れ子展開を使う場合に true。 */\n includeNestedScope: boolean\n /**\n * Extra read-only scopes, when a deployment needs more than the defaults.\n * 既定に加えて要る読み取り専用スコープ (必要な配備のみ)。\n *\n * 受け取れるようにしてあるのは、読み取り専用の検査を **到達可能** にするためでもある。\n * 定数だけを検査する形では、検査は決して発火せず、契約を守っているように見えるだけになる。\n */\n additionalScopes?: readonly string[]\n}\n\n/** Rejects a scope that would grant more than reading. 読み取りを超える権限を与えるスコープを拒否する。 */\nfunction assertReadOnly(scopes: readonly string[]): void {\n const writable = scopes.filter((s) => !s.endsWith(\".readonly\"))\n if (writable.length > 0) {\n throw new TypeError(`[@aiquants/auth-google-directory] only read-only scopes are allowed, received ${writable.join(\", \")}`)\n }\n}\n\n/**\n * Creates a token supplier that impersonates a least-privileged directory reader.\n * 最小権限のディレクトリ読み取り専用ユーザーを借用するトークン供給を生成する処理。\n *\n * @param config Client factory, impersonated subject and nested-scope flag. 構成。\n * @returns A `getAccessToken` suitable for {@link GoogleHttpConfig}. トークン供給関数。\n * @throws {TypeError} When the subject is blank. 借用先が空の場合。\n *\n * トークンのキャッシュは `google-auth-library` の JWT クライアントが行う。ここで二重に\n * 貯めると、失効の扱いが 2 箇所に分かれて食い違う。\n */\nexport function createDomainWideDelegationTokenProvider(config: DomainWideDelegationConfig): GoogleHttpConfig[\"getAccessToken\"] {\n const subject = config.subject.trim()\n if (subject === \"\") throw new TypeError(\"[@aiquants/auth-google-directory] subject must be a non-empty Workspace user to impersonate\")\n const scopes = [...GOOGLE_DIRECTORY_READONLY_SCOPES, ...(config.includeNestedScope ? [GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE] : []), ...(config.additionalScopes ?? [])]\n assertReadOnly(scopes)\n const client = config.createClient({ subject, scopes })\n\n return async () => {\n const { token } = await client.getAccessToken()\n // トークンが取れないまま空文字で続行すると、401 が「メンバー 0 件」に化ける経路が生まれる\n if (!token) throw new Error(`[@aiquants/auth-google-directory] could not obtain an access token for ${subject}`)\n return token\n }\n}\n"],"mappings":"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","names":["GoogleDirectoryHttpError","message","status","url","reason","GoogleDirectoryAuthError","GoogleDirectoryShapeError","RATE_LIMIT_REASONS","extractReason","body","parsed","isTransient","redact","defaultRetry","assertRetry","retry","fetchJson","config","assertShape","doFetch","sleep","ms","random","lastError","attempt","token","error","controller","timer","response","raw","backoff","fetchAllPages","reader","all","seenTokens","pageToken","page","items","nextPageToken","DIRECTORY_BASE","str","value","createGoogleDirectoryCatalog","config","pageSize","maxPages","customerId","fetchAllPages","pageToken","body","url","groups","row","externalId","externalKey","rawCount","memberCount","DIRECTORY_BASE","CLOUD_IDENTITY_BASE","MEMBER_TYPES","USABLE_STATUSES","UNUSABLE_STATUSES","assertExternalId","externalId","trimmed","str","v","createGoogleDirectoryProvider","config","pageSize","maxPages","assertKind","expected","body","url","GoogleDirectoryShapeError","assertKeys","allowed","unexpected","k","toMember","email","externalUserId","getGroup","id","group","fetchJson","stableId","error","GoogleDirectoryHttpError","listDirectMembers","rows","fetchAllPages","pageToken","page","members","row","type","status","listTransitiveMembers","lookupUrl","looked","resourceName","member","keys","upstreamUserId","mode","GOOGLE_DIRECTORY_READONLY_SCOPES","GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE","assertReadOnly","scopes","writable","createDomainWideDelegationTokenProvider","config","subject","client","token"]}
1
+ {"version":3,"sources":["../src/http.ts","../src/catalog.ts","../src/provider.ts","../src/token.ts"],"sourcesContent":["/**\n * @module http\n * @description Authenticated JSON GET with bounded retries, used by the Google adapters.\n * Google アダプタが使う、認証付き JSON GET (再試行は有界)。\n *\n * **失敗を空配列へ倒さないことが本モジュールの要点である。** 呼び出し側は返ってきた一覧を\n * 「上流の全て」と解釈して差分を削除するため、取得できなかったことを「0 件だった」と報告すると、\n * その差分がそのまま大量剥奪になる。取れなければ必ず throw する。\n *\n * ⚠️ 「取れなかった」には **本文の形が想定と違う** 場合も含む。企業プロキシは遮断や\n * セッション切れを HTTP 200 + JSON で返すことがあり、その本文には当然 `members` が無い。\n * 型を主張するだけの `as T` では、その応答が「空のグループ」と区別できない。\n */\n\n/**\n * Minimal fetch shape, injected so the adapter is testable without a network.\n * 注入可能な fetch 面。\n *\n * メソッドも本文も渡せない形にしてある。読み取り専用であることを「送っていない」ではなく\n * 「送れない」で担保するためであり、ホスト側の実装にも書込可能な継ぎ目を作らせない。\n */\nexport type FetchLike = (\n url: string,\n init?: { headers?: Record<string, string>; signal?: AbortSignal },\n) => Promise<{\n ok: boolean\n status: number\n text(): Promise<string>\n}>\n\n/** Bounded retry policy. 有界な再試行方針。 */\nexport type RetryPolicy = {\n /** Total attempts including the first. 初回を含む試行回数。 */\n attempts: number\n /** Base delay; grows exponentially. 基準待機時間 (指数的に伸びる)。 */\n baseDelayMs: number\n /**\n * Deadline for one attempt. 1 回の試行の制限時間。\n *\n * 試行回数を有界にしても、1 回の試行が終わらなければ実行は止まったままになる。同期は\n * リンクを逐次処理するため、吊られた 1 本がテナント全体の同期を無期限に止める。\n */\n timeoutMs: number\n}\n\n/** Error carrying the upstream status, so callers can tell \"denied\" from \"unavailable\". 上流の状態を運ぶ例外。 */\nexport class GoogleDirectoryHttpError extends Error {\n readonly status: number\n readonly url: string\n /** Google's machine-readable reason, when the body carried one. 本文が持っていた reason。 */\n readonly reason?: string\n constructor(message: string, status: number, url: string, reason?: string) {\n super(message)\n this.name = \"GoogleDirectoryHttpError\"\n this.status = status\n this.url = url\n if (reason) this.reason = reason\n }\n}\n\n/** Raised when the credential could not be obtained. 資格情報を取得できなかったことを表す例外。 */\nexport class GoogleDirectoryAuthError extends Error {\n constructor(message: string) {\n super(message)\n this.name = \"GoogleDirectoryAuthError\"\n }\n}\n\n/** Raised when a 200 response is not the document this endpoint returns. 応答が想定の文書でない。 */\nexport class GoogleDirectoryShapeError extends Error {\n readonly url: string\n constructor(message: string, url: string) {\n super(message)\n this.name = \"GoogleDirectoryShapeError\"\n this.url = url\n }\n}\n\n/**\n * Reasons Google returns with HTTP 403 that are rate limits, not permission failures.\n * Google が HTTP 403 とともに返す、権限ではなくレート制限を意味する reason。\n *\n * Directory API の「Limits and Quotas」は 3 つのレート制限エラーを挙げており、うち 2 つは 403\n * である。403 を一律に恒久扱いすると、クォータに触れた瞬間から実行が全滅し、しかも待てば直る\n * はずのものを待たずに諦めるため、その間ずっと陳腐化が進む。\n */\nconst RATE_LIMIT_REASONS: ReadonlySet<string> = new Set([\"userRateLimitExceeded\", \"quotaExceeded\", \"rateLimitExceeded\", \"backendError\"])\n\n/** Extracts Google's machine-readable reason from an error body. 本文から reason を取り出す。 */\nfunction extractReason(body: string): string | undefined {\n try {\n const parsed = JSON.parse(body) as { error?: { errors?: Array<{ reason?: string }>; status?: string } }\n return parsed.error?.errors?.[0]?.reason ?? parsed.error?.status\n } catch {\n return undefined\n }\n}\n\n/** Whether waiting could plausibly make this attempt succeed. 待てば直る見込みがあるか。 */\nfunction isTransient(status: number, reason: string | undefined): boolean {\n if (status === 429 || status === 408 || status >= 500) return true\n // 403 はレート制限と権限不足の両方に使われる。reason を見ないと区別できない\n return status === 403 && reason !== undefined && RATE_LIMIT_REASONS.has(reason)\n}\n\n/**\n * Longest text the redaction rules will scan. 伏せ字規則が走査する最大長。\n *\n * ⚠️ 走査対象は **先に** 上限で切る。切らずに正規表現へ渡すと、開始マーカーだけを繰り返す本文\n * (企業プロキシが返す巨大な 502 など) で PEM 規則の遅延量指定子が末尾まで再走査を繰り返し、\n * 1 MB で 17 秒、4 MB で 2 分以上かかる。同期はリンクを逐次処理するため、汚染された応答\n * 1 本がテナント全体の同期を止める — 上流の応答長を信用してはならない。\n * JWT が 3 区間そろう長さは十分に確保してある。\n */\nconst MAX_REDACTED_LENGTH = 8192\n\n/**\n * Hides credentials an intermediary may have echoed into the text.\n * 中間装置が本文へ echo し得る資格情報を伏せる処理。\n *\n * @param text - Text that may carry credentials. 資格情報を含み得る文字列。\n * @returns The same text with credentials replaced. 資格情報を置き換えた文字列。\n */\nfunction redact(text: string): string {\n return (\n text\n .slice(0, MAX_REDACTED_LENGTH)\n .replace(/\\bBearer\\s+[\\w.\\-~+/]+=*/g, \"Bearer [redacted]\")\n // Basic 認証の資格情報も同じ経路 (プロキシの 407) で echo される\n .replace(/\\bBasic\\s+[A-Za-z0-9+/]+={0,2}/g, \"Basic [redacted]\")\n // 署名済み JWT (`eyJ` は `{\"` の base64url)。トークン取得の失敗は assertion を丸ごと\n // echo することがあり、その中身は秘密鍵で署名した主張そのものである。\n // ⚠️ 語境界を要求しない。上流やライブラリは区切りを入れずに値を連結することがあり\n // (`...tokenxeyJhbGci...`)、`\\b` を付けるとその形だけが素通りする\n .replace(/eyJ[\\w-]*\\.[\\w-]+\\.[\\w-]+/g, \"[redacted-jwt]\")\n // ⚠️ 鍵名を伴わない裸のトークンも伏せる。中間装置は `Bearer` を落として値だけを\n // echo することがあり、`ya29.` (アクセストークン) と `1//` (リフレッシュトークン)\n // はそのままで資格情報として使える。前置文字は限定しない — ログは括弧や角括弧で\n // 値を囲むため、区切り文字を列挙する形にすると囲まれた値だけが素通りする\n .replace(/ya29\\.[\\w.\\-~+/]+=*/g, \"[redacted-access-token]\")\n .replace(/1\\/\\/[\\w-]{10,}/g, \"[redacted-refresh-token]\")\n // PEM。鍵そのものが例外文へ載る経路は無いはずだが、無いことに依存しない。\n // 本体の長さは有界にする (無界の遅延量指定子は上の切り詰めが在っても二乗で効く)\n .replace(/-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----[\\s\\S]{0,4000}?-----END [A-Z ]{0,20}PRIVATE KEY-----/g, \"[redacted-private-key]\")\n // 終端マーカーを欠く (切り詰められた) 鍵も伏せる。上の規則は両端がそろわないと当たらず、\n // 切り詰めの結果として鍵材料の先頭が丸ごと残る\n .replace(/-----BEGIN [A-Z ]{0,20}PRIVATE KEY-----[\\s\\S]{0,4000}/g, \"[redacted-private-key]\")\n // ⚠️ 鍵名の規則は **PEM の後** に置く。先に置くと `\"private_key\":\"-----BEGIN ...` の\n // 値に当たって `-----BEGIN` までを伏せ字へ置き換え、PEM 規則が要求する開始マーカーが\n // 消える — 鍵本体だけが平文で残る。伏せる規則どうしが打ち消し合う形は、何も伏せない\n // より悪い (伏せたつもりになる)。既に伏せた値へ二重に当てないよう先読みで除く。\n // JSON でもクエリ形式でも同じ鍵名で運ばれる。`client_secret` はトークン端点の本文で\n // `assertion` と隣り合って現れる。区切りに改行を含めない — 含めると鍵名で終わる行の\n // 次の語まで飲み込み、診断に必要な文言が消える\n .replace(/\\b((?:access|refresh|id)_token|assertion|client_secret|private_key)(\"?[ \\t]*[:=][ \\t]*\"?)(?!\\[redacted)[^\\s\"',&)\\]}]+/g, \"$1$2[redacted]\")\n )\n}\n\n/**\n * Renders an untrusted value for a diagnostic message. 信用できない値を診断文へ載せる形にする処理。\n *\n * ⚠️ 上流 (あるいは割り込んだ中間装置) が決めた値をそのまま文へ入れない。長さは相手が決められる\n * ため、20 KB の文言がログにも `sync_error` 列にも流れる。資格情報が混じっていれば伏せる。\n *\n * @param value - Whatever the body carried. 本文が運んできた値。\n * @returns A short, redacted rendering. 短く、伏せ字を通した表現。\n */\nexport function describeUntrusted(value: unknown): string {\n return redact(typeof value === \"string\" ? value : String(value)).slice(0, 120)\n}\n\n/** Configuration shared by the Google API clients. Google API クライアント共通の構成。 */\nexport type GoogleHttpConfig = {\n /** Supplies a bearer token for each call. 呼び出しごとに Bearer トークンを供給する処理。 */\n getAccessToken: () => Promise<string>\n fetch?: FetchLike\n retry?: RetryPolicy\n /** Injected so tests do not actually wait. テストが実際に待たないよう注入する。 */\n sleep?: (ms: number) => Promise<void>\n /** Injected so backoff jitter is reproducible in tests. ジッタを再現可能にするため注入する。 */\n random?: () => number\n}\n\nconst defaultRetry: RetryPolicy = { attempts: 4, baseDelayMs: 500, timeoutMs: 30_000 }\n\n/** Rejects a retry policy that cannot bound anything. 何も有界にしない再試行方針を拒否する。 */\nfunction assertRetry(retry: RetryPolicy): void {\n if (!Number.isInteger(retry.attempts) || retry.attempts < 1) throw new TypeError(`[@aiquants/auth-google-directory] retry.attempts must be a positive integer, received ${String(retry.attempts)}`)\n if (!Number.isFinite(retry.baseDelayMs) || retry.baseDelayMs < 0) throw new TypeError(`[@aiquants/auth-google-directory] retry.baseDelayMs must be a non-negative number, received ${String(retry.baseDelayMs)}`)\n if (!Number.isFinite(retry.timeoutMs) || retry.timeoutMs <= 0) throw new TypeError(`[@aiquants/auth-google-directory] retry.timeoutMs must be a positive number, received ${String(retry.timeoutMs)}`)\n}\n\n/**\n * Performs an authenticated JSON GET, retrying transient failures a bounded number of times.\n * 認証付き JSON GET を行い、一時的な失敗のみ有界回数だけ再試行する処理。\n *\n * @param url Absolute request URL. 絶対 URL。\n * @param config Token supplier and injected collaborators. トークン供給と注入した協調要素。\n * @param assertShape Rejects a 200 whose body is not this endpoint's document. 応答形の検証。\n * @returns The parsed JSON body. 解析済み JSON ボディ。\n * @throws {GoogleDirectoryHttpError} When the request ultimately failed. 最終的に失敗した場合。\n * @throws {GoogleDirectoryShapeError} When the body is not the expected document. 想定外の本文。\n * @throws {GoogleDirectoryAuthError} When no token could be obtained. トークンを取得できない場合。\n */\nexport async function fetchJson<T>(url: string, config: GoogleHttpConfig, assertShape: (body: Record<string, unknown>, url: string) => T): Promise<T> {\n const doFetch = config.fetch ?? (globalThis.fetch as unknown as FetchLike)\n const retry = config.retry ?? defaultRetry\n assertRetry(retry)\n const sleep = config.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n const random = config.random ?? Math.random\n\n let lastError: Error = new GoogleDirectoryHttpError(`Google Directory request was never attempted: ${url}`, 0, url)\n for (let attempt = 1; attempt <= retry.attempts; attempt++) {\n try {\n let token: string\n try {\n token = await config.getAccessToken()\n } catch (error) {\n // 資格情報の失敗は待っても直らない。再試行に混ぜると、設定ミスの発覚が遅れるだけ\n // ⚠️ トークン取得の失敗は、資格情報そのものを載せた例外を投げてくることがある\n // (署名済み assertion、client_secret、鍵ファイルの中身)。ここを素通しにすると、\n // ログにも `sync_error` にも運用通知にも、その全部が流れる\n throw new GoogleDirectoryAuthError(`could not obtain an access token: ${redact(error instanceof Error ? error.message : String(error))}`)\n }\n const controller = new AbortController()\n const timer = setTimeout(() => controller.abort(), retry.timeoutMs)\n let response: Awaited<ReturnType<FetchLike>>\n let body: string\n try {\n response = await doFetch(url, { headers: { Authorization: `Bearer ${token}`, Accept: \"application/json\" }, signal: controller.signal })\n // ⚠️ 本文の読み取りも時限の内側に置く。応答ヘッダだけ返して本体を送り続ける相手\n // (遅い中間装置、切れかけた接続) では、ヘッダ受信で時計を止めると **待ち時間に\n // 上限が無くなる**。同期はリンクを逐次処理するため、1 本が全体を止める\n body = await response.text()\n } finally {\n clearTimeout(timer)\n }\n\n if (!response.ok) {\n // ⚠️ **伏せてから切る。** 先に切ると、境界をまたいだ JWT は 3 区間がそろわず規則が\n // 当たらない — 前半 (発行者を含む) が平文で残る。走査量は `redact` 自身が上限で\n // 抑えるので、ここで先に切って守るものは無い\n const failure = redact(body).slice(0, 500)\n const reason = extractReason(failure)\n const error = new GoogleDirectoryHttpError(`Google Directory request failed: ${response.status} ${failure}`, response.status, url, reason)\n // 恒久的な失敗は即座に諦める。待っても直らないうえ、待つ分だけ陳腐化が進む\n if (!isTransient(response.status, reason)) throw error\n lastError = error\n } else {\n let parsed: unknown\n try {\n parsed = JSON.parse(body)\n } catch {\n // 200 だが JSON ですらない。中間装置が差し込んだ応答であり、待っても直らない。\n // ⚠️ その中間装置は資格情報を echo し得る (プロキシの認証画面が典型) ので、\n // 状態が 200 でも本文をそのまま載せない\n throw new GoogleDirectoryShapeError(`Google Directory returned a non-JSON body (${redact(body).slice(0, 120)})`, url)\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new GoogleDirectoryShapeError(`Google Directory returned ${Array.isArray(parsed) ? \"an array\" : String(parsed)} where a JSON object was expected`, url)\n }\n return assertShape(parsed as Record<string, unknown>, url)\n }\n } catch (error) {\n if (error instanceof GoogleDirectoryAuthError || error instanceof GoogleDirectoryShapeError) throw error\n if (error instanceof GoogleDirectoryHttpError && !isTransient(error.status, error.reason)) throw error\n lastError = error instanceof Error ? error : new Error(String(error))\n }\n if (attempt < retry.attempts) {\n // ジッタを入れる。入れないと、同時に弾かれた呼び出しが揃って同じ時刻に再送し、\n // レート制限を同じ形で踏み直す\n const backoff = retry.baseDelayMs * 2 ** (attempt - 1)\n await sleep(backoff + Math.floor(random() * retry.baseDelayMs))\n }\n }\n throw lastError\n}\n\n/** How a paginated listing is read. ページング API の読み方。 */\nexport type PagedReader<TItem> = {\n /** Builds the URL for a page token. ページトークンから URL を組み立てる処理。 */\n buildUrl: (pageToken: string | undefined) => string\n /** Pulls the items and the next token out of one page. 1 ページから要素と次トークンを取り出す。 */\n extract: (body: Record<string, unknown>, url: string) => { items: TItem[]; nextPageToken?: string }\n /**\n * Upper bound so a broken cursor cannot loop forever. 壊れたカーソルで無限ループしない上限。\n *\n * 同時に、このアダプタが扱えるグループの大きさの上限でもある。到達したら例外になるため、\n * 大きなグループを黙って切り捨てることはない。\n */\n maxPages: number\n}\n\n/**\n * Walks every page of a paginated list endpoint. ページング API の全ページを辿る処理。\n *\n * ⚠️ **途中で失敗したら例外を送出し、それまでに集めた分は捨てる。** 部分的な一覧を返すと、\n * 呼び出し側はそれを「上流の全て」と解釈して残りを削除する。\n *\n * @param reader How to build, parse and bound the listing. 一覧の組み立て・解析・上限。\n * @param config Token supplier and injected collaborators. トークン供給と注入した協調要素。\n * @returns Every item across every page. 全ページの全要素。\n * @throws {Error} On any page failure, a cursor that revisits a token, or the page bound.\n * ページ失敗・トークンの再訪・上限超過のいずれでも throw。\n */\nexport async function fetchAllPages<TItem>(reader: PagedReader<TItem>, config: GoogleHttpConfig): Promise<TItem[]> {\n const all: TItem[] = []\n // 直前のトークンとの比較だけでは A→B→A→B の循環を検出できず、上限まで往復してから落ちる。\n // レート制限のある API に対して 200 往復を空費するのは、それ自体が次の失敗の原因になる\n const seenTokens = new Set<string>()\n let pageToken: string | undefined\n for (let page = 0; page < reader.maxPages; page++) {\n const { items, nextPageToken } = await fetchJson(reader.buildUrl(pageToken), config, reader.extract)\n all.push(...items)\n if (!nextPageToken) return all\n if (seenTokens.has(nextPageToken)) throw new Error(`Google Directory paging revisited a page token; refusing to loop (${reader.buildUrl(pageToken)})`)\n seenTokens.add(nextPageToken)\n pageToken = nextPageToken\n }\n throw new Error(`Google Directory paging exceeded ${reader.maxPages} pages; refusing to report a partial member list`)\n}\n","/**\n * @module catalog\n * @description Lists the domain's groups so an admin surface can offer them for selection.\n * ドメインのグループを列挙し、管理面が選択肢として提示できるようにするモジュール。\n *\n * ⚠️ **同期には使わない。** 同期は「連携済みの 1 グループの所属」を読む ({@link createGoogleDirectoryProvider})。\n * こちらは「ドメインにどんなグループがあるか」を読む。用途が違えば必要な権限も失敗の意味も違うため、\n * ポートを分けてある — 同期ジョブは候補一覧を必要とせず、管理面はメンバー一覧を必要としない。\n *\n * ⚠️ **これは利便のための読み取りである。** 失敗しても管理面は開かなければならない。呼び出し側は\n * 例外を「選択欄を出さない」状態へ写すこと。ここで既定値へ倒す (空配列を返す) と、上流に\n * グループが 1 件も無いことと、上流へ届かないことが区別できなくなる。\n */\nimport type { AuthDirectoryCatalogGroup } from \"@aiquants/auth-core\"\nimport { describeUntrusted, fetchAllPages, GoogleDirectoryShapeError, type GoogleHttpConfig } from \"./http\"\n\n/** Admin SDK Directory API v1. */\nconst DIRECTORY_BASE = \"https://admin.googleapis.com/admin/directory/v1\"\n\n/**\n * One group as the catalog reports it. 候補一覧が報告するグループ 1 件。\n *\n * ⚠️ **別名ではなく `@aiquants/auth-core` の型そのものである。** この形は上流アダプタ (ここ)・\n * 永続化層・管理 UI の 3 層を通る。層ごとに「同じ意味の別の型」を宣言すると、構造的部分型の\n * せいでどの境界も通ってしまい、**上流へ足した欄が途中の層で黙って捨てられる** — 読み手側の\n * 境界は型エラーを出さないので、足した本人も気付けない。定義は 1 つに置き、各層が import する。\n */\nexport type DirectoryCatalogGroup = AuthDirectoryCatalogGroup\n\n/** Configuration of the catalog reader. 候補一覧リーダーの構成。 */\nexport type GoogleDirectoryCatalogConfig = GoogleHttpConfig & {\n /**\n * Customer whose groups are listed. 列挙対象の顧客。\n *\n * 既定の `my_customer` は「借用先ユーザーが属する顧客」を指す。複数顧客を扱う配備だけが\n * 明示する必要がある。\n */\n customerId?: string\n /** Page size, 1..200 (the Admin SDK maximum). ページ長 (Admin SDK の上限は 200)。 */\n pageSize?: number\n /** Page limit, so a runaway cursor cannot loop forever. ページ数上限 (暴走の歯止め)。 */\n maxPages?: number\n}\n\n/**\n * Reads a string field, treating anything else as absent. 文字列項目を読む処理 (他の型は不在扱い)。\n *\n * @param value Raw field value. 生の項目値。\n * @returns The trimmed string, or `\"\"`. 前後の空白を除いた文字列、または空文字。\n */\nfunction str(value: unknown): string {\n return typeof value === \"string\" ? value.trim() : \"\"\n}\n\n/**\n * Creates the catalog reader. 候補一覧リーダーを生成する処理。\n *\n * @param config Transport and paging configuration. トランスポートとページングの構成。\n * @returns An object exposing `listGroups`. `listGroups` を持つオブジェクト。\n * @throws {TypeError} When the paging configuration is unusable. ページング構成が使えない場合。\n */\nexport function createGoogleDirectoryCatalog(config: GoogleDirectoryCatalogConfig): { listGroups: () => Promise<DirectoryCatalogGroup[]> } {\n const pageSize = config.pageSize ?? 200\n const maxPages = config.maxPages ?? 200\n const customerId = (config.customerId ?? \"my_customer\").trim()\n // 既定へ倒さず結線時点で落とす。0 を渡すと Admin SDK の最小値 1 を下回り、毎回 400 になる\n if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 200) throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(config.pageSize)}`)\n if (!Number.isInteger(maxPages) || maxPages < 1) throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(config.maxPages)}`)\n if (customerId === \"\") throw new TypeError(\"[@aiquants/auth-google-directory] customerId must not be blank\")\n\n return {\n async listGroups(): Promise<DirectoryCatalogGroup[]> {\n const rows = await fetchAllPages<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown; directMembersCount?: unknown }>(\n {\n buildUrl: (pageToken) => `${DIRECTORY_BASE}/groups?customer=${encodeURIComponent(customerId)}&maxResults=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n // `kind` を要求するのは、企業プロキシが遮断やセッション切れを HTTP 200 + JSON で\n // 返すためである。その本文には `groups` が無いので、検証しないと「グループが\n // 1 件も無いドメイン」と区別できない\n // ⚠️ `GoogleDirectoryShapeError` を投げる。素の `Error` は `fetchJson` から\n // 見て「一時的な失敗」であり、待っても直らないこの応答を 4 回再送する。\n // ⚠️ `kind` は上流 (あるいは割り込んだプロキシ) が決めた値なので、そのまま\n // 文へ入れない。長さを切り、資格情報が混じっていれば伏せる\n if (body.kind !== \"admin#directory#groups\") throw new GoogleDirectoryShapeError(`expected an admin#directory#groups document but the body carried kind=${describeUntrusted(body.kind)}`, url)\n const groups = body.groups\n // グループが 1 件も無いドメインでは `groups` が現れない。空配列として扱う\n if (groups === undefined) return { items: [], nextPageToken: str(body.nextPageToken) || undefined }\n if (!Array.isArray(groups)) throw new GoogleDirectoryShapeError(\"expected groups to be an array\", url)\n return { items: groups as Array<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown; directMembersCount?: unknown }>, nextPageToken: str(body.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n return rows\n .map((row) => {\n const externalId = str(row.id)\n const externalKey = str(row.email)\n // 識別子かアドレスを欠く行は選択肢にならない。落とさず残すと、選んだ瞬間に失敗する\n if (externalId === \"\" || externalKey === \"\") return null\n // `directMembersCount` は文字列で返ることがある。数として読めないなら伏せる\n const rawCount = Number(str(row.directMembersCount) || row.directMembersCount)\n const memberCount = Number.isInteger(rawCount) && rawCount >= 0 ? rawCount : undefined\n return {\n externalId,\n externalKey,\n name: str(row.name) || externalKey,\n ...(memberCount === undefined ? {} : { memberCount }),\n ...(str(row.description) === \"\" ? {} : { description: str(row.description) }),\n }\n })\n .filter((row): row is DirectoryCatalogGroup => row !== null)\n },\n }\n}\n","/**\n * @module provider\n * @description Google Workspace implementation of the `DirectoryProvider` port.\n * `DirectoryProvider` ポートの Google Workspace 実装。\n *\n * 直接所属は Admin SDK Directory、入れ子を展開した所属は Cloud Identity から取る。**書込\n * メソッドは 1 つも持たない** — 同期が上流を書き換える経路を、このパッケージに存在させない。\n *\n * 資格情報はトークン供給関数として注入する。おかげで本モジュールは Google の SDK に一切\n * 依存せず、ネットワークもトークンも無しで全分岐をテストできる。\n */\nimport type { AuthDirectoryMembershipMode, DirectoryGroup, DirectoryMember, DirectoryProvider } from \"@aiquants/auth-directory-core\"\nimport { fetchAllPages, fetchJson, GoogleDirectoryHttpError, GoogleDirectoryShapeError, type GoogleHttpConfig } from \"./http\"\n\nconst DIRECTORY_BASE = \"https://admin.googleapis.com/admin/directory/v1\"\nconst CLOUD_IDENTITY_BASE = \"https://cloudidentity.googleapis.com/v1\"\n\n/** Configuration of the Google directory provider. Google ディレクトリ provider の構成。 */\nexport type GoogleDirectoryConfig = GoogleHttpConfig & {\n /**\n * Page size for member listings. メンバー一覧のページサイズ。\n *\n * Admin SDK の `maxResults` は 1..200。Cloud Identity の `pageSize` は既定 200・最大 1000 で\n * 上限が違うため、両者で共有できる範囲に制限する。\n */\n pageSize?: number\n /**\n * Page bound, and therefore the largest group this adapter will report. ページ上限。\n *\n * 既定 200 ページ × 200 件 = 40,000 人。超えるグループは切り捨てではなく例外になる。\n * 実際にそれより大きなグループを扱う配備は、ここを明示的に引き上げること。\n */\n maxPages?: number\n}\n\n/** Member kinds the Admin SDK reports. Admin SDK が報告するメンバー種別。 */\nconst MEMBER_TYPES = { person: new Set([\"USER\", \"EXTERNAL\"]), group: new Set([\"GROUP\"]) }\n/** Member statuses that mean the account can actually be used. 実際に使えることを意味する状態。 */\nconst USABLE_STATUSES = new Set([\"ACTIVE\", \"\"])\n/** Statuses that mean the account exists but cannot be used. 存在するが使えないことを意味する状態。 */\nconst UNUSABLE_STATUSES = new Set([\"SUSPENDED\", \"ARCHIVED\", \"INVITED\", \"PENDING\"])\n\n/**\n * Rejects a group identifier that would change the meaning of the request URL.\n * リクエスト URL の意味を変えてしまうグループ識別子を拒否する処理。\n *\n * 否定リストではなく **肯定的な文字クラス** で検証する。以前は `/` `?` `#` だけを弾いており、\n * `..` が通っていた。`encodeURIComponent(\"..\")` は `..` のままなので、URL 解析器が経路を\n * 畳んで別のエンドポイントを叩き、Google が 404 を返し、呼び出し側が「上流でグループが消えた」と\n * 解釈してローカルのグループを無効化していた。\n *\n * @throws {TypeError} When the identifier is not a plain id or address. 素の ID・アドレスでない場合。\n */\nfunction assertExternalId(externalId: string): string {\n const trimmed = externalId.trim()\n // Google のグループ ID は英数字、アドレスはメール形式。どちらもこの集合に収まる\n if (!/^[A-Za-z0-9][A-Za-z0-9._%+-]*(@[A-Za-z0-9][A-Za-z0-9.-]*\\.[A-Za-z]{2,})?$/.test(trimmed)) {\n throw new TypeError(`[@aiquants/auth-google-directory] externalId must be a plain identifier or address, received ${JSON.stringify(externalId)}`)\n }\n return trimmed\n}\n\n/** Reads a string field, treating absent and non-string alike. 文字列項目の読み取り。 */\nconst str = (v: unknown): string => (typeof v === \"string\" ? v.trim() : \"\")\n\n/**\n * Creates a read-only Google Workspace directory provider.\n * 読み取り専用の Google Workspace ディレクトリ provider を生成する処理。\n *\n * @param config Token supplier, injected fetch/sleep, retry policy and bounds. 構成。\n * @returns The provider. provider。\n * @throws {TypeError} When a bound is outside its usable range. 上限が使用可能な範囲外の場合。\n */\nexport function createGoogleDirectoryProvider(config: GoogleDirectoryConfig): DirectoryProvider {\n const pageSize = config.pageSize ?? 200\n const maxPages = config.maxPages ?? 200\n // 既定へ倒さず結線時点で落とす。0 を渡すと Admin SDK の最小値 1 を下回り、毎回 400 になる\n if (!Number.isInteger(pageSize) || pageSize < 1 || pageSize > 200) throw new TypeError(`[@aiquants/auth-google-directory] pageSize must be an integer within 1..200 (the Admin SDK maximum), received ${String(config.pageSize)}`)\n if (!Number.isInteger(maxPages) || maxPages < 1) throw new TypeError(`[@aiquants/auth-google-directory] maxPages must be a positive integer, received ${String(config.maxPages)}`)\n\n /**\n * Refuses a body that is not the document this endpoint returns.\n * このエンドポイントが返す文書でない本文を拒否する処理。\n *\n * `kind` を要求するのは、企業プロキシが遮断やセッション切れを HTTP 200 + JSON で返すため\n * である。その本文には `members` が無いので、検証しないと「空のグループ」と区別できず、\n * 全員が削除対象になる。Admin SDK は空のグループでも `kind` を必ず返す。\n */\n function assertKind<T>(expected: string, body: Record<string, unknown>, url: string): T {\n if (str(body.kind) !== expected) {\n throw new GoogleDirectoryShapeError(`expected a ${expected} document but the body carried kind=${JSON.stringify(body.kind ?? null)}; refusing to treat it as an empty result`, url)\n }\n return body as T\n }\n\n /**\n * Refuses a Cloud Identity body carrying keys this endpoint never returns.\n * このエンドポイントが返さないキーを持つ Cloud Identity の本文を拒否する処理。\n *\n * Cloud Identity には `kind` に相当する判別子が無い。代わりに、想定外のキーを持つ本文を\n * 拒否する。遮断ページは `error` や `message` を持つため、これで区別できる。\n */\n function assertKeys<T>(allowed: readonly string[], body: Record<string, unknown>, url: string): T {\n const unexpected = Object.keys(body).filter((k) => !allowed.includes(k))\n if (unexpected.length > 0) {\n throw new GoogleDirectoryShapeError(`body carried unexpected keys (${unexpected.join(\", \")}); refusing to treat it as an empty result`, url)\n }\n return body as T\n }\n\n /**\n * Converts an upstream row into a member, refusing rows it cannot classify.\n * 上流の行をメンバーへ変換する処理。分類できない行は拒否する。\n *\n * 黙って捨てるとメンバーが 1 人減り、その差分が削除として実行される。\n */\n function toMember(email: string, externalUserId?: string): DirectoryMember {\n if (email === \"\") throw new GoogleDirectoryShapeError(\"Google Directory returned a member row without an address; refusing to report an incomplete member list\", DIRECTORY_BASE)\n return externalUserId ? { email, externalUserId } : { email }\n }\n\n /**\n * Fetches one group's metadata. 1 グループのメタデータを取得する処理。\n *\n * `this` を使わずクロージャとして持つ。オブジェクトのメソッドとして `this.getGroup` を\n * 呼ぶと、分割代入やコールバック経由で渡された時点で実行時に落ちる。\n */\n async function getGroup(externalId: string): Promise<DirectoryGroup | null> {\n const id = assertExternalId(externalId)\n try {\n const group = await fetchJson<{ id?: unknown; email?: unknown; name?: unknown; description?: unknown }>(`${DIRECTORY_BASE}/groups/${encodeURIComponent(id)}`, config, (body, url) => assertKind(\"admin#directory#group\", body, url))\n const stableId = str(group.id)\n const email = str(group.email)\n if (stableId === \"\" || email === \"\") throw new GoogleDirectoryShapeError(`Google Directory returned an incomplete group resource for ${id}`, `${DIRECTORY_BASE}/groups/${id}`)\n return { externalId: stableId, externalKey: email, name: str(group.name) || email, ...(str(group.description) ? { description: str(group.description) } : {}) }\n } catch (error) {\n // 404 だけが「上流に無い」。他の失敗を null に畳むと、到達できないことが\n // 「グループが消えた」に化けて、生きているグループが無効化されてしまう\n if (error instanceof GoogleDirectoryHttpError && error.status === 404) return null\n throw error\n }\n }\n\n /** Direct membership via the Admin SDK. Admin SDK による直接所属。 */\n async function listDirectMembers(group: DirectoryGroup): Promise<DirectoryMember[]> {\n const id = assertExternalId(group.externalId)\n const rows = await fetchAllPages<{ id?: unknown; email?: unknown; type?: unknown; status?: unknown }>(\n {\n buildUrl: (pageToken) => `${DIRECTORY_BASE}/groups/${encodeURIComponent(id)}/members?maxResults=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n const page = assertKind<{ members?: Array<Record<string, unknown>>; nextPageToken?: string }>(\"admin#directory#members\", body, url)\n return { items: page.members ?? [], nextPageToken: str(page.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n const members: DirectoryMember[] = []\n for (const row of rows) {\n const type = str(row.type) || \"USER\"\n // 入れ子グループ自身の行は「人」ではない。direct では展開しない約束なので落とす\n if (MEMBER_TYPES.group.has(type)) continue\n // 未知の種別を人として扱うのも、黙って落とすのも誤り。CUSTOMER (ドメイン全体) を\n // 落とすとグループが 0 人に見え、そのまま全員削除の plan になる\n if (!MEMBER_TYPES.person.has(type)) throw new GoogleDirectoryShapeError(`Google Directory reported member type ${JSON.stringify(type)}, which this adapter cannot classify; refusing to guess whether it is a person`, DIRECTORY_BASE)\n const status = str(row.status)\n // 外部メンバーは status が空で返ることがある。空を「使える」と読むのは、Google が\n // 使えない状態には必ず名前を付けるからである\n if (UNUSABLE_STATUSES.has(status)) continue\n if (!USABLE_STATUSES.has(status)) throw new GoogleDirectoryShapeError(`Google Directory reported member status ${JSON.stringify(status)}, which this adapter cannot classify`, DIRECTORY_BASE)\n members.push(toMember(str(row.email), str(row.id) || undefined))\n }\n return members\n }\n\n /** Nested membership flattened via Cloud Identity. Cloud Identity による入れ子展開所属。 */\n async function listTransitiveMembers(group: DirectoryGroup): Promise<DirectoryMember[]> {\n const lookupUrl = `${CLOUD_IDENTITY_BASE}/groups:lookup?groupKey.id=${encodeURIComponent(group.externalKey)}`\n const looked = await fetchJson<{ name?: unknown }>(lookupUrl, config, (body, url) => assertKeys([\"name\"], body, url))\n const resourceName = str(looked.name)\n if (resourceName === \"\") throw new GoogleDirectoryShapeError(`Cloud Identity did not return a resource name for ${group.externalKey}`, lookupUrl)\n // URL のパスへそのまま差し込む唯一の値なので、形を検査する。応答本文が汚染されたとき\n // に `..` が入ると、Bearer トークンを載せた要求が別のパスへ向かう\n if (!/^groups\\/[A-Za-z0-9_-]+$/.test(resourceName)) {\n throw new GoogleDirectoryShapeError(`Cloud Identity returned a resource name this adapter will not put in a URL: ${JSON.stringify(resourceName)}`, lookupUrl)\n }\n\n const rows = await fetchAllPages<{ preferredMemberKey?: unknown; member?: unknown; relationType?: unknown }>(\n {\n buildUrl: (pageToken) => `${CLOUD_IDENTITY_BASE}/${resourceName}/memberships:searchTransitiveMemberships?pageSize=${pageSize}${pageToken ? `&pageToken=${encodeURIComponent(pageToken)}` : \"\"}`,\n extract: (body, url) => {\n const page = assertKeys<{ memberships?: Array<Record<string, unknown>>; nextPageToken?: string }>([\"memberships\", \"nextPageToken\"], body, url)\n return { items: page.memberships ?? [], nextPageToken: str(page.nextPageToken) || undefined }\n },\n maxPages,\n },\n config,\n )\n\n const members: DirectoryMember[] = []\n for (const row of rows) {\n const member = str(row.member)\n // 入れ子グループ自身も所属として返る。人として数えると、グループアドレスと同じ\n // ローカルユーザーが居た場合に親グループのロールを継承してしまう\n if (member.startsWith(\"groups/\")) continue\n if (!member.startsWith(\"users/\")) throw new GoogleDirectoryShapeError(`Cloud Identity reported a member resource ${JSON.stringify(member)}, which this adapter cannot classify; refusing to guess whether it is a person`, CLOUD_IDENTITY_BASE)\n const keys = Array.isArray(row.preferredMemberKey) ? (row.preferredMemberKey as Array<Record<string, unknown>>) : []\n if (keys.length !== 1) throw new GoogleDirectoryShapeError(`Cloud Identity reported ${keys.length} member keys for ${member}; refusing to pick one`, CLOUD_IDENTITY_BASE)\n // namespace 付きは外部 identity 由来で、id はアドレスではない。アドレスとして扱うと\n // 台帳に人でない文字列が入る\n if (str(keys[0]?.namespace) !== \"\") throw new GoogleDirectoryShapeError(`Cloud Identity reported an external-identity member (${str(keys[0]?.namespace)}); its id is not an address`, CLOUD_IDENTITY_BASE)\n // `users/<id>` の id を捨ててはならない。捨てると台帳の `external_user_id` が\n // 全行 NULL になり、「上流 ID 第一キー・アドレス第二キー」の防御が入れ子展開の\n // リンクでだけ無効化される — アドレスを引き継いだ後任者が前任者の所属を継承する\n const upstreamUserId = member.slice(\"users/\".length).trim()\n if (upstreamUserId === \"\") throw new GoogleDirectoryShapeError(`Cloud Identity reported an empty user id in ${JSON.stringify(member)}`, CLOUD_IDENTITY_BASE)\n members.push(toMember(str(keys[0]?.id), upstreamUserId))\n }\n return members\n }\n\n return {\n getGroup,\n async listGroupMembers(group: DirectoryGroup, mode: AuthDirectoryMembershipMode): Promise<DirectoryMember[]> {\n return mode === \"direct\" ? listDirectMembers(group) : listTransitiveMembers(group)\n },\n }\n}\n","/**\n * @module token\n * @description Domain-wide delegation token provider.\n * ドメイン全体の委任によるトークン供給。\n *\n * ⚠️ **借用先 (`subject`) には特権管理者を指定しないこと。** ドメイン全体の委任は「指定した\n * ユーザーとして振る舞う」機能であり、特権管理者を借用すると、この鍵は実質的に特権管理者の鍵に\n * なる。「グループ > 読み取り」だけを持つカスタム管理者ロールを割り当てた専用ユーザーを用意し、\n * それを指定すること。\n *\n * ⚠️ **スコープは readonly のみ。** 書込スコープを渡すと、このパッケージが読み取り専用である\n * という契約を、パッケージの外側で破ることになる。\n *\n * ただしこの検査が守れるのは、ここで組み立てるスコープ集合までである。**実際の境界は\n * `createClient` であり、呼び出し側は渡された scopes を無視して自前の JWT を作れる。** 読み取り\n * 専用であることは、このガードと、借用先ユーザーに与える管理者ロールの狭さの両方で担保する。\n */\nimport type { GoogleHttpConfig } from \"./http\"\n\n/** Read-only scopes this integration needs. 本連携が必要とする読み取り専用スコープ。 */\nexport const GOOGLE_DIRECTORY_READONLY_SCOPES = [\"https://www.googleapis.com/auth/admin.directory.group.readonly\", \"https://www.googleapis.com/auth/admin.directory.group.member.readonly\"] as const\n\n/** Additional read-only scope required to expand nested groups. 入れ子展開に要る追加スコープ。 */\nexport const GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE = \"https://www.googleapis.com/auth/cloud-identity.groups.readonly\"\n\n/** Minimal shape of `google-auth-library`'s JWT client. JWT クライアントの最小形。 */\nexport type JwtClientLike = {\n getAccessToken(): Promise<{ token?: string | null }>\n}\n\n/** Configuration of the delegation token provider. 委任トークン供給の構成。 */\nexport type DomainWideDelegationConfig = {\n /**\n * Builds the JWT client. Injected so this package does not depend on a Google SDK.\n * JWT クライアントを構築する処理。Google SDK への依存を持ち込まないため注入する。\n *\n * 呼び出し側の実装例:\n * `({ subject, scopes }) => new JWT({ keyFile, subject, scopes })`\n */\n createClient: (args: { subject: string; scopes: string[] }) => JwtClientLike\n /** Workspace user to impersonate. Must NOT be a super administrator. 借用先ユーザー。 */\n subject: string\n /** True when nested membership expansion is used anywhere. 入れ子展開を使う場合に true。 */\n includeNestedScope: boolean\n /**\n * Extra read-only scopes, when a deployment needs more than the defaults.\n * 既定に加えて要る読み取り専用スコープ (必要な配備のみ)。\n *\n * 受け取れるようにしてあるのは、読み取り専用の検査を **到達可能** にするためでもある。\n * 定数だけを検査する形では、検査は決して発火せず、契約を守っているように見えるだけになる。\n */\n additionalScopes?: readonly string[]\n}\n\n/** Rejects a scope that would grant more than reading. 読み取りを超える権限を与えるスコープを拒否する。 */\nfunction assertReadOnly(scopes: readonly string[]): void {\n const writable = scopes.filter((s) => !s.endsWith(\".readonly\"))\n if (writable.length > 0) {\n throw new TypeError(`[@aiquants/auth-google-directory] only read-only scopes are allowed, received ${writable.join(\", \")}`)\n }\n}\n\n/**\n * Creates a token supplier that impersonates a least-privileged directory reader.\n * 最小権限のディレクトリ読み取り専用ユーザーを借用するトークン供給を生成する処理。\n *\n * @param config Client factory, impersonated subject and nested-scope flag. 構成。\n * @returns A `getAccessToken` suitable for {@link GoogleHttpConfig}. トークン供給関数。\n * @throws {TypeError} When the subject is blank. 借用先が空の場合。\n *\n * トークンのキャッシュは `google-auth-library` の JWT クライアントが行う。ここで二重に\n * 貯めると、失効の扱いが 2 箇所に分かれて食い違う。\n */\nexport function createDomainWideDelegationTokenProvider(config: DomainWideDelegationConfig): GoogleHttpConfig[\"getAccessToken\"] {\n const subject = config.subject.trim()\n if (subject === \"\") throw new TypeError(\"[@aiquants/auth-google-directory] subject must be a non-empty Workspace user to impersonate\")\n const scopes = [...GOOGLE_DIRECTORY_READONLY_SCOPES, ...(config.includeNestedScope ? [GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE] : []), ...(config.additionalScopes ?? [])]\n assertReadOnly(scopes)\n const client = config.createClient({ subject, scopes })\n\n return async () => {\n const { token } = await client.getAccessToken()\n // トークンが取れないまま空文字で続行すると、401 が「メンバー 0 件」に化ける経路が生まれる\n if (!token) throw new Error(`[@aiquants/auth-google-directory] could not obtain an access token for ${subject}`)\n return token\n }\n}\n"],"mappings":"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","names":["GoogleDirectoryHttpError","message","status","url","reason","GoogleDirectoryAuthError","GoogleDirectoryShapeError","RATE_LIMIT_REASONS","extractReason","body","parsed","isTransient","MAX_REDACTED_LENGTH","redact","text","describeUntrusted","value","defaultRetry","assertRetry","retry","fetchJson","config","assertShape","doFetch","sleep","ms","random","lastError","attempt","token","error","controller","timer","response","failure","backoff","fetchAllPages","reader","all","seenTokens","pageToken","page","items","nextPageToken","DIRECTORY_BASE","str","value","createGoogleDirectoryCatalog","config","pageSize","maxPages","customerId","fetchAllPages","pageToken","body","url","GoogleDirectoryShapeError","describeUntrusted","groups","row","externalId","externalKey","rawCount","memberCount","DIRECTORY_BASE","CLOUD_IDENTITY_BASE","MEMBER_TYPES","USABLE_STATUSES","UNUSABLE_STATUSES","assertExternalId","externalId","trimmed","str","v","createGoogleDirectoryProvider","config","pageSize","maxPages","assertKind","expected","body","url","GoogleDirectoryShapeError","assertKeys","allowed","unexpected","k","toMember","email","externalUserId","getGroup","id","group","fetchJson","stableId","error","GoogleDirectoryHttpError","listDirectMembers","rows","fetchAllPages","pageToken","page","members","row","type","status","listTransitiveMembers","lookupUrl","looked","resourceName","member","keys","upstreamUserId","mode","GOOGLE_DIRECTORY_READONLY_SCOPES","GOOGLE_CLOUD_IDENTITY_READONLY_SCOPE","assertReadOnly","scopes","writable","createDomainWideDelegationTokenProvider","config","subject","client","token"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/auth-google-directory",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Google Workspace implementation of the @aiquants/auth-directory-core DirectoryProvider port: Admin SDK Directory for direct membership, Cloud Identity for nested membership, and a domain-wide delegation token provider. Read-only by construction — the injected fetch type has no slot for a method or a body.",
5
5
  "sideEffects": false,
6
6
  "main": "dist/index.js",
@@ -33,6 +33,7 @@
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
35
  "google-auth-library": ">=9",
36
+ "@aiquants/auth-core": "^0.6.0",
36
37
  "@aiquants/auth-directory-core": "^0.2.0"
37
38
  },
38
39
  "peerDependenciesMeta": {
@@ -47,6 +48,7 @@
47
48
  "tsup": "^8.5.1",
48
49
  "typescript": "^5.9.3",
49
50
  "vitest": "^4.1.8",
51
+ "@aiquants/auth-core": "0.6.0",
50
52
  "@aiquants/auth-directory-core": "0.2.0"
51
53
  },
52
54
  "engines": {