@tyroneross/build-loop 0.30.3
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/.agents/plugins/marketplace.json +10 -0
- package/.claude-plugin/marketplace.json +38 -0
- package/.claude-plugin/plugin.json +30 -0
- package/.codex-plugin/plugin.json +41 -0
- package/.cursor/rules/build-loop-surface.mdc +22 -0
- package/AGENTS.md +498 -0
- package/LICENSE +202 -0
- package/README.md +403 -0
- package/agents/alignment-checker.md +124 -0
- package/agents/api-assessor.md +136 -0
- package/agents/architecture-scout.md +185 -0
- package/agents/assessment-orchestrator.md +163 -0
- package/agents/build-orchestrator.md +198 -0
- package/agents/database-assessor.md +133 -0
- package/agents/design-contract-specialist.md +239 -0
- package/agents/fact-checker.md +69 -0
- package/agents/fix-critique.md +161 -0
- package/agents/frontend-assessor.md +136 -0
- package/agents/implementer.md +185 -0
- package/agents/independent-auditor.md +143 -0
- package/agents/mock-scanner.md +78 -0
- package/agents/optimize-runner.md +112 -0
- package/agents/overfitting-reviewer.md +96 -0
- package/agents/performance-assessor.md +159 -0
- package/agents/plan-critic.md +85 -0
- package/agents/promotion-reviewer.md +121 -0
- package/agents/recurring-pattern-detector.md +248 -0
- package/agents/retrospective-synthesizer.md +109 -0
- package/agents/root-cause-investigator.md +285 -0
- package/agents/scope-auditor.md +121 -0
- package/agents/security-reviewer.md +151 -0
- package/agents/self-improvement-architect.md +125 -0
- package/agents/synthesis-critic.md +99 -0
- package/agents/transcript-pattern-miner.md +165 -0
- package/agents/ui-validator.md +193 -0
- package/codex-skills/build-loop/SKILL.md +20 -0
- package/codex-skills/debug-loop/SKILL.md +18 -0
- package/codex-skills/handoff/SKILL.md +22 -0
- package/codex-skills/knowledge/SKILL.md +21 -0
- package/codex-skills/optimize/SKILL.md +18 -0
- package/codex-skills/research/SKILL.md +18 -0
- package/commands/agent-rally-point.md +301 -0
- package/commands/assess.md +41 -0
- package/commands/debug.md +20 -0
- package/commands/debugger-detail.md +34 -0
- package/commands/debugger-scan.md +15 -0
- package/commands/debugger-status.md +20 -0
- package/commands/debugger.md +32 -0
- package/commands/handoff.md +113 -0
- package/commands/knowledge-review.md +16 -0
- package/commands/optimize.md +31 -0
- package/commands/plan-verify.md +39 -0
- package/commands/promote-experiment.md +129 -0
- package/commands/research.md +22 -0
- package/commands/run.md +12 -0
- package/commands/self-review.md +51 -0
- package/commands/start-prd.md +57 -0
- package/commands/test.md +34 -0
- package/dist/src/assessment-orchestrator.d.ts +78 -0
- package/dist/src/assessment-orchestrator.d.ts.map +1 -0
- package/dist/src/assessment-orchestrator.js +304 -0
- package/dist/src/assessment-orchestrator.js.map +1 -0
- package/dist/src/audit-miner.d.ts +21 -0
- package/dist/src/audit-miner.d.ts.map +1 -0
- package/dist/src/audit-miner.js +421 -0
- package/dist/src/audit-miner.js.map +1 -0
- package/dist/src/batch-operations.d.ts +30 -0
- package/dist/src/batch-operations.d.ts.map +1 -0
- package/dist/src/batch-operations.js +339 -0
- package/dist/src/batch-operations.js.map +1 -0
- package/dist/src/config.d.ts +83 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +176 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context-engine.d.ts +45 -0
- package/dist/src/context-engine.d.ts.map +1 -0
- package/dist/src/context-engine.js +135 -0
- package/dist/src/context-engine.js.map +1 -0
- package/dist/src/debug-wrapper.d.ts +58 -0
- package/dist/src/debug-wrapper.d.ts.map +1 -0
- package/dist/src/debug-wrapper.js +285 -0
- package/dist/src/debug-wrapper.js.map +1 -0
- package/dist/src/index.d.ts +31 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +149 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/interactive-verifier.d.ts +26 -0
- package/dist/src/interactive-verifier.d.ts.map +1 -0
- package/dist/src/interactive-verifier.js +435 -0
- package/dist/src/interactive-verifier.js.map +1 -0
- package/dist/src/lessons.d.ts +59 -0
- package/dist/src/lessons.d.ts.map +1 -0
- package/dist/src/lessons.js +172 -0
- package/dist/src/lessons.js.map +1 -0
- package/dist/src/log-reader.d.ts +39 -0
- package/dist/src/log-reader.d.ts.map +1 -0
- package/dist/src/log-reader.js +291 -0
- package/dist/src/log-reader.js.map +1 -0
- package/dist/src/logger.d.ts +59 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +328 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/parallel-retrieval.d.ts +52 -0
- package/dist/src/parallel-retrieval.d.ts.map +1 -0
- package/dist/src/parallel-retrieval.js +309 -0
- package/dist/src/parallel-retrieval.js.map +1 -0
- package/dist/src/pattern-extractor.d.ts +30 -0
- package/dist/src/pattern-extractor.d.ts.map +1 -0
- package/dist/src/pattern-extractor.js +386 -0
- package/dist/src/pattern-extractor.js.map +1 -0
- package/dist/src/result-aggregator.d.ts +44 -0
- package/dist/src/result-aggregator.d.ts.map +1 -0
- package/dist/src/result-aggregator.js +315 -0
- package/dist/src/result-aggregator.js.map +1 -0
- package/dist/src/retrieval.d.ts +139 -0
- package/dist/src/retrieval.d.ts.map +1 -0
- package/dist/src/retrieval.js +724 -0
- package/dist/src/retrieval.js.map +1 -0
- package/dist/src/setup/auto-setup.d.ts +15 -0
- package/dist/src/setup/auto-setup.d.ts.map +1 -0
- package/dist/src/setup/auto-setup.js +157 -0
- package/dist/src/setup/auto-setup.js.map +1 -0
- package/dist/src/setup/configure-hooks.d.ts +8 -0
- package/dist/src/setup/configure-hooks.d.ts.map +1 -0
- package/dist/src/setup/configure-hooks.js +16 -0
- package/dist/src/setup/configure-hooks.js.map +1 -0
- package/dist/src/setup/create-slash-commands.d.ts +8 -0
- package/dist/src/setup/create-slash-commands.d.ts.map +1 -0
- package/dist/src/setup/create-slash-commands.js +101 -0
- package/dist/src/setup/create-slash-commands.js.map +1 -0
- package/dist/src/setup/index.d.ts +8 -0
- package/dist/src/setup/index.d.ts.map +1 -0
- package/dist/src/setup/index.js +17 -0
- package/dist/src/setup/index.js.map +1 -0
- package/dist/src/setup/inject-claude-md.d.ts +10 -0
- package/dist/src/setup/inject-claude-md.d.ts.map +1 -0
- package/dist/src/setup/inject-claude-md.js +150 -0
- package/dist/src/setup/inject-claude-md.js.map +1 -0
- package/dist/src/setup/uninstall.d.ts +22 -0
- package/dist/src/setup/uninstall.d.ts.map +1 -0
- package/dist/src/setup/uninstall.js +235 -0
- package/dist/src/setup/uninstall.js.map +1 -0
- package/dist/src/storage.d.ts +192 -0
- package/dist/src/storage.d.ts.map +1 -0
- package/dist/src/storage.js +1037 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/string-similarity.d.ts +8 -0
- package/dist/src/string-similarity.d.ts.map +1 -0
- package/dist/src/string-similarity.js +57 -0
- package/dist/src/string-similarity.js.map +1 -0
- package/dist/src/traces/adapters/browser.d.ts +69 -0
- package/dist/src/traces/adapters/browser.d.ts.map +1 -0
- package/dist/src/traces/adapters/browser.js +280 -0
- package/dist/src/traces/adapters/browser.js.map +1 -0
- package/dist/src/traces/adapters/index.d.ts +23 -0
- package/dist/src/traces/adapters/index.d.ts.map +1 -0
- package/dist/src/traces/adapters/index.js +67 -0
- package/dist/src/traces/adapters/index.js.map +1 -0
- package/dist/src/traces/adapters/langchain.d.ts +42 -0
- package/dist/src/traces/adapters/langchain.d.ts.map +1 -0
- package/dist/src/traces/adapters/langchain.js +151 -0
- package/dist/src/traces/adapters/langchain.js.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts +46 -0
- package/dist/src/traces/adapters/opentelemetry.d.ts.map +1 -0
- package/dist/src/traces/adapters/opentelemetry.js +164 -0
- package/dist/src/traces/adapters/opentelemetry.js.map +1 -0
- package/dist/src/traces/adapters/sentry.d.ts +58 -0
- package/dist/src/traces/adapters/sentry.d.ts.map +1 -0
- package/dist/src/traces/adapters/sentry.js +212 -0
- package/dist/src/traces/adapters/sentry.js.map +1 -0
- package/dist/src/traces/index.d.ts +10 -0
- package/dist/src/traces/index.d.ts.map +1 -0
- package/dist/src/traces/index.js +45 -0
- package/dist/src/traces/index.js.map +1 -0
- package/dist/src/traces/storage.d.ts +78 -0
- package/dist/src/traces/storage.d.ts.map +1 -0
- package/dist/src/traces/storage.js +399 -0
- package/dist/src/traces/storage.js.map +1 -0
- package/dist/src/traces/summarizer.d.ts +83 -0
- package/dist/src/traces/summarizer.d.ts.map +1 -0
- package/dist/src/traces/summarizer.js +247 -0
- package/dist/src/traces/summarizer.js.map +1 -0
- package/dist/src/traces/types.d.ts +365 -0
- package/dist/src/traces/types.d.ts.map +1 -0
- package/dist/src/traces/types.js +14 -0
- package/dist/src/traces/types.js.map +1 -0
- package/dist/src/types.d.ts +495 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +27 -0
- package/dist/src/types.js.map +1 -0
- package/docs/agent-surface-policy.md +45 -0
- package/docs/memory-setup.md +226 -0
- package/hooks/_arch_scan_bg.py +88 -0
- package/hooks/_plugin_drift_check_bg.py +85 -0
- package/hooks/_session_start_lib.sh +42 -0
- package/hooks/git/pre-push +141 -0
- package/hooks/hooks.json +126 -0
- package/hooks/post-commit +30 -0
- package/hooks/pre-commit +29 -0
- package/hooks/pre-edit-apppulse.sh +6 -0
- package/hooks/pre-edit-architecture.sh +106 -0
- package/hooks/pre-edit-rally-point.sh +92 -0
- package/hooks/session-start-apppulse.sh +6 -0
- package/hooks/session-start-architecture.sh +10 -0
- package/hooks/session-start-git-hooks.sh +12 -0
- package/hooks/session-start-plugin-drift.sh +27 -0
- package/hooks/session-start-rally-point.sh +44 -0
- package/hooks/session-start-retrieval.sh +24 -0
- package/hooks/session-start-worktree-gc.sh +105 -0
- package/hooks/test_apppulse_hooks.sh +6 -0
- package/hooks/test_rally_point_hooks.sh +388 -0
- package/hooks/test_sec008_hooks.sh +66 -0
- package/package.json +111 -0
- package/scripts/README.md +301 -0
- package/scripts/_db_url.py +61 -0
- package/scripts/_paths.py +445 -0
- package/scripts/blm.py +162 -0
- package/scripts/blm_api.py +263 -0
- package/scripts/check_cache_sync.py +490 -0
- package/scripts/embed_backend.py +475 -0
- package/scripts/install_memory.py +556 -0
- package/scripts/lessons_index/__init__.py +77 -0
- package/scripts/lessons_index/__main__.py +135 -0
- package/scripts/lessons_index/ingest.py +288 -0
- package/scripts/lessons_index/query.py +285 -0
- package/scripts/lessons_index/schema.py +114 -0
- package/scripts/memory_context/__init__.py +610 -0
- package/scripts/memory_graph/__init__.py +393 -0
- package/scripts/project_resolver.py +172 -0
- package/scripts/prune_codex_plugin_cache.py +23 -0
- package/scripts/prune_plugin_cache.py +359 -0
- package/scripts/semantic_index/__init__.py +482 -0
- package/scripts/semantic_index/_bench_hybrid.py +175 -0
- package/scripts/semantic_index/backfill.py +263 -0
- package/scripts/semantic_index/hybrid.py +174 -0
- package/scripts/semantic_index/test_backfill.py +292 -0
- package/scripts/semantic_index/test_hybrid.py +541 -0
- package/scripts/sync_navgator_lessons.py +666 -0
- package/scripts/sync_plugin_cache.py +388 -0
- package/skills/agent-rally-point/SKILL.md +165 -0
- package/skills/agent-rally-watcher/SKILL.md +83 -0
- package/skills/api-registry-bridge/SKILL.md +59 -0
- package/skills/architecture/dead/SKILL.md +56 -0
- package/skills/architecture/impact/SKILL.md +88 -0
- package/skills/architecture/review/SKILL.md +227 -0
- package/skills/architecture/rules/SKILL.md +72 -0
- package/skills/architecture/scan/SKILL.md +79 -0
- package/skills/architecture/trace/SKILL.md +65 -0
- package/skills/attribution-standard/SKILL.md +122 -0
- package/skills/authentication/SKILL.md +81 -0
- package/skills/authentication/references/better-auth-magic-link.md +135 -0
- package/skills/authentication/references/better-auth-setup.md +224 -0
- package/skills/authentication/references/google-calendar-sync.md +278 -0
- package/skills/authentication/references/google-cloud-console.md +159 -0
- package/skills/authentication/references/google-geocoding-directions.md +126 -0
- package/skills/authentication/references/google-lessons-example-web-app.md +177 -0
- package/skills/authentication/references/google-maps.md +162 -0
- package/skills/authentication/references/google-oauth-setup.md +343 -0
- package/skills/authentication/references/google-places.md +173 -0
- package/skills/authentication/references/lessons-example-web-app-better-auth.md +126 -0
- package/skills/authentication/references/resend-email.md +227 -0
- package/skills/authentication/references/resend-otp-magic-link.md +153 -0
- package/skills/authentication/references/supabase-auth.md +172 -0
- package/skills/auto-decision-capture/SKILL.md +419 -0
- package/skills/build-loop/SKILL.md +516 -0
- package/skills/build-loop/detect-plugins.mjs +82 -0
- package/skills/build-loop/eval-guide.md +65 -0
- package/skills/build-loop/fallbacks.md +549 -0
- package/skills/build-loop/phases/fact-check.md +42 -0
- package/skills/build-loop/phases/ui-validation.md +267 -0
- package/skills/build-loop/references/apple-native-planning.md +439 -0
- package/skills/build-loop/references/capability-routing.md +181 -0
- package/skills/build-loop/references/codex-subagents.md +98 -0
- package/skills/build-loop/references/coordination.md +161 -0
- package/skills/build-loop/references/correction-aware-capture.md +177 -0
- package/skills/build-loop/references/independent-auditor.md +72 -0
- package/skills/build-loop/references/intent-capability-pack.md +170 -0
- package/skills/build-loop/references/intent-exploration-prompts.md +96 -0
- package/skills/build-loop/references/leadership.md +72 -0
- package/skills/build-loop/references/memory.md +284 -0
- package/skills/build-loop/references/modular-systems-pack.md +96 -0
- package/skills/build-loop/references/output-style.md +129 -0
- package/skills/build-loop/references/pay-it-forward-arch.md +98 -0
- package/skills/build-loop/references/phase-1-assess.md +218 -0
- package/skills/build-loop/references/phase-2-plan.md +77 -0
- package/skills/build-loop/references/phase-3-execute.md +47 -0
- package/skills/build-loop/references/phase-4-review.md +310 -0
- package/skills/build-loop/references/phase-5-iterate.md +71 -0
- package/skills/build-loop/references/phase-6-learn.md +56 -0
- package/skills/build-loop/references/recent-design-structures.md +274 -0
- package/skills/build-loop/references/refactor-history/ASSESSMENT.md +85 -0
- package/skills/build-loop/references/refactor-history/STANDALONE_TEST_RUN.md +149 -0
- package/skills/build-loop/references/refactor-history/scenarios/01-simple-bugfix.md +32 -0
- package/skills/build-loop/references/refactor-history/scenarios/02-ui-build-with-iteration.md +48 -0
- package/skills/build-loop/references/refactor-history/scenarios/03-multi-failure-escalation.md +60 -0
- package/skills/build-loop/references/refactor-history/scenarios/04-ui-build-ibr-absent.md +51 -0
- package/skills/build-loop/references/refactor-history/scenarios/05-refactor-navgator-absent.md +71 -0
- package/skills/build-loop/references/refactor-history/scenarios/06-recurring-bug-debugger-absent.md +52 -0
- package/skills/build-loop/references/refactor-history/trace-comparison.md +202 -0
- package/skills/build-loop/references/self-recursive-dev.md +77 -0
- package/skills/build-loop/references/self-review.md +232 -0
- package/skills/build-loop/references/ui-io-contract.md +116 -0
- package/skills/build-loop/scanners/audit-design-rules.mjs +476 -0
- package/skills/build-loop/scanners/require-visual-evidence.mjs +239 -0
- package/skills/build-loop/templates/backlog-item.md +35 -0
- package/skills/build-loop/templates/codex-worker-prompt.md +89 -0
- package/skills/build-loop/templates/ui-subagent-prompt.md +179 -0
- package/skills/build-loop/templates/ux-fix-plan.md +40 -0
- package/skills/building-with-deepagents/SKILL.md +195 -0
- package/skills/building-with-deepagents/references/anti-patterns.md +141 -0
- package/skills/building-with-deepagents/references/api-quick-reference.md +184 -0
- package/skills/capabilities/SKILL.md +91 -0
- package/skills/debug-loop/SKILL.md +278 -0
- package/skills/debug-loop/references/convergence-rules.md +145 -0
- package/skills/debug-loop/references/swiftui-macos-clunky-ui-debugger.html +823 -0
- package/skills/debugging/assess/SKILL.md +111 -0
- package/skills/debugging/debug-loop/SKILL.md +231 -0
- package/skills/debugging/memory/SKILL.md +147 -0
- package/skills/debugging/store/SKILL.md +153 -0
- package/skills/debugging-memory/SKILL.md +440 -0
- package/skills/debugging-memory/examples/incident-example.json +67 -0
- package/skills/debugging-memory/references/incident-documentation.md +280 -0
- package/skills/debugging-memory/references/ios-notification-alarm-playbook.md +300 -0
- package/skills/debugging-memory/references/pattern-extraction.md +163 -0
- package/skills/debugging-memory/references/subagent-integration.md +312 -0
- package/skills/defenseclaw-bridge/SKILL.md +156 -0
- package/skills/defenseclaw-bridge/references/dc-config-mapping.md +162 -0
- package/skills/defenseclaw-bridge/references/output-format.md +108 -0
- package/skills/handoff/SKILL.md +94 -0
- package/skills/ibr-bridge/SKILL.md +68 -0
- package/skills/knowledge/SKILL.md +127 -0
- package/skills/knowledge/references/recall-integration.md +83 -0
- package/skills/knowledge/templates/madr-minimal.md +56 -0
- package/skills/knowledge-review/SKILL.md +112 -0
- package/skills/logging-tracer/SKILL.md +202 -0
- package/skills/logging-tracer/references/log-analysis.md +128 -0
- package/skills/logging-tracer/references/stack-templates.md +274 -0
- package/skills/logging-tracer-bridge/SKILL.md +70 -0
- package/skills/mcp-builder/SKILL.md +278 -0
- package/skills/mcp-builder/references/mcp-security.md +106 -0
- package/skills/model-tiering/SKILL.md +216 -0
- package/skills/native-ax-driver/SKILL.md +220 -0
- package/skills/native-ax-driver/scripts/layout_fill.py +222 -0
- package/skills/native-ax-driver/scripts/native_driver.py +495 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Package.swift +20 -0
- package/skills/native-ax-driver/swift/bl-ax-driver/Sources/main.swift +542 -0
- package/skills/optimize/SKILL.md +202 -0
- package/skills/optimize/profiles.md +61 -0
- package/skills/plan-verify/SKILL.md +115 -0
- package/skills/plan-verify/test-fixtures/example-app-v20-findings.json +27 -0
- package/skills/plan-verify/test-fixtures/example-app-v20.md +42 -0
- package/skills/plan-verify/test-fixtures/example-app-v22-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/example-app-v22.md +1608 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan-findings.json +15 -0
- package/skills/plan-verify/test-fixtures/unrelated-good-plan.md +55 -0
- package/skills/plugin-builder/SKILL.md +329 -0
- package/skills/plugin-builder/references/authoritative-sources.md +104 -0
- package/skills/plugin-builder/references/build-loop-phase-guidance.md +94 -0
- package/skills/plugin-builder/references/components-guide.md +221 -0
- package/skills/plugin-builder/references/distribution.md +317 -0
- package/skills/plugin-builder/references/dual-host-claude-codex.md +220 -0
- package/skills/plugin-builder/references/hooks-reference.md +210 -0
- package/skills/plugin-builder/references/manifest-schema.md +111 -0
- package/skills/plugin-builder/references/plugin-hygiene-lessons.md +307 -0
- package/skills/plugin-tests/SKILL.md +94 -0
- package/skills/prd-bridge/SKILL.md +135 -0
- package/skills/research/SKILL.md +80 -0
- package/skills/security-methodology/SKILL.md +105 -0
- package/skills/security-methodology/references/cross-source-matrix.md +77 -0
- package/skills/security-methodology/references/mitre-atlas-starter.md +83 -0
- package/skills/security-methodology/references/nist-600-1-mapping.md +119 -0
- package/skills/security-methodology/references/owasp-agentic-top-10.md +157 -0
- package/skills/security-methodology/references/owasp-llm-top-10.md +144 -0
- package/skills/security-methodology/references/owasp-web-top-10.md +65 -0
- package/skills/self-improve/SKILL.md +298 -0
- package/skills/spec-writing/SKILL.md +548 -0
- package/skills/spec-writing/scripts/check_checklist.py +478 -0
- package/skills/sync-skills/SKILL.md +132 -0
- package/skills/telemetry/SKILL.md +66 -0
- package/skills/ui-design/SKILL.md +75 -0
- package/skills/ui-design/references/design-patterns-multi.md +165 -0
- package/skills/ui-design/references/design-preferences-from-owned-apps.md +150 -0
- package/skills/ui-design/references/evidence-capture-policy.md +55 -0
- package/skills/ui-design/references/ui-guidance-sources.md +330 -0
- package/skills/ui-design/references/universal-design-principles.alt.md +182 -0
- package/skills/ui-design/references/universal-design-principles.md +419 -0
- package/templates/memory/MEMORY.md.template +55 -0
- package/templates/memory/constitution.md.template +64 -0
- package/templates/memory/manifest.json +47 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
|
|
3
|
+
# Google Calendar API v3 — Sync Patterns
|
|
4
|
+
|
|
5
|
+
Syncing events between your app and a user's Google Calendar. Covers scopes, incremental sync via `syncToken`, refresh-token storage, and the 410-recovery dance.
|
|
6
|
+
|
|
7
|
+
## Scopes — Pick the Narrowest
|
|
8
|
+
|
|
9
|
+
| Scope | What you can do | Sensitive? |
|
|
10
|
+
|-------|-----------------|------------|
|
|
11
|
+
| `calendar.readonly` | List events, list calendars | Yes (verification required) |
|
|
12
|
+
| `calendar.events.readonly` | List events (any calendar you specify) | Yes |
|
|
13
|
+
| `calendar.events` | Create/update/delete events | Yes |
|
|
14
|
+
| `calendar.app.created` | Only events your app created | **No** (unverified OK) |
|
|
15
|
+
| `calendar` | Full access (including settings) | Yes |
|
|
16
|
+
|
|
17
|
+
**Use `calendar.app.created` if possible** — it only lets your app touch events it created, requires no Google verification, and is the least privacy-invasive. The catch: you can't read events the user created manually. For two-way sync of user events you need `calendar.events`.
|
|
18
|
+
|
|
19
|
+
## Adding Calendar Scope to Existing OAuth
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Google Cloud Console → APIs & Services → OAuth consent screen → EDIT APP
|
|
23
|
+
→ Scopes → ADD OR REMOVE SCOPES
|
|
24
|
+
→ Search for "calendar" → select scope → UPDATE → SAVE
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If your app was already published, adding a sensitive scope triggers re-verification. Plan for ~1 week.
|
|
28
|
+
|
|
29
|
+
## Store the Refresh Token (Critical)
|
|
30
|
+
|
|
31
|
+
Google only returns a `refresh_token` on the **first** consent, unless you pass `prompt=consent&access_type=offline`. Always request both:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
https://accounts.google.com/o/oauth2/v2/auth?
|
|
35
|
+
client_id=...&
|
|
36
|
+
redirect_uri=...&
|
|
37
|
+
response_type=code&
|
|
38
|
+
scope=openid%20email%20profile%20https%3A//www.googleapis.com/auth/calendar.events&
|
|
39
|
+
access_type=offline& ← required for refresh_token
|
|
40
|
+
prompt=consent& ← force refresh_token even on re-consent
|
|
41
|
+
state=...
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The refresh_token is **long-lived** (months). The access_token expires in 1 hour. Store both:
|
|
45
|
+
|
|
46
|
+
```sql
|
|
47
|
+
CREATE TABLE user_calendars (
|
|
48
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
49
|
+
user_id UUID REFERENCES auth.users(id) NOT NULL,
|
|
50
|
+
google_email VARCHAR(255) NOT NULL,
|
|
51
|
+
google_calendar_id VARCHAR(255) NOT NULL, -- 'primary' or specific ID
|
|
52
|
+
access_token TEXT NOT NULL, -- expires in ~1h
|
|
53
|
+
refresh_token TEXT NOT NULL, -- long-lived
|
|
54
|
+
token_expires_at TIMESTAMPTZ NOT NULL,
|
|
55
|
+
sync_token TEXT, -- Google's nextSyncToken for incremental sync
|
|
56
|
+
last_synced_at TIMESTAMPTZ,
|
|
57
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
58
|
+
UNIQUE (user_id, google_calendar_id)
|
|
59
|
+
);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Encrypt refresh_token at rest.** Use column-level encryption (pgcrypto) or your hosting provider's secret manager.
|
|
63
|
+
|
|
64
|
+
## Refresh an Access Token
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
async function refreshAccessToken(refreshToken: string) {
|
|
68
|
+
const res = await fetch('https://oauth2.googleapis.com/token', {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
71
|
+
body: new URLSearchParams({
|
|
72
|
+
client_id: process.env.GOOGLE_CLIENT_ID!,
|
|
73
|
+
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
|
|
74
|
+
refresh_token: refreshToken,
|
|
75
|
+
grant_type: 'refresh_token',
|
|
76
|
+
}),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
const err = await res.json()
|
|
81
|
+
if (err.error === 'invalid_grant') {
|
|
82
|
+
// User revoked access or refresh_token is dead
|
|
83
|
+
throw new CalendarReauthRequiredError()
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Token refresh failed: ${JSON.stringify(err)}`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return res.json() as Promise<{ access_token: string; expires_in: number }>
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**`invalid_grant` handling**: Mark the user_calendar row as needing re-auth and show a UI prompt. Don't retry silently — they've revoked access.
|
|
93
|
+
|
|
94
|
+
## Incremental Sync with syncToken
|
|
95
|
+
|
|
96
|
+
This is the whole point — don't fetch all events every time.
|
|
97
|
+
|
|
98
|
+
### First sync (full)
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
async function fullSync(calendarId: string, accessToken: string) {
|
|
102
|
+
let pageToken: string | undefined
|
|
103
|
+
let nextSyncToken: string | undefined
|
|
104
|
+
const events: any[] = []
|
|
105
|
+
|
|
106
|
+
do {
|
|
107
|
+
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events`)
|
|
108
|
+
url.searchParams.set('singleEvents', 'true') // expand recurring
|
|
109
|
+
url.searchParams.set('showDeleted', 'false')
|
|
110
|
+
url.searchParams.set('maxResults', '250')
|
|
111
|
+
if (pageToken) url.searchParams.set('pageToken', pageToken)
|
|
112
|
+
|
|
113
|
+
const res = await fetch(url, {
|
|
114
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
if (!res.ok) throw new Error(`Calendar list failed: ${res.status}`)
|
|
118
|
+
const data = await res.json()
|
|
119
|
+
|
|
120
|
+
events.push(...(data.items ?? []))
|
|
121
|
+
pageToken = data.nextPageToken
|
|
122
|
+
nextSyncToken = data.nextSyncToken // only on the LAST page
|
|
123
|
+
} while (pageToken)
|
|
124
|
+
|
|
125
|
+
// Store nextSyncToken for the next incremental sync
|
|
126
|
+
return { events, nextSyncToken }
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Incremental sync (subsequent runs)
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
async function incrementalSync(calendarId: string, accessToken: string, syncToken: string) {
|
|
134
|
+
let pageToken: string | undefined
|
|
135
|
+
let newSyncToken: string | undefined
|
|
136
|
+
const changes: any[] = []
|
|
137
|
+
|
|
138
|
+
do {
|
|
139
|
+
const url = new URL(`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events`)
|
|
140
|
+
url.searchParams.set('syncToken', syncToken)
|
|
141
|
+
if (pageToken) url.searchParams.set('pageToken', pageToken)
|
|
142
|
+
|
|
143
|
+
const res = await fetch(url, {
|
|
144
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// 410 GONE — syncToken expired (happens after ~7 days of inactivity, or if Google invalidates it)
|
|
148
|
+
if (res.status === 410) {
|
|
149
|
+
// Drop stored syncToken and do a full sync
|
|
150
|
+
await db.query('UPDATE user_calendars SET sync_token = NULL WHERE ...')
|
|
151
|
+
return fullSync(calendarId, accessToken)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!res.ok) throw new Error(`Calendar sync failed: ${res.status}`)
|
|
155
|
+
const data = await res.json()
|
|
156
|
+
|
|
157
|
+
// Events returned here include deletions (status: 'cancelled')
|
|
158
|
+
changes.push(...(data.items ?? []))
|
|
159
|
+
pageToken = data.nextPageToken
|
|
160
|
+
newSyncToken = data.nextSyncToken
|
|
161
|
+
} while (pageToken)
|
|
162
|
+
|
|
163
|
+
return { changes, newSyncToken }
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Critical**: `nextSyncToken` is only on the LAST page of results. If you stop paging early, you lose the token and have to full-sync next time.
|
|
168
|
+
|
|
169
|
+
**Do NOT combine `syncToken` with other filters** like `timeMin`, `timeMax`, `q`. Google errors out. Sync tokens return ALL events that changed — filter client-side after.
|
|
170
|
+
|
|
171
|
+
## Create an Event
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
async function createEvent(calendarId: string, accessToken: string, event: {
|
|
175
|
+
summary: string
|
|
176
|
+
description?: string
|
|
177
|
+
location?: string
|
|
178
|
+
start: { dateTime: string; timeZone: string }
|
|
179
|
+
end: { dateTime: string; timeZone: string }
|
|
180
|
+
}) {
|
|
181
|
+
const res = await fetch(
|
|
182
|
+
`https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent(calendarId)}/events`,
|
|
183
|
+
{
|
|
184
|
+
method: 'POST',
|
|
185
|
+
headers: {
|
|
186
|
+
Authorization: `Bearer ${accessToken}`,
|
|
187
|
+
'Content-Type': 'application/json',
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify(event),
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if (!res.ok) throw new Error(`Create event failed: ${res.status} ${await res.text()}`)
|
|
194
|
+
return res.json() as Promise<{ id: string; htmlLink: string }>
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Store the returned `id` as `google_event_id` on your local row so you can update/delete it later.
|
|
199
|
+
|
|
200
|
+
## Push Notifications (Watch Channels)
|
|
201
|
+
|
|
202
|
+
For near-real-time sync, subscribe to a channel:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
await fetch(`https://www.googleapis.com/calendar/v3/calendars/primary/events/watch`, {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: {
|
|
208
|
+
Authorization: `Bearer ${accessToken}`,
|
|
209
|
+
'Content-Type': 'application/json',
|
|
210
|
+
},
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
id: randomUUID(),
|
|
213
|
+
type: 'web_hook',
|
|
214
|
+
address: 'https://your-domain.com/api/calendar/webhook',
|
|
215
|
+
token: 'your-verification-secret',
|
|
216
|
+
expiration: String(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days max
|
|
217
|
+
}),
|
|
218
|
+
})
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Google POSTs to your webhook when the calendar changes. The webhook doesn't contain the change — it's a signal to trigger an incremental sync.
|
|
222
|
+
|
|
223
|
+
**Your webhook must be HTTPS with a valid public cert.** ngrok works for local testing.
|
|
224
|
+
|
|
225
|
+
Channels expire after 7 days. Refresh them on a cron.
|
|
226
|
+
|
|
227
|
+
## Sync Job Pattern
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// Cron or queued job — runs per user_calendar row
|
|
231
|
+
async function syncUserCalendar(userCalendarId: string) {
|
|
232
|
+
const row = await db.queryOne(/* user_calendars by id */)
|
|
233
|
+
if (!row) return
|
|
234
|
+
|
|
235
|
+
// Refresh token if needed
|
|
236
|
+
if (row.token_expires_at < new Date(Date.now() + 5 * 60_000)) {
|
|
237
|
+
try {
|
|
238
|
+
const { access_token, expires_in } = await refreshAccessToken(row.refresh_token)
|
|
239
|
+
await db.query(/* update access_token, token_expires_at */)
|
|
240
|
+
row.access_token = access_token
|
|
241
|
+
} catch (e) {
|
|
242
|
+
if (e instanceof CalendarReauthRequiredError) {
|
|
243
|
+
await db.query(/* mark needs_reauth = true */)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
throw e
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Sync
|
|
251
|
+
let result
|
|
252
|
+
if (row.sync_token) {
|
|
253
|
+
result = await incrementalSync(row.google_calendar_id, row.access_token, row.sync_token)
|
|
254
|
+
} else {
|
|
255
|
+
result = await fullSync(row.google_calendar_id, row.access_token)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Apply changes to local DB
|
|
259
|
+
for (const ev of result.changes ?? result.events) {
|
|
260
|
+
if (ev.status === 'cancelled') {
|
|
261
|
+
await db.query('DELETE FROM events WHERE google_event_id = $1', [ev.id])
|
|
262
|
+
} else {
|
|
263
|
+
await upsertEvent(ev)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Persist new sync token
|
|
268
|
+
await db.query('UPDATE user_calendars SET sync_token = $1, last_synced_at = NOW() WHERE id = $2',
|
|
269
|
+
[result.nextSyncToken ?? result.newSyncToken, userCalendarId])
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Sources
|
|
274
|
+
|
|
275
|
+
- [Sync resources efficiently](https://developers.google.com/workspace/calendar/api/guides/sync)
|
|
276
|
+
- [Choose Calendar API scopes](https://developers.google.com/workspace/calendar/api/auth)
|
|
277
|
+
- [Events: list](https://developers.google.com/workspace/calendar/api/v3/reference/events/list)
|
|
278
|
+
- [Push notifications](https://developers.google.com/workspace/calendar/api/guides/push)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: google-cloud-console
|
|
3
|
+
description: Use when setting up Google Cloud Console for a web app — Google OAuth sign-in, Maps JavaScript API, Places API (New), Geocoding, Directions, or Google Calendar sync. Covers console setup, credentials, scopes, common footguns, and Next.js 16 / Supabase patterns. Trigger on "google oauth", "google sign in", "google maps", "places api", "google calendar sync", "google cloud credentials".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
7
|
+
|
|
8
|
+
# Google Cloud Console — Auth + Maps + Calendar
|
|
9
|
+
|
|
10
|
+
Hard-won reference for wiring Google Cloud services into a web app. Lessons baked in from a real Next.js + Supabase build (Example Web App, 2025). Every footgun here was paid for once already — don't pay for it again.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- Setting up Google sign-in (OAuth 2.0) for a web or mobile app
|
|
15
|
+
- Adding Google Maps, Places, Geocoding, or Directions to a UI
|
|
16
|
+
- Syncing Google Calendar events to/from your app
|
|
17
|
+
- Debugging auth redirect loops, `redirect_uri_mismatch`, or `invalid_grant`
|
|
18
|
+
- Migrating from legacy Places API / `gapi.auth2` / `google.maps.Marker`
|
|
19
|
+
|
|
20
|
+
## Core Mental Model
|
|
21
|
+
|
|
22
|
+
Google Cloud Console is four layered concerns — get them in this order:
|
|
23
|
+
|
|
24
|
+
1. **Project** — a container. APIs are enabled _per project_.
|
|
25
|
+
2. **APIs** — enable only the ones you need (billing consequences).
|
|
26
|
+
3. **Credentials** — OAuth client IDs (for user auth) OR API keys (for Maps) OR service accounts (server-to-server).
|
|
27
|
+
4. **OAuth Consent Screen** — what the user sees on the consent dialog. Required before OAuth works.
|
|
28
|
+
|
|
29
|
+
A single project can hold multiple credentials. Use **separate projects for dev and prod** — not just separate credentials. This keeps quotas, billing, and consent-screen state isolated.
|
|
30
|
+
|
|
31
|
+
## Decision Matrix
|
|
32
|
+
|
|
33
|
+
| Need | Credential type | Key doc |
|
|
34
|
+
|------|-----------------|---------|
|
|
35
|
+
| "Sign in with Google" button | OAuth 2.0 client (Web application) | [OAuth web server](https://developers.google.com/identity/protocols/oauth2/web-server) |
|
|
36
|
+
| Embed a map in a web page | API key (HTTP referrer restricted) | [Load Maps JS API](https://developers.google.com/maps/documentation/javascript/load-maps-js-api) |
|
|
37
|
+
| Search places / autocomplete | API key (same one if same project) | [Places API New](https://developers.google.com/maps/documentation/places/web-service/overview) |
|
|
38
|
+
| Read user's Google Calendar | OAuth client + Calendar scope | [Calendar auth](https://developers.google.com/workspace/calendar/api/auth) |
|
|
39
|
+
| Server-to-server Calendar (Workspace) | Service account + domain-wide delegation | [Service accounts](https://developers.google.com/identity/protocols/oauth2/service-account) |
|
|
40
|
+
| Native mobile sign-in | OAuth client (iOS/Android) + PKCE | [Mobile OAuth](https://developers.google.com/identity/protocols/oauth2/native-app) |
|
|
41
|
+
|
|
42
|
+
## Setup Order (Do NOT skip steps)
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
1. Create project (dev + prod, separate)
|
|
46
|
+
2. Enable required APIs (per project)
|
|
47
|
+
3. Configure OAuth consent screen (BEFORE creating OAuth client)
|
|
48
|
+
4. Create credentials (OAuth client / API key / service account)
|
|
49
|
+
5. Restrict credentials (referrer, IP, or scope)
|
|
50
|
+
6. Add credentials to app env vars (NEVER commit)
|
|
51
|
+
7. Configure app-side redirect URIs to match EXACTLY
|
|
52
|
+
8. Test in dev → test in prod-like env → ship
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Details per surface are in `references/` — load progressively.
|
|
56
|
+
|
|
57
|
+
## References (Progressive Disclosure)
|
|
58
|
+
|
|
59
|
+
Load only what you need:
|
|
60
|
+
|
|
61
|
+
- **`references/oauth-setup.md`** — OAuth consent screen fields, web-app client creation, Supabase Google provider wiring, Next.js 16 cookie API, CSRF via state param, redirect URI rules. Start here for "add sign in with Google".
|
|
62
|
+
- **`references/maps.md`** — Maps JS API loader, `importLibrary()` async pattern, Advanced Markers (Marker is deprecated), Map ID requirement, key restriction recipe.
|
|
63
|
+
- **`references/places.md`** — Places API (New) v1 — Text Search, Nearby, Place Details. Field masks (cost control). Migration from legacy. Autocomplete.
|
|
64
|
+
- **`references/geocoding-directions.md`** — Geocoding + Directions + Distance Matrix, rate limits, caching strategy.
|
|
65
|
+
- **`references/calendar-sync.md`** — Google Calendar API v3 scopes, incremental sync with `syncToken`, 410 recovery, refresh-token storage, watch channels for push.
|
|
66
|
+
- **`references/lessons-example-web-app.md`** — Real failure modes from production: Supabase SSR cookie bug, production-only redirect loops, route-group manifest conflicts, credential rotation pain.
|
|
67
|
+
|
|
68
|
+
## The 10 Footguns (Read This Section Every Time)
|
|
69
|
+
|
|
70
|
+
These are the failures that cost days in the Example Web App build. Check for each when something breaks.
|
|
71
|
+
|
|
72
|
+
1. **Supabase SSR cookie API mismatch (Next.js 15+)** — Must use `getAll`/`setAll`, NOT `get`/`set`/`remove`. Works in dev, fails in prod. Error surfaces as "Cannot read properties of undefined (reading 'get')" in Supabase SSR. See `references/oauth-setup.md#nextjs-cookies`.
|
|
73
|
+
|
|
74
|
+
2. **`redirect_uri_mismatch`** — The redirect URI in Google Cloud Console must match the one your app sends **exactly** (no trailing slash, correct protocol, correct port). For Supabase it must be `https://<project>.supabase.co/auth/v1/callback` — **not** your app's URL.
|
|
75
|
+
|
|
76
|
+
3. **OAuth consent screen in "Testing" mode** — Until you publish it, only listed test users can sign in. Publishing triggers verification if you use sensitive scopes (like Calendar).
|
|
77
|
+
|
|
78
|
+
4. **No OAuth consent screen at all** — You cannot create a Web-app OAuth client until the consent screen is configured. The console lets you start and then fails silently on some fields.
|
|
79
|
+
|
|
80
|
+
5. **Map ID missing for Advanced Markers** — `google.maps.Marker` is deprecated (Feb 2024). `AdvancedMarkerElement` requires a **Map ID** on the `Map` constructor. Without it, markers silently don't render.
|
|
81
|
+
|
|
82
|
+
6. **Legacy Places API enabled instead of Places API (New)** — Legacy can no longer be enabled for new projects. If you followed a 2023 tutorial it won't work. Use the New API with `FieldMask` headers.
|
|
83
|
+
|
|
84
|
+
7. **API key with no referrer restriction** — Anyone who views source grabs your key and racks up billing. Always restrict Maps/Places keys by HTTP referrer (`https://your-domain.com/*`) and API (only the APIs you actually use).
|
|
85
|
+
|
|
86
|
+
8. **Client secret in `NEXT_PUBLIC_*`** — Easy to do by mistake; exposes the secret to the browser. Client secrets go in server-only env vars. API keys for Maps JS are public by design (protected by referrer restriction, not secrecy).
|
|
87
|
+
|
|
88
|
+
9. **Route-group conflicts cause Vercel build failures** — `app/page.tsx` + `app/(app)/page.tsx` both existing generates the `client-reference-manifest.js` at the wrong path. Builds locally, fails on Vercel. Pick one.
|
|
89
|
+
|
|
90
|
+
10. **`invalid_grant` on token refresh** — User revoked access, or the refresh token wasn't stored (Google only returns it on the _first_ consent unless you pass `prompt=consent` or `access_type=offline`). Always request `access_type=offline&prompt=consent` for first consent if you need durable calendar access.
|
|
91
|
+
|
|
92
|
+
## Required Env Vars (Web App Template)
|
|
93
|
+
|
|
94
|
+
```env
|
|
95
|
+
# Supabase (if using)
|
|
96
|
+
NEXT_PUBLIC_SUPABASE_URL=https://xxx.supabase.co
|
|
97
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY=ey...
|
|
98
|
+
|
|
99
|
+
# Google OAuth (server-side only — Supabase handles the flow)
|
|
100
|
+
GOOGLE_CLIENT_ID=...apps.googleusercontent.com
|
|
101
|
+
GOOGLE_CLIENT_SECRET=GOCSPX-...
|
|
102
|
+
|
|
103
|
+
# Google Maps (browser-exposed, protected by HTTP referrer restriction)
|
|
104
|
+
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=AIza...
|
|
105
|
+
NEXT_PUBLIC_GOOGLE_MAPS_MAP_ID=... # required for Advanced Markers
|
|
106
|
+
|
|
107
|
+
# App URLs — MUST match what's in Google Cloud Console + Supabase
|
|
108
|
+
NEXT_PUBLIC_APP_URL=https://your-domain.com
|
|
109
|
+
NEXT_PUBLIC_SITE_URL=https://your-domain.com # must equal APP_URL in prod
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Add a boot-time validator that refuses to run if any are missing or contain `your_`/`demo_`/`development`. See `references/oauth-setup.md#env-validation`.
|
|
113
|
+
|
|
114
|
+
## Quick Verification Commands
|
|
115
|
+
|
|
116
|
+
Before claiming auth works:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
# 1. Build succeeds
|
|
120
|
+
npm run build
|
|
121
|
+
|
|
122
|
+
# 2. Manifest file exists where Vercel expects it
|
|
123
|
+
ls .next/server/app/**/page_client-reference-manifest.js
|
|
124
|
+
|
|
125
|
+
# 3. Sign-in flow end to end (manual)
|
|
126
|
+
# - open /signin
|
|
127
|
+
# - click Google
|
|
128
|
+
# - grant consent
|
|
129
|
+
# - land on /dashboard
|
|
130
|
+
# - hard refresh — session persists
|
|
131
|
+
# - close browser, reopen — session persists for 7 days
|
|
132
|
+
# - sign out — cookies cleared, /dashboard redirects to /signin
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
"Build passed" ≠ "auth works". "Sign-in UI appears" ≠ "session persists". Test the full loop.
|
|
136
|
+
|
|
137
|
+
## Honest Certainty Markers
|
|
138
|
+
|
|
139
|
+
When wiring this up, report state with markers — NEVER "100% complete":
|
|
140
|
+
|
|
141
|
+
- ✅ Console configured (project exists, APIs enabled, consent screen published, credentials created)
|
|
142
|
+
- ✅ App env vars set (validated at boot)
|
|
143
|
+
- ✅ Sign-in UI renders
|
|
144
|
+
- ⚠️ Sign-in flow works in dev — not yet tested in prod
|
|
145
|
+
- ❓ Refresh-token storage — need to verify on 2nd-day session
|
|
146
|
+
|
|
147
|
+
## Latest Docs (April 2026)
|
|
148
|
+
|
|
149
|
+
Bookmark these — Google rewrites them regularly, and old blog posts lie.
|
|
150
|
+
|
|
151
|
+
- [OAuth 2.0 for Web Server Apps](https://developers.google.com/identity/protocols/oauth2/web-server) — the canonical auth flow doc
|
|
152
|
+
- [Using OAuth 2.0 (index)](https://developers.google.com/identity/protocols/oauth2) — scope & flow overview
|
|
153
|
+
- [Load Maps JS API](https://developers.google.com/maps/documentation/javascript/load-maps-js-api) — modern async loader
|
|
154
|
+
- [Advanced Markers migration](https://developers.google.com/maps/documentation/javascript/advanced-markers/migration) — required migration from deprecated `Marker`
|
|
155
|
+
- [Places API (New) overview](https://developers.google.com/maps/documentation/places/web-service/overview) — legacy is end-of-life for new projects
|
|
156
|
+
- [Calendar API sync guide](https://developers.google.com/workspace/calendar/api/guides/sync) — incremental sync tokens
|
|
157
|
+
- [Calendar API scopes](https://developers.google.com/workspace/calendar/api/auth) — pick the narrowest scope
|
|
158
|
+
|
|
159
|
+
When in doubt, use Context7 MCP (`context7__query-docs`) against library IDs for `@googlemaps/js-api-loader`, `@supabase/ssr`, `googleapis`.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
<!-- SPDX-FileCopyrightText: 2025-2026 Tyrone Ross, Jr <46267523+tyroneross@users.noreply.github.com> | SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
|
|
3
|
+
# Geocoding + Directions + Distance Matrix
|
|
4
|
+
|
|
5
|
+
Non-interactive, server-side APIs. Get an API key restricted to your server's IP (or unrestricted + used only via server routes).
|
|
6
|
+
|
|
7
|
+
## Geocoding — address ↔ lat/lng
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
async function geocode(address: string) {
|
|
11
|
+
const url = new URL('https://maps.googleapis.com/maps/api/geocode/json')
|
|
12
|
+
url.searchParams.set('address', address)
|
|
13
|
+
url.searchParams.set('key', process.env.GOOGLE_GEOCODING_API_KEY!)
|
|
14
|
+
|
|
15
|
+
const res = await fetch(url)
|
|
16
|
+
const json = await res.json()
|
|
17
|
+
|
|
18
|
+
if (json.status !== 'OK') {
|
|
19
|
+
throw new Error(`Geocode failed: ${json.status} ${json.error_message ?? ''}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const first = json.results[0]
|
|
23
|
+
return {
|
|
24
|
+
formattedAddress: first.formatted_address,
|
|
25
|
+
location: first.geometry.location, // { lat, lng }
|
|
26
|
+
placeId: first.place_id,
|
|
27
|
+
types: first.types,
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Status codes to handle:**
|
|
33
|
+
- `OK` — results found
|
|
34
|
+
- `ZERO_RESULTS` — valid request, no match (show "address not found")
|
|
35
|
+
- `OVER_QUERY_LIMIT` — rate limited, back off
|
|
36
|
+
- `REQUEST_DENIED` — key issue, log loudly
|
|
37
|
+
- `INVALID_REQUEST` — bad params
|
|
38
|
+
|
|
39
|
+
**Reverse geocoding** uses the same endpoint with `latlng=` instead of `address=`.
|
|
40
|
+
|
|
41
|
+
**Cache aggressively.** A given address's lat/lng doesn't change. Store results permanently in your DB keyed by normalized address.
|
|
42
|
+
|
|
43
|
+
## Directions
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
async function directions(origin: string, destination: string, mode: 'driving' | 'walking' | 'bicycling' | 'transit' = 'driving') {
|
|
47
|
+
const url = new URL('https://maps.googleapis.com/maps/api/directions/json')
|
|
48
|
+
url.searchParams.set('origin', origin)
|
|
49
|
+
url.searchParams.set('destination', destination)
|
|
50
|
+
url.searchParams.set('mode', mode)
|
|
51
|
+
url.searchParams.set('key', process.env.GOOGLE_DIRECTIONS_API_KEY!)
|
|
52
|
+
|
|
53
|
+
const res = await fetch(url)
|
|
54
|
+
const json = await res.json()
|
|
55
|
+
return json.routes[0] // legs, overview_polyline, duration, distance
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Directions returns an **encoded polyline** (`overview_polyline.points`) — decode client-side with `google.maps.geometry.encoding.decodePath()` (import the `geometry` library) to draw on a map.
|
|
60
|
+
|
|
61
|
+
## Distance Matrix — many-to-many
|
|
62
|
+
|
|
63
|
+
When you need "what's the drive time from home to each of these 5 camps":
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
async function distanceMatrix(origins: string[], destinations: string[]) {
|
|
67
|
+
const url = new URL('https://maps.googleapis.com/maps/api/distancematrix/json')
|
|
68
|
+
url.searchParams.set('origins', origins.join('|'))
|
|
69
|
+
url.searchParams.set('destinations', destinations.join('|'))
|
|
70
|
+
url.searchParams.set('mode', 'driving')
|
|
71
|
+
url.searchParams.set('key', process.env.GOOGLE_DIRECTIONS_API_KEY!)
|
|
72
|
+
|
|
73
|
+
const res = await fetch(url)
|
|
74
|
+
return res.json() // rows[i].elements[j] = { distance, duration, status }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Billing**: Distance Matrix is charged per element (origins × destinations). 3 origins × 5 destinations = 15 elements. Cap batch sizes to avoid surprise bills.
|
|
79
|
+
|
|
80
|
+
## Rate Limiting + Caching Strategy
|
|
81
|
+
|
|
82
|
+
Both Geocoding and Directions have generous free tiers but billing ramps fast. Build a simple cache layer:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
// lib/google-cache.ts
|
|
86
|
+
import { createHash } from 'crypto'
|
|
87
|
+
|
|
88
|
+
type CacheRow = { key: string; value: unknown; cached_at: Date }
|
|
89
|
+
|
|
90
|
+
async function cachedFetch<T>(
|
|
91
|
+
bucket: 'geocode' | 'directions',
|
|
92
|
+
keyParts: string[],
|
|
93
|
+
fetcher: () => Promise<T>,
|
|
94
|
+
ttlDays: number,
|
|
95
|
+
): Promise<T> {
|
|
96
|
+
const key = createHash('sha256').update(`${bucket}:${keyParts.join('|')}`).digest('hex')
|
|
97
|
+
|
|
98
|
+
const row = await db.query<CacheRow>(
|
|
99
|
+
'SELECT value, cached_at FROM google_cache WHERE key = $1',
|
|
100
|
+
[key],
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if (row && daysSince(row.cached_at) < ttlDays) {
|
|
104
|
+
return row.value as T
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const value = await fetcher()
|
|
108
|
+
await db.query(
|
|
109
|
+
'INSERT INTO google_cache (key, value, cached_at) VALUES ($1, $2, NOW()) ON CONFLICT (key) DO UPDATE SET value = $2, cached_at = NOW()',
|
|
110
|
+
[key, value],
|
|
111
|
+
)
|
|
112
|
+
return value
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
TTL suggestions:
|
|
117
|
+
- Geocode (address → lat/lng): **permanent** (addresses don't move)
|
|
118
|
+
- Reverse geocode: **90 days** (neighborhoods/places do change)
|
|
119
|
+
- Directions: **1 day** (traffic + road changes + route updates)
|
|
120
|
+
- Distance Matrix: **1 day**
|
|
121
|
+
|
|
122
|
+
## Sources
|
|
123
|
+
|
|
124
|
+
- [Geocoding API](https://developers.google.com/maps/documentation/geocoding/overview)
|
|
125
|
+
- [Directions API](https://developers.google.com/maps/documentation/directions/overview)
|
|
126
|
+
- [Distance Matrix API](https://developers.google.com/maps/documentation/distance-matrix/overview)
|