Merge pull request #1 from Sea-Haven-Industries/feature/initial-setup

Add Google Workspace to Front user sync stack
This commit is contained in:
Adam Moussa 2026-05-11 14:26:59 -04:00 • committed by GitHub
commit 4b7bb97566
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 969 additions and 0 deletions

11
.github/workflows/ci.yaml vendored Normal file
View file

@ -0,0 +1,11 @@
name: CI
on:
pull_request:
branches: [main]
jobs:
ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@main
with:
run-typecheck: false
run-cdk-synth: true

18
.github/workflows/deploy.yaml vendored Normal file
View file

@ -0,0 +1,18 @@
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
concurrency:
group: deploy
cancel-in-progress: false
jobs:
deploy:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@main
secrets:
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
node_modules/
cdk.out/
.env
__pycache__/
.aws-sam/
PLAN.md

93
README.md Normal file
View file

@ -0,0 +1,93 @@
# google-user-sync
A scheduled Lambda that pulls user profile data (job title, phone) from Google Workspace Admin Directory API and syncs it to Front teammate custom fields via the Front Core API. Runs weekdays at 6:00 AM ET via EventBridge.
## Architecture
```
EventBridge (weekday cron, 6:00 AM ET)
|
v
Lambda (Node.js 22, arm64)
|
+-- Secrets Manager --> google-user-sync/google-service-account
+-- Secrets Manager --> google-user-sync/front-api-token
|
+-- GET Google Admin Directory API --> list users in target OUs
+-- PATCH Front API /teammates/alt:email:{email} --> update custom_fields
```
## AWS Resources
- **Stack:** `google-user-sync` (CDK, us-east-1)
- **Lambda:** `google-user-sync` — Node.js 22, arm64, 256 MB, 5 min timeout, 60-day log retention
- **EventBridge Rule:** `google-user-sync-daily` — weekdays at 11:00 UTC
- **Secrets Manager:** `google-user-sync/google-service-account`, `google-user-sync/front-api-token`
## Prerequisites
### 1. Create Custom Fields in Front
1. Gear icon > Company Settings > Custom Fields > Teammates tab
2. Create two fields:
- **Job Title** (String)
- **Phone** (String)
### 2. Generate a Front API Token
1. Gear icon > Company Settings > Developers > API tokens
2. Create token with **Shared resources** scope (teammate read/write)
### 3. Create a Google Cloud Service Account
1. Enable the **Admin SDK API** in Google Cloud Console
2. Create a service account named `front-directory-sync`
3. Create a JSON key and download it
4. Enable Domain-Wide Delegation and copy the Client ID
### 4. Authorize in Google Workspace Admin
1. Security > Access and Data Control > API Controls > Domain-Wide Delegation
2. Add the Client ID with scope: `https://www.googleapis.com/auth/admin.directory.user.readonly`
### 5. Populate Google User Profiles
Ensure each user has their **Job title** and **Phone** filled in under Directory > Users.
### 6. Store Secrets in AWS Secrets Manager
```bash
aws secretsmanager create-secret \
--name "google-user-sync/front-api-token" \
--secret-string "YOUR_FRONT_API_TOKEN" \
--region us-east-1
aws secretsmanager create-secret \
--name "google-user-sync/google-service-account" \
--secret-string file://path-to-service-account-key.json \
--region us-east-1
```
## Deployment
```bash
npm install
npx cdk deploy google-user-sync
```
## Verification
```bash
aws lambda invoke \
--function-name google-user-sync \
--region us-east-1 \
output.json && cat output.json
```
Check Front > Company Settings > Teammates > pick a user > Custom Fields to confirm Job Title and Phone are populated.
## Maintenance
- **New teammates:** Automatically picked up if they exist in both Google Workspace and Front with the same email.
- **Schedule changes:** Update the EventBridge rule in `lib/google-user-sync-stack.js`.
- **Additional fields:** Add as custom fields in Front, then update the field mapping in `lambda/index.js`.

13
bin/app.js Normal file
View file

@ -0,0 +1,13 @@
#!/usr/bin/env node
const cdk = require("aws-cdk-lib");
const { GoogleUserSyncStack } = require("../lib/google-user-sync-stack");
const app = new cdk.App();
new GoogleUserSyncStack(app, "GoogleUserSyncStack", {
stackName: "google-user-sync",
env: {
account: "328440206208",
region: "us-east-1",
},
});

3
cdk.json Normal file
View file

@ -0,0 +1,3 @@
{
"app": "node bin/app.js"
}

288
lambda/index.js Normal file
View file

@ -0,0 +1,288 @@
/**
* google-user-sync Lambda
*
* Pulls user profiles from Google Workspace Admin Directory API
* and syncs job title + phone to Front teammate custom fields.
*
* Environment variables:
* GOOGLE_SECRET_NAME - Secrets Manager name for Google service account JSON key
* FRONT_SECRET_NAME - Secrets Manager name for Front API token
* GOOGLE_ADMIN_EMAIL - Email of a Google Workspace admin to impersonate
* GOOGLE_DOMAIN - Domain to list users for (e.g. seahaven.com)
* GOOGLE_OUS - Comma-separated org unit paths to sync (e.g. /Office/Scheduling,/Office/Operations)
*/
const {
SecretsManagerClient,
GetSecretValueCommand,
} = require("@aws-sdk/client-secrets-manager");
const crypto = require("crypto");
const sm = new SecretsManagerClient({ region: "us-east-1" });
// ---------------------------------------------------------------------------
// Google Auth (JWT → Access Token using service account)
// ---------------------------------------------------------------------------
function base64url(buf) {
return Buffer.from(buf)
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
async function getGoogleAccessToken(serviceAccountKey, adminEmail) {
const now = Math.floor(Date.now() / 1000);
const header = { alg: "RS256", typ: "JWT" };
const payload = {
iss: serviceAccountKey.client_email,
sub: adminEmail,
scope: "https://www.googleapis.com/auth/admin.directory.user.readonly",
aud: "https://oauth2.googleapis.com/token",
iat: now,
exp: now + 3600,
};
const segments = [
base64url(JSON.stringify(header)),
base64url(JSON.stringify(payload)),
];
const signingInput = segments.join(".");
const sign = crypto.createSign("RSA-SHA256");
sign.update(signingInput);
const signature = sign.sign(serviceAccountKey.private_key);
const jwt = signingInput + "." + base64url(signature);
const res = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: jwt,
}),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Google token exchange failed (${res.status}): ${text}`);
}
const data = await res.json();
return data.access_token;
}
// ---------------------------------------------------------------------------
// Google Admin Directory: list users for a specific OU
// ---------------------------------------------------------------------------
async function listGoogleUsersInOU(accessToken, domain, orgUnitPath) {
const users = [];
let pageToken = null;
do {
const params = new URLSearchParams({
domain,
maxResults: "500",
projection: "full",
orderBy: "email",
query: `orgUnitPath='${orgUnitPath}'`,
});
if (pageToken) params.set("pageToken", pageToken);
const res = await fetch(
`https://admin.googleapis.com/admin/directory/v1/users?${params}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
if (!res.ok) {
const text = await res.text();
throw new Error(
`Google Directory API error for OU "${orgUnitPath}" (${res.status}): ${text}`
);
}
const data = await res.json();
if (data.users) users.push(...data.users);
pageToken = data.nextPageToken || null;
} while (pageToken);
return users;
}
async function listGoogleUsers(accessToken, domain, orgUnits) {
const allUsers = [];
const seen = new Set();
for (const ou of orgUnits) {
console.log(`Fetching users from OU: ${ou}`);
const users = await listGoogleUsersInOU(accessToken, domain, ou);
console.log(` Found ${users.length} users in ${ou}`);
for (const user of users) {
if (!seen.has(user.primaryEmail)) {
seen.add(user.primaryEmail);
allUsers.push(user);
}
}
}
return allUsers;
}
// ---------------------------------------------------------------------------
// Extract fields from a Google user object
// ---------------------------------------------------------------------------
function extractUserFields(googleUser) {
const email = googleUser.primaryEmail;
let jobTitle = "";
if (googleUser.organizations && googleUser.organizations.length > 0) {
const primaryOrg = googleUser.organizations.find((o) => o.primary) || googleUser.organizations[0];
jobTitle = primaryOrg.title || "";
}
let phone = "";
if (googleUser.phones && googleUser.phones.length > 0) {
const workPhone = googleUser.phones.find((p) => p.type === "work" && p.value);
const primaryPhone = googleUser.phones.find((p) => p.primary && p.value) || googleUser.phones.find((p) => p.value);
phone = (workPhone || primaryPhone || {}).value || "";
}
return { email, jobTitle, phone };
}
// ---------------------------------------------------------------------------
// Front API: update teammate custom fields
// ---------------------------------------------------------------------------
async function updateFrontTeammate(frontToken, email, customFields) {
const url = `https://api2.frontapp.com/teammates/alt:email:${encodeURIComponent(email)}`;
const headers = {
Authorization: `Bearer ${frontToken}`,
"Content-Type": "application/json",
Accept: "application/json",
};
const getRes = await fetch(url, { headers });
if (getRes.status === 404) return { status: "not_in_front" };
if (!getRes.ok) {
const text = await getRes.text();
return { status: "error", code: getRes.status, message: text };
}
const teammate = await getRes.json();
const merged = { ...teammate.custom_fields, ...customFields };
const patchRes = await fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify({ custom_fields: merged }),
});
if (patchRes.status === 204) return { status: "updated" };
const text = await patchRes.text();
return { status: "error", code: patchRes.status, message: text };
}
// ---------------------------------------------------------------------------
// Handler
// ---------------------------------------------------------------------------
exports.handler = async (event) => {
console.log("Starting Front ← Google Directory sync");
// 1. Fetch secrets
const [googleSecretRes, frontSecretRes] = await Promise.all([
sm.send(
new GetSecretValueCommand({ SecretId: process.env.GOOGLE_SECRET_NAME })
),
sm.send(
new GetSecretValueCommand({ SecretId: process.env.FRONT_SECRET_NAME })
),
]);
const googleKey = JSON.parse(googleSecretRes.SecretString);
const frontToken = frontSecretRes.SecretString;
// 2. Get Google access token
const accessToken = await getGoogleAccessToken(
googleKey,
process.env.GOOGLE_ADMIN_EMAIL
);
console.log("Obtained Google access token");
// 3. List Google Workspace users from specified OUs
const orgUnits = process.env.GOOGLE_OUS
.split(",")
.map((ou) => ou.trim())
.filter(Boolean);
console.log(`Syncing OUs: ${orgUnits.join(", ")}`);
const googleUsers = await listGoogleUsers(
accessToken,
process.env.GOOGLE_DOMAIN,
orgUnits
);
console.log(`Found ${googleUsers.length} total users across ${orgUnits.length} OUs`);
// 4. Sync each user to Front
const summary = {
updated: 0,
notInFront: 0,
noData: 0,
errors: 0,
errorDetails: [],
};
for (const googleUser of googleUsers) {
const { email, jobTitle, phone } = extractUserFields(googleUser);
// Skip users with no title AND no phone — nothing to sync
if (!jobTitle && !phone) {
console.log(`Skipping ${email} — no title or phone in Google`);
summary.noData++;
continue;
}
const customFields = {};
if (jobTitle) customFields["Job Title"] = jobTitle;
if (phone) customFields["Phone"] = phone;
try {
const result = await updateFrontTeammate(frontToken, email, customFields);
if (result.status === "updated") {
console.log(`Updated ${email}: ${JSON.stringify(customFields)}`);
summary.updated++;
} else if (result.status === "not_in_front") {
console.log(`Skipped ${email} — not a Front teammate`);
summary.notInFront++;
} else {
console.error(`Error updating ${email}: ${result.code} ${result.message}`);
summary.errors++;
summary.errorDetails.push({
email,
message: `${result.code}: ${result.message}`,
});
}
} catch (err) {
console.error(`Exception updating ${email}:`, err);
summary.errors++;
summary.errorDetails.push({ email, message: err.message });
}
// Simple rate-limit courtesy — Front API has rate limits
await new Promise((r) => setTimeout(r, 200));
}
console.log("Sync summary:", JSON.stringify(summary));
return {
statusCode: 200,
body: summary,
};
};

9
lambda/package.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "google-user-sync",
"version": "1.0.0",
"description": "Syncs Google Workspace user profiles to Front teammate custom fields",
"main": "index.js",
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.600.0"
}
}

View file

@ -0,0 +1,75 @@
const { Stack, Duration, RemovalPolicy, CfnOutput } = require("aws-cdk-lib");
const lambda = require("aws-cdk-lib/aws-lambda");
const events = require("aws-cdk-lib/aws-events");
const targets = require("aws-cdk-lib/aws-events-targets");
const secretsmanager = require("aws-cdk-lib/aws-secretsmanager");
const logs = require("aws-cdk-lib/aws-logs");
const path = require("path");
class GoogleUserSyncStack extends Stack {
constructor(scope, id, props) {
super(scope, id, props);
// -----------------------------------------------------------------
// Reference existing secrets (created manually per PLAN.md Step 6)
// -----------------------------------------------------------------
const googleSecret = secretsmanager.Secret.fromSecretNameV2(
this,
"GoogleServiceAccountSecret",
"google-user-sync/google-service-account"
);
const frontSecret = secretsmanager.Secret.fromSecretNameV2(
this,
"FrontApiTokenSecret",
"google-user-sync/front-api-token"
);
// -----------------------------------------------------------------
// Lambda function
// -----------------------------------------------------------------
const syncFn = new lambda.Function(this, "GoogleUserSyncFn", {
functionName: "google-user-sync",
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64,
handler: "index.handler",
code: lambda.Code.fromAsset(path.join(__dirname, "..", "lambda")),
timeout: Duration.minutes(5),
memorySize: 256,
environment: {
GOOGLE_SECRET_NAME: "google-user-sync/google-service-account",
FRONT_SECRET_NAME: "google-user-sync/front-api-token",
GOOGLE_ADMIN_EMAIL: "adam@seahavenind.com",
GOOGLE_DOMAIN: "seahavenind.com",
GOOGLE_OUS: "/Office/Scheduling,/Office/Operations",
},
logRetention: logs.RetentionDays.TWO_MONTHS,
});
// Grant the Lambda read access to both secrets
googleSecret.grantRead(syncFn);
frontSecret.grantRead(syncFn);
// -----------------------------------------------------------------
// EventBridge scheduled rule — daily at 6:00 AM ET (11:00 UTC)
// -----------------------------------------------------------------
const rule = new events.Rule(this, "DailySyncRule", {
ruleName: "google-user-sync-daily",
schedule: events.Schedule.cron({
minute: "0",
hour: "11", // 11:00 UTC = 6:00 AM ET (EST+5) / 7:00 AM EDT
month: "*",
weekDay: "MON-FRI",
year: "*",
}),
});
rule.addTarget(new targets.LambdaFunction(syncFn));
new CfnOutput(this, "SyncFunctionArn", {
value: syncFn.functionArn,
});
}
}
module.exports = { GoogleUserSyncStack };

444
package-lock.json generated Normal file
View file

@ -0,0 +1,444 @@
{
"name": "google-user-sync",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "google-user-sync",
"version": "1.0.0",
"dependencies": {
"aws-cdk-lib": "^2.150.0",
"constructs": "^10.0.0"
}
},
"node_modules/@aws-cdk/asset-awscli-v1": {
"version": "2.2.273",
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz",
"integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==",
"license": "Apache-2.0"
},
"node_modules/@aws-cdk/asset-node-proxy-agent-v6": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz",
"integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==",
"license": "Apache-2.0"
},
"node_modules/@aws-cdk/cloud-assembly-schema": {
"version": "53.22.0",
"resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.22.0.tgz",
"integrity": "sha512-2GLhjjf7Db697rRyYt6rjN06fwbBIiewPo/jxSCyUN259ejF7NQQRwvrdAQb9Aq2jPaIIp1cfHSoEdXyA6Bb7Q==",
"bundleDependencies": [
"jsonschema",
"semver"
],
"license": "Apache-2.0",
"dependencies": {
"jsonschema": "~1.4.1",
"semver": "^7.7.4"
},
"engines": {
"node": ">= 18.0.0"
}
},
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": {
"version": "1.4.1",
"inBundle": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": {
"version": "7.7.4",
"inBundle": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aws-cdk-lib": {
"version": "2.253.1",
"resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.253.1.tgz",
"integrity": "sha512-vy+hA15/ZfSQpivkNdlIn2ZDA2hesp3WJgmtIZJDFwu6xzwv7wH7glbAdu5xCHGcOjepOaTKZSvCPC6sN+0/Vw==",
"bundleDependencies": [
"@balena/dockerignore",
"@aws-cdk/cloud-assembly-api",
"case",
"fs-extra",
"ignore",
"jsonschema",
"minimatch",
"punycode",
"semver",
"table",
"yaml",
"mime-types"
],
"license": "Apache-2.0",
"dependencies": {
"@aws-cdk/asset-awscli-v1": "2.2.273",
"@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1",
"@aws-cdk/cloud-assembly-api": "^2.2.2",
"@aws-cdk/cloud-assembly-schema": "^53.18.0",
"@balena/dockerignore": "^1.0.2",
"case": "1.6.3",
"fs-extra": "^11.3.3",
"ignore": "^5.3.2",
"jsonschema": "^1.5.0",
"mime-types": "^2.1.35",
"minimatch": "^10.2.3",
"punycode": "^2.3.1",
"semver": "^7.7.4",
"table": "^6.9.0",
"yaml": "1.10.3"
},
"engines": {
"node": ">= 20.0.0"
},
"peerDependencies": {
"constructs": "^10.5.0"
}
},
"node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": {
"version": "2.2.2",
"bundleDependencies": [
"jsonschema",
"semver"
],
"inBundle": true,
"license": "Apache-2.0",
"dependencies": {
"jsonschema": "~1.4.1",
"semver": "^7.7.4"
},
"engines": {
"node": ">= 18.0.0"
},
"peerDependencies": {
"@aws-cdk/cloud-assembly-schema": ">=53.15.0"
}
},
"node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": {
"version": "1.0.2",
"inBundle": true,
"license": "Apache-2.0"
},
"node_modules/aws-cdk-lib/node_modules/ajv": {
"version": "8.18.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/aws-cdk-lib/node_modules/ansi-regex": {
"version": "5.0.1",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/ansi-styles": {
"version": "4.3.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/aws-cdk-lib/node_modules/astral-regex": {
"version": "2.0.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/balanced-match": {
"version": "4.0.4",
"inBundle": true,
"license": "MIT",
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/aws-cdk-lib/node_modules/brace-expansion": {
"version": "5.0.5",
"inBundle": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
"node": "18 || 20 || >=22"
}
},
"node_modules/aws-cdk-lib/node_modules/case": {
"version": "1.6.3",
"inBundle": true,
"license": "(MIT OR GPL-3.0-or-later)",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/aws-cdk-lib/node_modules/color-convert": {
"version": "2.0.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/aws-cdk-lib/node_modules/color-name": {
"version": "1.1.4",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/emoji-regex": {
"version": "8.0.0",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/fast-deep-equal": {
"version": "3.1.3",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/fast-uri": {
"version": "3.1.0",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"inBundle": true,
"license": "BSD-3-Clause"
},
"node_modules/aws-cdk-lib/node_modules/fs-extra": {
"version": "11.3.3",
"inBundle": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/aws-cdk-lib/node_modules/graceful-fs": {
"version": "4.2.11",
"inBundle": true,
"license": "ISC"
},
"node_modules/aws-cdk-lib/node_modules/ignore": {
"version": "5.3.2",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/json-schema-traverse": {
"version": "1.0.0",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/jsonfile": {
"version": "6.2.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/aws-cdk-lib/node_modules/jsonschema": {
"version": "1.5.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/aws-cdk-lib/node_modules/lodash.truncate": {
"version": "4.4.2",
"inBundle": true,
"license": "MIT"
},
"node_modules/aws-cdk-lib/node_modules/mime-db": {
"version": "1.52.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/aws-cdk-lib/node_modules/mime-types": {
"version": "2.1.35",
"inBundle": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/aws-cdk-lib/node_modules/minimatch": {
"version": "10.2.5",
"inBundle": true,
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/aws-cdk-lib/node_modules/punycode": {
"version": "2.3.1",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/aws-cdk-lib/node_modules/require-from-string": {
"version": "2.0.2",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/aws-cdk-lib/node_modules/semver": {
"version": "7.7.4",
"inBundle": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/aws-cdk-lib/node_modules/slice-ansi": {
"version": "4.0.0",
"inBundle": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/aws-cdk-lib/node_modules/string-width": {
"version": "4.2.3",
"inBundle": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/strip-ansi": {
"version": "6.0.1",
"inBundle": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/aws-cdk-lib/node_modules/table": {
"version": "6.9.0",
"inBundle": true,
"license": "BSD-3-Clause",
"dependencies": {
"ajv": "^8.0.1",
"lodash.truncate": "^4.4.2",
"slice-ansi": "^4.0.0",
"string-width": "^4.2.3",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/aws-cdk-lib/node_modules/universalify": {
"version": "2.0.1",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/aws-cdk-lib/node_modules/yaml": {
"version": "1.10.3",
"inBundle": true,
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/constructs": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz",
"integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==",
"license": "Apache-2.0"
}
}
}

9
package.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "google-user-sync",
"version": "1.0.0",
"private": true,
"dependencies": {
"aws-cdk-lib": "^2.150.0",
"constructs": "^10.0.0"
}
}